feat: conferencing atoms (#17988)
* added api/v2 for zoom connect * Update conferencing.controller.ts * fix: added api for ConferencingAppsViewPlatformWrapper * type fixes * added getBulkEventTypes api for bulk-update default location * added bulk-update-to-default-location * fix:bulk-update-to-default-location * invalidated * fixed returnTo and onErrorReturnTo * make logos work for conferencing atoms * smalll improvements * fixup * share common types * type fix, and fixed a typo * fix: type-error * fix: json.parse(json.parse()) * fix: invalidate query * fix: removing app throws error in the main app * undo platform-libraries-1.2.3 * update platform-libraries version --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.69",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.70",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -22,6 +22,15 @@ export class AppsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAppCredentials(credentialIdsToDelete: number[], userId: number) {
|
||||
return this.dbWrite.prisma.credential.deleteMany({
|
||||
where: {
|
||||
id: { in: credentialIdsToDelete },
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createTeamAppCredential(type: string, key: Prisma.InputJsonValue, teamId: number, appId: string) {
|
||||
return this.dbWrite.prisma.credential.create({
|
||||
data: {
|
||||
@@ -32,4 +41,28 @@ export class AppsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAppCredential({
|
||||
type,
|
||||
appId,
|
||||
userId,
|
||||
teamId,
|
||||
}: {
|
||||
type: string;
|
||||
appId: string;
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
}) {
|
||||
return this.dbWrite.prisma.credential.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
type,
|
||||
userId,
|
||||
teamId,
|
||||
appId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_0
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
@@ -15,6 +16,7 @@ import { Module } from "@nestjs/common";
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
|
||||
providers: [
|
||||
EventTypesAtomService,
|
||||
ConferencingAtomsService,
|
||||
MembershipsRepository,
|
||||
CredentialsRepository,
|
||||
UsersRepository,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { EventTypesAppInput } from "@/modules/atoms/inputs/event-types-app.input";
|
||||
import {
|
||||
BulkUpdateEventTypeToDefaultLocationDto,
|
||||
EventTypesAppInput,
|
||||
} from "@/modules/atoms/inputs/event-types-app.input";
|
||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -18,8 +22,9 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
|
||||
import { ConnectedApps } from "@calcom/platform-libraries";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
/*
|
||||
@@ -36,7 +41,10 @@ these endpoints should not be recommended for use by third party and are exclude
|
||||
@DocsTags("Atoms - endpoints for atoms")
|
||||
@DocsExcludeController(true)
|
||||
export class AtomsController {
|
||||
constructor(private readonly eventTypesService: EventTypesAtomService) {}
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesAtomService,
|
||||
private readonly conferencingService: ConferencingAtomsService
|
||||
) {}
|
||||
|
||||
@Get("event-types/:eventTypeId")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@@ -52,6 +60,17 @@ export class AtomsController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/event-types")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async getAtomEventTypes(@GetUser("id") userId: number): Promise<ApiResponse<unknown>> {
|
||||
const eventType = await this.eventTypesService.getUserEventTypes(userId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("event-types-app/:appSlug")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -84,6 +103,19 @@ export class AtomsController {
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/event-types/bulk-update-to-default-location")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async bulkUpdateAtomEventTypes(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() body: BulkUpdateEventTypeToDefaultLocationDto
|
||||
): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> {
|
||||
await this.eventTypesService.bulkUpdateEventTypesDefaultLocation(user, body.eventTypeIds);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("event-types/:eventTypeId")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -114,4 +146,13 @@ export class AtomsController {
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/conferencing")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async listInstalledConferencingApps(@GetUser() user: UserWithProfile): Promise<ApiResponse<ConnectedApps>> {
|
||||
const conferencingApps = await this.conferencingService.getConferencingApps(user);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: conferencingApps };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsNumber, IsOptional } from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { ArrayNotEmpty, IsArray, IsInt, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
export class EventTypesAppInput {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@@ -7,3 +7,10 @@ export class EventTypesAppInput {
|
||||
@IsOptional()
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export class BulkUpdateEventTypeToDefaultLocationDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
eventTypeIds!: number[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getConnectedApps, ConnectedApps } from "@calcom/platform-libraries";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class ConferencingAtomsService {
|
||||
private logger = new Logger("ConferencingAtomService");
|
||||
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getConferencingApps(user: UserWithProfile): Promise<ConnectedApps> {
|
||||
return getConnectedApps({
|
||||
user,
|
||||
input: {
|
||||
variant: "conferencing",
|
||||
onlyInstalled: true,
|
||||
},
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
getAppFromSlug,
|
||||
MembershipRole,
|
||||
EventTypeMetaDataSchema,
|
||||
getClientSecretFromPayment,
|
||||
getBulkEventTypes,
|
||||
bulkUpdateEventsToDefaultLocation,
|
||||
} from "@calcom/platform-libraries";
|
||||
import type {
|
||||
App,
|
||||
@@ -29,8 +32,7 @@ import type {
|
||||
TDependencyData,
|
||||
CredentialPayload,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { getClientSecretFromPayment } from "@calcom/platform-libraries";
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type EnabledAppType = App & {
|
||||
credential: CredentialDataWithTeamName;
|
||||
@@ -81,6 +83,10 @@ export class EventTypesAtomService {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getUserEventTypes(userId: number) {
|
||||
return getBulkEventTypes(userId);
|
||||
}
|
||||
|
||||
async updateTeamEventType(
|
||||
eventTypeId: number,
|
||||
body: TUpdateEventTypeInputSchema,
|
||||
@@ -317,4 +323,12 @@ export class EventTypesAtomService {
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
async bulkUpdateEventTypesDefaultLocation(user: UserWithProfile, eventTypeIds: number[]) {
|
||||
return bulkUpdateEventsToDefaultLocation({
|
||||
eventTypeIds,
|
||||
user,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { ConferencingController } from "@/modules/conferencing/controllers/conferencing.controller";
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, ConfigModule],
|
||||
providers: [
|
||||
ConferencingService,
|
||||
ConferencingRepository,
|
||||
GoogleMeetService,
|
||||
CredentialsRepository,
|
||||
UsersRepository,
|
||||
TokensRepository,
|
||||
ZoomVideoService,
|
||||
AppsRepository,
|
||||
],
|
||||
exports: [],
|
||||
controllers: [ConferencingController],
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import {
|
||||
ConferencingAppsOauthUrlOutputDto,
|
||||
GetConferencingAppsOauthUrlResponseDto,
|
||||
} from "@/modules/conferencing/outputs/get-conferencing-apps-oauth-url";
|
||||
import {
|
||||
ConferencingAppsOutputResponseDto,
|
||||
ConferencingAppOutputResponseDto,
|
||||
ConferencingAppsOutputDto,
|
||||
DisconnectConferencingAppOutputResponseDto,
|
||||
} from "@/modules/conferencing/outputs/get-conferencing-apps.output";
|
||||
import { GetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/get-default-conferencing-app.output";
|
||||
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
@@ -20,11 +30,24 @@ import {
|
||||
Param,
|
||||
BadRequestException,
|
||||
Delete,
|
||||
Headers,
|
||||
Redirect,
|
||||
UnauthorizedException,
|
||||
Req,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
|
||||
import { CONFERENCING_APPS, GOOGLE_MEET, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export type OAuthCallbackState = {
|
||||
accessToken: string;
|
||||
teamId?: number;
|
||||
fromApp?: boolean;
|
||||
returnTo?: string;
|
||||
onErrorReturnTo?: string;
|
||||
};
|
||||
|
||||
@Controller({
|
||||
path: "/v2/conferencing",
|
||||
@@ -35,8 +58,10 @@ export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly conferencingService: ConferencingService,
|
||||
private readonly googleMeetService: GoogleMeetService
|
||||
private readonly googleMeetService: GoogleMeetService,
|
||||
private readonly zoomVideoService: ZoomVideoService
|
||||
) {}
|
||||
|
||||
@Post("/:app/connect")
|
||||
@@ -56,16 +81,89 @@ export class ConferencingController {
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
[GOOGLE_MEET].join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get("/:app/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Get oauth conferencing app auth url" })
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Param("app") app: string,
|
||||
@Query("returnTo") returnTo?: string,
|
||||
@Query("onErrorReturnTo") onErrorReturnTo?: string
|
||||
): Promise<GetConferencingAppsOauthUrlResponseDto> {
|
||||
let credential;
|
||||
const origin = req.headers.origin;
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
|
||||
const state: OAuthCallbackState = {
|
||||
returnTo: returnTo ?? origin,
|
||||
onErrorReturnTo: onErrorReturnTo ?? origin,
|
||||
fromApp: false,
|
||||
accessToken,
|
||||
};
|
||||
|
||||
switch (app) {
|
||||
case ZOOM:
|
||||
credential = await this.zoomVideoService.generateZoomAuthUrl(JSON.stringify(state));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
|
||||
};
|
||||
|
||||
default:
|
||||
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
@Get("/:app/oauth/callback")
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "conferencing apps oauths callback" })
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Param("app") app: string,
|
||||
@Query("code") code: string,
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<{ url: string }> {
|
||||
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
||||
try {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
|
||||
if (error) {
|
||||
throw new BadRequestException(error_description);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException("Invalid Access token.");
|
||||
}
|
||||
|
||||
switch (app) {
|
||||
case ZOOM:
|
||||
return await this.zoomVideoService.connectZoomApp(decodedCallbackState, code, userId);
|
||||
|
||||
default:
|
||||
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
url: decodedCallbackState.onErrorReturnTo ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "List your conferencing applications" })
|
||||
async listConferencingApps(@GetUser("id") userId: number): Promise<ConferencingAppsOutputResponseDto> {
|
||||
async listInstalledConferencingApps(
|
||||
@GetUser("id") userId: number
|
||||
): Promise<ConferencingAppsOutputResponseDto> {
|
||||
const conferencingApps = await this.conferencingService.getConferencingApps(userId);
|
||||
|
||||
const data = conferencingApps.map((conferencingApps) =>
|
||||
@@ -83,18 +181,17 @@ export class ConferencingController {
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
): Promise<SetDefaultConferencingAppOutputResponseDto> {
|
||||
switch (app) {
|
||||
case GOOGLE_MEET:
|
||||
await this.googleMeetService.setDefault(userId);
|
||||
await this.conferencingService.setDefaultConferencingApp(userId, app);
|
||||
return { status: SUCCESS_STATUS };
|
||||
}
|
||||
|
||||
return { status: SUCCESS_STATUS };
|
||||
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
);
|
||||
}
|
||||
@Get("/default")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Get your default conferencing application" })
|
||||
async getDefault(@GetUser("id") userId: number): Promise<GetDefaultConferencingAppOutputResponseDto> {
|
||||
const defaultconferencingApp = await this.conferencingService.getUserDefaultConferencingApp(userId);
|
||||
return { status: SUCCESS_STATUS, data: defaultconferencingApp };
|
||||
}
|
||||
|
||||
@Delete("/:app/disconnect")
|
||||
@@ -102,20 +199,10 @@ export class ConferencingController {
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
||||
async disconnect(
|
||||
@GetUser("id") userId: number,
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
): Promise<ConferencingAppOutputResponseDto> {
|
||||
switch (app) {
|
||||
case GOOGLE_MEET:
|
||||
const credential = await this.googleMeetService.disconnectGoogleMeetApp(userId);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
|
||||
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
CONFERENCING_APPS.join(", ")
|
||||
);
|
||||
}
|
||||
): Promise<DisconnectConferencingAppOutputResponseDto> {
|
||||
await this.conferencingService.disconnectConferencingApp(user, app);
|
||||
return { status: SUCCESS_STATUS };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class ConferencingAppsOauthUrlOutputDto {
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly url!: string;
|
||||
}
|
||||
|
||||
export class GetConferencingAppsOauthUrlResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ConferencingAppsOauthUrlOutputDto)
|
||||
data!: ConferencingAppsOauthUrlOutputDto;
|
||||
}
|
||||
@@ -48,3 +48,9 @@ export class ConferencingAppOutputResponseDto {
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
data!: ConferencingAppsOutputDto;
|
||||
}
|
||||
|
||||
export class DisconnectConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class DefaultConferencingAppsOutputDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
readonly appSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
readonly appLink?: string;
|
||||
}
|
||||
|
||||
export class GetDefaultConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => DefaultConferencingAppsOutputDto)
|
||||
data?: DefaultConferencingAppsOutputDto;
|
||||
}
|
||||
@@ -22,4 +22,10 @@ export class ConferencingRepository {
|
||||
where: { userId, type: GOOGLE_MEET_TYPE },
|
||||
});
|
||||
}
|
||||
|
||||
async findConferencingApp(userId: number, app: string) {
|
||||
return this.dbRead.prisma.credential.findFirst({
|
||||
where: { userId, appId: app },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,61 @@
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CONFERENCING_APPS, CAL_VIDEO } from "@calcom/platform-constants";
|
||||
import { userMetadata, handleDeleteCredential } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class ConferencingService {
|
||||
private logger = new Logger("ConferencingService");
|
||||
|
||||
constructor(private readonly conferencingRepository: ConferencingRepository) {}
|
||||
constructor(
|
||||
private readonly conferencingRepository: ConferencingRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async getConferencingApps(userId: number) {
|
||||
return this.conferencingRepository.findConferencingApps(userId);
|
||||
}
|
||||
|
||||
async getUserDefaultConferencingApp(userId: number) {
|
||||
const user = await this.usersRepository.findById(userId);
|
||||
return userMetadata.parse(user?.metadata)?.defaultConferencingApp;
|
||||
}
|
||||
|
||||
async checkAppIsValidAndConnected(userId: number, app: string) {
|
||||
if (!CONFERENCING_APPS.includes(app)) {
|
||||
throw new BadRequestException("Invalid app, available apps are: ", CONFERENCING_APPS.join(", "));
|
||||
}
|
||||
const credential = await this.conferencingRepository.findConferencingApp(userId, app);
|
||||
|
||||
if (!credential) {
|
||||
throw new BadRequestException(`${app} not connected.`);
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
||||
async disconnectConferencingApp(user: UserWithProfile, app: string) {
|
||||
const credential = await this.checkAppIsValidAndConnected(user.id, app);
|
||||
return handleDeleteCredential({
|
||||
userId: user.id,
|
||||
userMetadata: user?.metadata,
|
||||
credentialId: credential.id,
|
||||
});
|
||||
}
|
||||
|
||||
async setDefaultConferencingApp(userId: number, app: string) {
|
||||
// cal-video is global, so we can skip this check
|
||||
if (app !== CAL_VIDEO) {
|
||||
await this.checkAppIsValidAndConnected(userId, app);
|
||||
}
|
||||
const user = await this.usersRepository.setDefaultConferencingApp(userId, app);
|
||||
const metadata = user.metadata as { defaultConferencingApp?: { appSlug?: string } };
|
||||
if (metadata?.defaultConferencingApp?.appSlug !== app) {
|
||||
throw new InternalServerErrorException(`Could not set ${app} as default conferencing app`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,29 +43,4 @@ export class GoogleMeetService {
|
||||
|
||||
return googleMeetCredential;
|
||||
}
|
||||
|
||||
async disconnectGoogleMeetApp(userId: number) {
|
||||
const googleMeet = await this.conferencingRepository.findGoogleMeet(userId);
|
||||
|
||||
if (!googleMeet) {
|
||||
throw new BadRequestException("Google Meet is not connected.");
|
||||
}
|
||||
|
||||
const googleMeetCredential = await this.credentialsRepository.deleteUserCredentialById(
|
||||
userId,
|
||||
googleMeet.id
|
||||
);
|
||||
|
||||
return googleMeetCredential;
|
||||
}
|
||||
|
||||
async setDefault(userId: number) {
|
||||
const user = await this.usersRepository.setDefaultConferencingApp(userId, GOOGLE_MEET);
|
||||
const metadata = user.metadata as { defaultConferencingApp?: { appSlug?: string } };
|
||||
|
||||
if (metadata?.defaultConferencingApp?.appSlug !== GOOGLE_MEET) {
|
||||
throw new InternalServerErrorException("Could not set Google Meet as default conferencing app");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { OAuthCallbackState } from "@/modules/conferencing/controllers/conferencing.controller";
|
||||
import { BadRequestException, Logger, NotFoundException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZOOM, ZOOM_TYPE } from "@calcom/platform-constants";
|
||||
|
||||
import stringify = require("qs-stringify");
|
||||
|
||||
const zoomAppKeysSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class ZoomVideoService {
|
||||
private logger = new Logger("ZoomVideoService");
|
||||
private redirectUri = `${this.config.get("api.url")}/conferencing/zoom/oauth/callback`;
|
||||
|
||||
constructor(private readonly config: ConfigService, private readonly appsRepository: AppsRepository) {}
|
||||
|
||||
async getZoomAppKeys() {
|
||||
const app = await this.appsRepository.getAppBySlug(ZOOM);
|
||||
|
||||
const { client_id, client_secret } = zoomAppKeysSchema.parse(app?.keys);
|
||||
|
||||
if (!client_id) {
|
||||
throw new NotFoundException("Zoom app not found");
|
||||
}
|
||||
|
||||
if (!client_secret) {
|
||||
throw new NotFoundException("Zoom app not found");
|
||||
}
|
||||
|
||||
return { client_id, client_secret };
|
||||
}
|
||||
|
||||
async generateZoomAuthUrl(state: string) {
|
||||
const { client_id } = await this.getZoomAppKeys();
|
||||
|
||||
const params = {
|
||||
response_type: "code",
|
||||
client_id,
|
||||
redirect_uri: this.redirectUri,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const query = stringify(params);
|
||||
const url = `https://zoom.us/oauth/authorize?${query}`;
|
||||
return { url };
|
||||
}
|
||||
|
||||
async connectZoomApp(state: OAuthCallbackState, code: string, userId: number) {
|
||||
const { client_id, client_secret } = await this.getZoomAppKeys();
|
||||
const redirectUri = encodeURI(this.redirectUri);
|
||||
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
|
||||
const result = await fetch(
|
||||
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (result.status !== 200) {
|
||||
let errorMessage = "Something is wrong with Zoom API";
|
||||
try {
|
||||
const responseBody = await result.json();
|
||||
errorMessage = responseBody.error;
|
||||
} catch (e) {
|
||||
errorMessage = await result.clone().text();
|
||||
}
|
||||
throw new BadRequestException(errorMessage);
|
||||
}
|
||||
|
||||
const responseBody = await result.json();
|
||||
|
||||
if (responseBody.error) {
|
||||
throw new BadRequestException(responseBody.error);
|
||||
}
|
||||
|
||||
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||
delete responseBody.expires_in;
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException("Invalid Access token.");
|
||||
}
|
||||
const existingCredentialZoomVideo = await this.appsRepository.findAppCredential({
|
||||
type: ZOOM_TYPE,
|
||||
userId,
|
||||
appId: ZOOM,
|
||||
});
|
||||
|
||||
const credentialIdsToDelete = existingCredentialZoomVideo.map((item) => item.id);
|
||||
if (credentialIdsToDelete.length > 0) {
|
||||
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
|
||||
}
|
||||
|
||||
await this.appsRepository.createAppCredential(
|
||||
ZOOM_TYPE,
|
||||
responseBody as unknown as Prisma.InputJsonObject,
|
||||
userId,
|
||||
ZOOM
|
||||
);
|
||||
|
||||
return { url: state.returnTo ?? "" };
|
||||
}
|
||||
}
|
||||
@@ -3862,9 +3862,104 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_redirect",
|
||||
"summary": "Get oauth conferencing app auth url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "returnTo",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onErrorReturnTo",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetConferencingAppsOauthUrlResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Conferencing"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/oauth/callback": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_save",
|
||||
"summary": "conferencing apps oauths callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "state",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Conferencing"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_listConferencingApps",
|
||||
"operationId": "ConferencingController_listInstalledConferencingApps",
|
||||
"summary": "List your conferencing applications",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
@@ -3915,6 +4010,28 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/default": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_getDefault",
|
||||
"summary": "Get your default conferencing application",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetDefaultConferencingAppOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Conferencing"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/disconnect": {
|
||||
"delete": {
|
||||
"operationId": "ConferencingController_disconnect",
|
||||
@@ -3935,7 +4052,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConferencingAppOutputResponseDto"
|
||||
"$ref": "#/components/schemas/DisconnectConferencingAppOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14726,6 +14843,22 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"GetConferencingAppsOauthUrlResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"ConferencingAppsOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14764,6 +14897,52 @@
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"DefaultConferencingAppsOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appSlug": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDefaultConferencingAppOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/DefaultConferencingAppsOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"DisconnectConferencingAppOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useReducer } from "react";
|
||||
|
||||
import getAppCategoryTitle from "@calcom/app-store/_utils/getAppCategoryTitle";
|
||||
import type { UpdateDefaultConferencingAppParams } from "@calcom/features/apps/components/AppList";
|
||||
import { AppList } from "@calcom/features/apps/components/AppList";
|
||||
import { AppList, type HandleDisconnect } from "@calcom/features/apps/components/AppList";
|
||||
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import type { RemoveAppParams } from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
@@ -31,7 +31,7 @@ import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||
interface IntegrationsContainerProps {
|
||||
variant?: AppCategories;
|
||||
exclude?: AppCategories[];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
handleDisconnect: HandleDisconnect;
|
||||
}
|
||||
|
||||
const IntegrationsContainer = ({
|
||||
@@ -54,17 +54,25 @@ const IntegrationsContainer = ({
|
||||
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation();
|
||||
|
||||
const handleUpdateDefaultConferencingApp = ({ appSlug, callback }: UpdateDefaultConferencingAppParams) => {
|
||||
const { data: eventTypesQueryData, isFetching: isEventTypesFetching } =
|
||||
trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
|
||||
const handleUpdateUserDefaultConferencingApp = ({
|
||||
appSlug,
|
||||
onSuccessCallback,
|
||||
onErrorCallback,
|
||||
}: UpdateUsersDefaultConferencingAppParams) => {
|
||||
updateDefaultAppMutation.mutate(
|
||||
{ appSlug },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast("Default app updated successfully", "success");
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
callback();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
onErrorCallback();
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -84,6 +92,14 @@ const IntegrationsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleConnectDisconnectIntegrationMenuToggle = () => {
|
||||
utils.viewer.integrations.invalidate();
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
// TODO: Refactor and reuse getAppCategories?
|
||||
const emptyIcon: Record<AppCategories, React.ComponentProps<typeof Icon>["name"]> = {
|
||||
calendar: "calendar",
|
||||
@@ -146,9 +162,13 @@ const IntegrationsContainer = ({
|
||||
data={data}
|
||||
variant={variant}
|
||||
defaultConferencingApp={defaultConferencingApp}
|
||||
handleUpdateDefaultConferencingApp={handleUpdateDefaultConferencingApp}
|
||||
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
|
||||
handleBulkUpdateDefaultLocation={handleBulkUpdateDefaultLocation}
|
||||
isBulkUpdateDefaultLocationPending={updateDefaultAppMutation.isPending}
|
||||
eventTypes={eventTypesQueryData?.eventTypes}
|
||||
isEventTypesFetching={isEventTypesFetching}
|
||||
handleConnectDisconnectIntegrationMenuToggle={handleConnectDisconnectIntegrationMenuToggle}
|
||||
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -187,7 +207,7 @@ export default function InstalledApps(props: PageProps) {
|
||||
updateData({ isOpen: false, credentialId: null });
|
||||
};
|
||||
|
||||
const handleDisconnect = (credentialId: number, teamId?: number) => {
|
||||
const handleDisconnect = (credentialId: number, app: string, teamId?: number) => {
|
||||
updateData({ isOpen: true, credentialId, teamId });
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
const bulkUpdateDefaultAvailabilityMutation =
|
||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation();
|
||||
|
||||
const { data: eventTypesQueryData, isFetching: isEventTypesFetching } =
|
||||
trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
|
||||
const bulkUpdateFunction = ({ eventTypeIds, callback }: BulkUpdatParams) => {
|
||||
bulkUpdateDefaultAvailabilityMutation.mutate(
|
||||
{
|
||||
@@ -65,6 +68,10 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
const isDefaultSchedule = me.data?.defaultScheduleId === scheduleId;
|
||||
|
||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||
@@ -142,6 +149,9 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
setIsOpen: setIsBulkUpdateModalOpen,
|
||||
save: bulkUpdateFunction,
|
||||
isSaving: bulkUpdateDefaultAvailabilityMutation.isPending,
|
||||
eventTypes: eventTypesQueryData?.eventTypes,
|
||||
isEventTypesFetching,
|
||||
handleBulkEditDialogToggle: handleBulkEditDialogToggle,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -83,6 +83,9 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
const bulkUpdateDefaultAvailabilityMutation =
|
||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation();
|
||||
|
||||
const { data: eventTypesQueryData, isFetching: isEventTypesFetching } =
|
||||
trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
|
||||
const bulkUpdateFunction = ({ eventTypeIds, callback }: BulkUpdatParams) => {
|
||||
bulkUpdateDefaultAvailabilityMutation.mutate(
|
||||
{
|
||||
@@ -98,6 +101,10 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
);
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push(`/availability/${schedule.id}`);
|
||||
@@ -161,6 +168,9 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
setOpen={setBulkUpdateModal}
|
||||
bulkUpdateFunction={bulkUpdateFunction}
|
||||
description={t("default_schedules_bulk_description")}
|
||||
eventTypes={eventTypesQueryData?.eventTypes}
|
||||
isEventTypesFetching={isEventTypesFetching}
|
||||
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -9,15 +9,7 @@ import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListIt
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
EmptyScreen,
|
||||
Meta,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, EmptyScreen, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
|
||||
@@ -2834,6 +2834,8 @@
|
||||
"multi_select": "Multi Select",
|
||||
"group_meeting": "Group Meeting",
|
||||
"salesforce_ignore_guests": "Do not create new records for guests added to the booking",
|
||||
"google_meet": "Google Meet",
|
||||
"zoom": "Zoom",
|
||||
"lock_attribute_for_assignment": "Lock for assignment",
|
||||
"lock_attribute_for_assignment_description": "Locking would only allow assignments from Directory Sync",
|
||||
"attribute_edited_successfully": "Attribute edited successfully",
|
||||
|
||||
@@ -3662,9 +3662,100 @@
|
||||
"tags": ["Conferencing"]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_redirect",
|
||||
"summary": "Get oauth conferencing app auth url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "returnTo",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onErrorReturnTo",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetConferencingAppsOauthUrlResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Conferencing"]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/oauth/callback": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_save",
|
||||
"summary": "conferencing apps oauths callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "state",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": ["Conferencing"]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_listConferencingApps",
|
||||
"operationId": "ConferencingController_listInstalledConferencingApps",
|
||||
"summary": "List your conferencing applications",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
@@ -3711,6 +3802,26 @@
|
||||
"tags": ["Conferencing"]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/default": {
|
||||
"get": {
|
||||
"operationId": "ConferencingController_getDefault",
|
||||
"summary": "Get your default conferencing application",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetDefaultConferencingAppOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Conferencing"]
|
||||
}
|
||||
},
|
||||
"/v2/conferencing/{app}/disconnect": {
|
||||
"delete": {
|
||||
"operationId": "ConferencingController_disconnect",
|
||||
@@ -3731,7 +3842,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConferencingAppOutputResponseDto"
|
||||
"$ref": "#/components/schemas/DisconnectConferencingAppOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13210,6 +13321,17 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"GetConferencingAppsOauthUrlResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
},
|
||||
"ConferencingAppsOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13237,6 +13359,42 @@
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
},
|
||||
"DefaultConferencingAppsOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appSlug": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDefaultConferencingAppOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/DefaultConferencingAppsOutputDto"
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
},
|
||||
"DisconnectConferencingAppOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { getLocationFromApp, type EventLocationType } from "@calcom/app-store/locations";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import AppListCard from "@calcom/features/apps/components/AppListCard";
|
||||
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import type {
|
||||
BulkUpdatParams,
|
||||
EventTypes,
|
||||
} from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
import { type RouterOutputs } from "@calcom/trpc";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import {
|
||||
Alert,
|
||||
@@ -24,17 +28,21 @@ import {
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
|
||||
export type UpdateDefaultConferencingAppParams = { appSlug: string; callback: () => void };
|
||||
export type HandleDisconnect = (credentialId: number, app: App["slug"], teamId?: number) => void;
|
||||
|
||||
interface AppListProps {
|
||||
variant?: AppCategories;
|
||||
data: RouterOutputs["viewer"]["integrations"];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
handleDisconnect: HandleDisconnect;
|
||||
listClassName?: string;
|
||||
defaultConferencingApp: RouterOutputs["viewer"]["getUsersDefaultConferencingApp"];
|
||||
handleUpdateDefaultConferencingApp: (params: UpdateDefaultConferencingAppParams) => void;
|
||||
handleUpdateUserDefaultConferencingApp: (params: UpdateUsersDefaultConferencingAppParams) => void;
|
||||
handleBulkUpdateDefaultLocation: (params: BulkUpdatParams) => void;
|
||||
isBulkUpdateDefaultLocationPending: boolean;
|
||||
eventTypes?: EventTypes;
|
||||
isEventTypesFetching?: boolean;
|
||||
handleConnectDisconnectIntegrationMenuToggle: () => void;
|
||||
handleBulkEditDialogToggle: () => void;
|
||||
}
|
||||
|
||||
export const AppList = ({
|
||||
@@ -43,15 +51,18 @@ export const AppList = ({
|
||||
variant,
|
||||
listClassName,
|
||||
defaultConferencingApp,
|
||||
handleUpdateDefaultConferencingApp,
|
||||
handleUpdateUserDefaultConferencingApp,
|
||||
handleBulkUpdateDefaultLocation,
|
||||
isBulkUpdateDefaultLocationPending,
|
||||
eventTypes,
|
||||
isEventTypesFetching,
|
||||
handleConnectDisconnectIntegrationMenuToggle,
|
||||
handleBulkEditDialogToggle,
|
||||
}: AppListProps) => {
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const onSuccessCallback = useCallback(() => {
|
||||
setBulkUpdateModal(true);
|
||||
showToast("Default app updated successfully", "success");
|
||||
@@ -98,9 +109,12 @@ export const AppList = ({
|
||||
if (locationType?.linkType === "static") {
|
||||
setLocationType({ ...locationType, slug: appSlug });
|
||||
} else {
|
||||
handleUpdateDefaultConferencingApp({
|
||||
handleUpdateUserDefaultConferencingApp({
|
||||
appSlug,
|
||||
callback: () => setBulkUpdateModal(true),
|
||||
onSuccessCallback: () => setBulkUpdateModal(true),
|
||||
onErrorCallback: () => {
|
||||
return;
|
||||
},
|
||||
});
|
||||
}
|
||||
}}>
|
||||
@@ -111,11 +125,15 @@ export const AppList = ({
|
||||
<ConnectOrDisconnectIntegrationMenuItem
|
||||
credentialId={item.credentialOwner?.credentialId || item.userCredentialIds[0]}
|
||||
type={item.type}
|
||||
app={item.slug}
|
||||
isGlobal={item.isGlobal}
|
||||
installed
|
||||
invalidCredentialIds={item.invalidCredentialIds}
|
||||
handleDisconnect={handleDisconnect}
|
||||
teamId={item.credentialOwner ? item.credentialOwner?.teamId : undefined}
|
||||
handleConnectDisconnectIntegrationMenuToggle={
|
||||
handleConnectDisconnectIntegrationMenuToggle
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
@@ -171,6 +189,7 @@ export const AppList = ({
|
||||
locationType={locationType}
|
||||
setLocationType={() => setLocationType(undefined)}
|
||||
onSuccess={onSuccessCallback}
|
||||
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -181,6 +200,9 @@ export const AppList = ({
|
||||
setOpen={setBulkUpdateModal}
|
||||
isPending={isBulkUpdateDefaultLocationPending}
|
||||
description={t("default_conferencing_bulk_description")}
|
||||
eventTypes={eventTypes}
|
||||
isEventTypesFetching={isEventTypesFetching}
|
||||
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -194,22 +216,19 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
installed?: boolean;
|
||||
invalidCredentialIds?: number[];
|
||||
teamId?: number;
|
||||
handleDisconnect: (credentialId: number, teamId?: number) => void;
|
||||
app: App["slug"];
|
||||
handleDisconnect: HandleDisconnect;
|
||||
handleConnectDisconnectIntegrationMenuToggle: () => void;
|
||||
}) {
|
||||
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
|
||||
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId, app } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const handleOpenChange = () => {
|
||||
utils.viewer.integrations.invalidate();
|
||||
};
|
||||
|
||||
if (credentialId || type === "stripe_payment" || isGlobal) {
|
||||
return (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => handleDisconnect(credentialId, teamId)}
|
||||
onClick={() => handleDisconnect(credentialId, app, teamId)}
|
||||
disabled={isGlobal}
|
||||
StartIcon="trash">
|
||||
{t("remove_app")}
|
||||
@@ -234,7 +253,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
{t("install")}
|
||||
</Button>
|
||||
)}
|
||||
onChanged={handleOpenChange}
|
||||
onChanged={props.handleConnectDisconnectIntegrationMenuToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ export type AppListCardProps = {
|
||||
const schema = z.object({ hl: z.string().optional() });
|
||||
|
||||
function AppListCardPlatformWrapper(props: AppListCardProps) {
|
||||
return <AppListCardComponent {...props} />;
|
||||
const logo = `https://app.cal.com${props.logo}`;
|
||||
return <AppListCardComponent {...props} logo={logo} />;
|
||||
}
|
||||
function AppListCardWebWrapper(props: AppListCardProps) {
|
||||
const { slug, shouldHighlight } = props;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { z } from "zod";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -18,6 +17,13 @@ import {
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
|
||||
export type UpdateUsersDefaultConferencingAppParams = {
|
||||
appSlug: string;
|
||||
appLink?: string;
|
||||
onSuccessCallback: () => void;
|
||||
onErrorCallback: () => void;
|
||||
};
|
||||
|
||||
type LocationTypeSetLinkDialogFormProps = {
|
||||
link?: string;
|
||||
type: EventLocationType["type"];
|
||||
@@ -27,10 +33,12 @@ export function AppSetDefaultLinkDialog({
|
||||
locationType,
|
||||
setLocationType,
|
||||
onSuccess,
|
||||
handleUpdateUserDefaultConferencingApp,
|
||||
}: {
|
||||
locationType: EventLocationType & { slug: string };
|
||||
setLocationType: Dispatch<SetStateAction<(EventLocationType & { slug: string }) | undefined>>;
|
||||
onSuccess: () => void;
|
||||
handleUpdateUserDefaultConferencingApp: (params: UpdateUsersDefaultConferencingAppParams) => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const eventLocationTypeOptions = getEventLocationType(locationType.type);
|
||||
@@ -41,15 +49,6 @@ export function AppSetDefaultLinkDialog({
|
||||
),
|
||||
});
|
||||
|
||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(`Invalid App Link Format`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={!!locationType} onOpenChange={() => setLocationType(undefined)}>
|
||||
<DialogContent
|
||||
@@ -60,9 +59,15 @@ export function AppSetDefaultLinkDialog({
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
updateDefaultAppMutation.mutate({
|
||||
handleUpdateUserDefaultConferencingApp({
|
||||
appSlug: locationType.slug,
|
||||
appLink: values.link,
|
||||
onSuccessCallback: () => {
|
||||
onSuccess();
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
showToast(`Invalid App Link Format`, "error");
|
||||
},
|
||||
});
|
||||
setLocationType(undefined);
|
||||
}}>
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import { useIsPlatform } from "@calcom/atoms/monorepo";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Dialog, ConfirmationDialogContent } from "@calcom/ui";
|
||||
|
||||
export type RemoveAppParams = { credentialId: number; teamId?: number; callback: () => void };
|
||||
export type RemoveAppParams = {
|
||||
credentialId: number;
|
||||
app?: App["slug"];
|
||||
teamId?: number;
|
||||
callback: () => void;
|
||||
};
|
||||
interface DisconnectIntegrationModalProps {
|
||||
credentialId: number | null;
|
||||
isOpen: boolean;
|
||||
handleModelClose: () => void;
|
||||
teamId?: number;
|
||||
handleRemoveApp: (params: RemoveAppParams) => void;
|
||||
app?: App["slug"] | null;
|
||||
}
|
||||
|
||||
export default function DisconnectIntegrationModal({
|
||||
credentialId,
|
||||
app,
|
||||
isOpen,
|
||||
handleModelClose,
|
||||
teamId,
|
||||
handleRemoveApp,
|
||||
}: DisconnectIntegrationModalProps) {
|
||||
const { t } = useLocale();
|
||||
const isPlatform = useIsPlatform();
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleModelClose}>
|
||||
<ConfirmationDialogContent
|
||||
@@ -25,7 +35,9 @@ export default function DisconnectIntegrationModal({
|
||||
title={t("remove_app")}
|
||||
confirmBtnText={t("yes_remove_app")}
|
||||
onConfirm={() => {
|
||||
if (credentialId) {
|
||||
if (isPlatform && app && credentialId) {
|
||||
handleRemoveApp({ credentialId, app, teamId, callback: () => handleModelClose() });
|
||||
} else if (credentialId) {
|
||||
handleRemoveApp({ credentialId, teamId, callback: () => handleModelClose() });
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -39,7 +39,7 @@ const handleDeleteCredential = async ({
|
||||
teamId,
|
||||
}: {
|
||||
userId: number;
|
||||
userMetadata: Prisma.JsonValue;
|
||||
userMetadata?: Prisma.JsonValue;
|
||||
credentialId: number;
|
||||
teamId?: number;
|
||||
}) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { Meta, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
export const AppearanceSkeletonLoader = () => {
|
||||
return (
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
Icon,
|
||||
showToast,
|
||||
useMeta,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { DeleteAttributeModal } from "./DeleteAttributeModal";
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button, Form, Meta, showToast, SettingsToggle } from "@calcom/ui";
|
||||
import { Button, Form, showToast, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
import ThemeLabel from "../../../settings/ThemeLabel";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import { TeamsListing } from "../components";
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import DisableTeamImpersonation from "../components/DisableTeamImpersonation";
|
||||
import InviteLinkSettingsModal from "../components/InviteLinkSettingsModal";
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
ImageUploader,
|
||||
Label,
|
||||
LinkIconButton,
|
||||
Meta,
|
||||
showToast,
|
||||
SkeletonAvatar,
|
||||
SkeletonButton,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogContent, Form, DialogFooter, DialogClose, Button, CheckboxField } from "@calcom/ui";
|
||||
|
||||
export const BulkUpdateEventSchema = z.object({
|
||||
@@ -11,28 +10,35 @@ export const BulkUpdateEventSchema = z.object({
|
||||
});
|
||||
|
||||
export type BulkUpdatParams = { eventTypeIds: number[]; callback: () => void };
|
||||
export type EventTypes = Array<{ id: number; title: string }>;
|
||||
|
||||
export function BulkEditDefaultForEventsModal(props: {
|
||||
export function BulkEditDefaultForEventsModal({
|
||||
eventTypes,
|
||||
isEventTypesFetching,
|
||||
...props
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
bulkUpdateFunction: (params: BulkUpdatParams) => void;
|
||||
isPending: boolean;
|
||||
description: string;
|
||||
isEventTypesFetching?: boolean;
|
||||
eventTypes?: EventTypes;
|
||||
handleBulkEditDialogToggle: () => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { data, isFetching } = trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(BulkUpdateEventSchema),
|
||||
defaultValues: {
|
||||
eventTypeIds: data?.eventTypes.map((e) => e.id) ?? [],
|
||||
eventTypeIds: eventTypes?.map((e) => e.id) ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesSelected = form.watch("eventTypeIds");
|
||||
const isButtonDisabled = eventTypesSelected.length === 0;
|
||||
|
||||
if (isFetching || !open || !data?.eventTypes) return null;
|
||||
if (isEventTypesFetching || !open || !eventTypes) return null;
|
||||
|
||||
return (
|
||||
<Dialog name="Bulk Default Location Update" open={props.open} onOpenChange={props.setOpen}>
|
||||
@@ -50,19 +56,19 @@ export function BulkEditDefaultForEventsModal(props: {
|
||||
});
|
||||
}}>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{data.eventTypes.length > 0 && (
|
||||
{eventTypes.length > 0 && (
|
||||
<div className="flex items-center space-x-2 rounded-md px-3 pb-2.5 pt-1">
|
||||
<CheckboxField
|
||||
description={t("select_all")}
|
||||
descriptionAsLabel
|
||||
onChange={(e) => {
|
||||
form.setValue("eventTypeIds", e.target.checked ? data.eventTypes.map((e) => e.id) : []);
|
||||
form.setValue("eventTypeIds", e.target.checked ? eventTypes.map((e) => e.id) : []);
|
||||
}}
|
||||
checked={eventTypesSelected.length === data.eventTypes.length}
|
||||
checked={eventTypesSelected.length === eventTypes.length}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{data.eventTypes.map((eventType) => (
|
||||
{eventTypes.map((eventType) => (
|
||||
<div key={eventType.id} className="bg-muted flex items-center space-x-2 rounded-md px-3 py-2.5">
|
||||
<CheckboxField
|
||||
description={eventType.title}
|
||||
@@ -83,7 +89,7 @@ export function BulkEditDefaultForEventsModal(props: {
|
||||
<DialogFooter showDivider className="mt-10">
|
||||
<DialogClose
|
||||
onClick={() => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
props.handleBulkEditDialogToggle();
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" color="primary" loading={props.isPending} disabled={isButtonDisabled}>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const bulkUpdateEventsToDefaultLocation = async ({
|
||||
eventTypeIds,
|
||||
user,
|
||||
prisma,
|
||||
}: {
|
||||
eventTypeIds: number[];
|
||||
user: Pick<User, "id" | "metadata">;
|
||||
prisma: PrismaClient;
|
||||
}) => {
|
||||
const defaultApp = userMetadataSchema.parse(user.metadata)?.defaultConferencingApp;
|
||||
|
||||
if (!defaultApp) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Default conferencing app not set",
|
||||
});
|
||||
}
|
||||
|
||||
const foundApp = getAppFromSlug(defaultApp.appSlug);
|
||||
const appType = foundApp?.appData?.location?.type;
|
||||
if (!appType) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Default conferencing app '${defaultApp.appSlug}' doesnt exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
appId: foundApp.slug,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.eventType.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: eventTypeIds,
|
||||
},
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
locations: [
|
||||
{ type: appType, link: defaultApp.appLink, credentialId: credential?.id },
|
||||
] as LocationObject[],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
import appStore from "@calcom/app-store";
|
||||
import type { TDependencyData } from "@calcom/app-store/_appRegistry";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TeamQuery } from "@calcom/trpc/server/routers/loggedInViewer/integrations.handler";
|
||||
import type { TIntegrationsInputSchema } from "@calcom/trpc/server/routers/loggedInViewer/integrations.schema";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { PaymentApp } from "@calcom/types/PaymentService";
|
||||
|
||||
export type ConnectedApps = Awaited<ReturnType<typeof getConnectedApps>>;
|
||||
|
||||
export async function getConnectedApps({
|
||||
user,
|
||||
prisma,
|
||||
input,
|
||||
}: {
|
||||
user: Pick<User, "id" | "name" | "avatarUrl"> & { avatar?: string };
|
||||
prisma: PrismaClient;
|
||||
input: TIntegrationsInputSchema;
|
||||
}) {
|
||||
const {
|
||||
variant,
|
||||
exclude,
|
||||
onlyInstalled,
|
||||
includeTeamInstalledApps,
|
||||
extendsFeature,
|
||||
teamId,
|
||||
sortByMostPopular,
|
||||
appId,
|
||||
} = input;
|
||||
let credentials = await getUsersCredentials(user);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
|
||||
if (includeTeamInstalledApps || teamId) {
|
||||
const teamsQuery = await prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
const parentTeams: TeamQuery[] = [];
|
||||
// Only loop and grab parent teams if a teamId was given. If not then all teams will be queried
|
||||
if (teamId) {
|
||||
teamsQuery.forEach((team) => {
|
||||
if (team?.parent) {
|
||||
const { parent, ...filteredTeam } = team;
|
||||
filteredTeams.push(filteredTeam);
|
||||
// Only add parent team if it's not already in teamsQuery
|
||||
if (!teamsQuery.some((t) => t.id === parent.id)) {
|
||||
parentTeams.push(parent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
userTeams = [...teamsQuery, ...parentTeams];
|
||||
|
||||
const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => {
|
||||
return teamApp.credentials ? teamApp.credentials.flat() : [];
|
||||
});
|
||||
if (!includeTeamInstalledApps || teamId) {
|
||||
credentials = teamAppCredentials;
|
||||
} else {
|
||||
credentials = credentials.concat(teamAppCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
filterOnCredentials: onlyInstalled,
|
||||
...(appId ? { where: { slug: appId } } : {}),
|
||||
});
|
||||
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||
let apps = await Promise.all(
|
||||
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
const userCredentialIds = credentials.filter((c) => c.appId === app.slug && !c.teamId).map((c) => c.id);
|
||||
const invalidCredentialIds = credentials
|
||||
.filter((c) => c.appId === app.slug && c.invalid)
|
||||
.map((c) => c.id);
|
||||
const teams = await Promise.all(
|
||||
credentials
|
||||
.filter((c) => c.appId === app.slug && c.teamId)
|
||||
.map(async (c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
logoUrl: team.logoUrl,
|
||||
credentialId: c.id,
|
||||
isAdmin:
|
||||
team.members[0].role === MembershipRole.ADMIN ||
|
||||
team.members[0].role === MembershipRole.OWNER,
|
||||
};
|
||||
})
|
||||
);
|
||||
// type infer as CredentialOwner
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: user.name,
|
||||
avatar: user?.avatar ?? user?.avatarUrl,
|
||||
};
|
||||
|
||||
// We need to know if app is payment type
|
||||
// undefined it means that app don't require app/setup/page
|
||||
let isSetupAlready = undefined;
|
||||
if (credential && app.categories.includes("payment")) {
|
||||
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null;
|
||||
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
|
||||
const PaymentService = paymentApp.lib.PaymentService;
|
||||
const paymentInstance = new PaymentService(credential);
|
||||
isSetupAlready = paymentInstance.isSetupAlready();
|
||||
}
|
||||
}
|
||||
|
||||
let dependencyData: TDependencyData = [];
|
||||
if (app.dependencies?.length) {
|
||||
dependencyData = app.dependencies.map((dependency) => {
|
||||
const dependencyInstalled = enabledApps.some(
|
||||
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
|
||||
);
|
||||
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
|
||||
const dependencyName = getAppFromSlug(dependency)?.name;
|
||||
return { name: dependencyName, installed: dependencyInstalled };
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...app,
|
||||
...(teams.length && {
|
||||
credentialOwner,
|
||||
}),
|
||||
userCredentialIds,
|
||||
invalidCredentialIds,
|
||||
teams,
|
||||
isInstalled: !!userCredentialIds.length || !!teams.length || app.isGlobal,
|
||||
isSetupAlready,
|
||||
...(app.dependencies && { dependencyData }),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (variant) {
|
||||
// `flatMap()` these work like `.filter()` but infers the types correctly
|
||||
apps = apps
|
||||
// variant check
|
||||
.flatMap((item) => (item.variant.startsWith(variant) ? [item] : []));
|
||||
}
|
||||
|
||||
if (exclude) {
|
||||
// exclusion filter
|
||||
apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true));
|
||||
}
|
||||
|
||||
if (onlyInstalled) {
|
||||
apps = apps.flatMap((item) =>
|
||||
item.userCredentialIds.length > 0 || item.teams.length || item.isGlobal ? [item] : []
|
||||
);
|
||||
}
|
||||
|
||||
if (extendsFeature) {
|
||||
apps = apps
|
||||
.filter((app) => app.extendsFeature?.includes(extendsFeature))
|
||||
.map((app) => ({
|
||||
...app,
|
||||
isInstalled: !!app.userCredentialIds?.length || !!app.teams?.length || app.isGlobal,
|
||||
}));
|
||||
}
|
||||
|
||||
if (sortByMostPopular) {
|
||||
const installCountPerApp = await getInstallCountPerApp();
|
||||
|
||||
// sort the apps array by the most popular apps
|
||||
apps.sort((a, b) => {
|
||||
const aCount = installCountPerApp[a.slug] || 0;
|
||||
const bCount = installCountPerApp[b.slug] || 0;
|
||||
return bCount - aCount;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: apps,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import { useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import type {
|
||||
BulkUpdatParams,
|
||||
EventTypes,
|
||||
} from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||
import WebSchedule, {
|
||||
@@ -109,6 +112,9 @@ type AvailabilitySettingsProps = {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
save: (params: BulkUpdatParams) => void;
|
||||
isSaving: boolean;
|
||||
eventTypes?: EventTypes;
|
||||
isEventTypesFetching?: boolean;
|
||||
handleBulkEditDialogToggle: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -365,6 +371,9 @@ export function AvailabilitySettings({
|
||||
setOpen={bulkUpdateModalProps.setIsOpen}
|
||||
bulkUpdateFunction={bulkUpdateModalProps?.save}
|
||||
description={t("default_schedules_bulk_description")}
|
||||
eventTypes={bulkUpdateModalProps?.eventTypes}
|
||||
isEventTypesFetching={bulkUpdateModalProps?.isEventTypesFetching}
|
||||
handleBulkEditDialogToggle={bulkUpdateModalProps.handleBulkEditDialogToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useReducer } from "react";
|
||||
|
||||
import { AppList } from "@calcom/features/apps/components/AppList";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { GOOGLE_MEET, ZOOM } from "@calcom/platform-constants";
|
||||
import { QueryCell } from "@calcom/trpc/components/QueryCell";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import {
|
||||
Button,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
Dropdown,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
EmptyScreen,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { useToast } from "../../src/components/ui/use-toast";
|
||||
import type {
|
||||
BulkUpdatParams,
|
||||
UpdateUsersDefaultConferencingAppParams,
|
||||
} from "./ConferencingAppsViewWebWrapper";
|
||||
import { useAtomBulkUpdateEventTypesToDefaultLocation } from "./hooks/useAtomBulkUpdateEventTypesToDefaultLocation";
|
||||
import { useAtomGetEventTypes } from "./hooks/useAtomGetEventTypes";
|
||||
import {
|
||||
useAtomsGetInstalledConferencingApps,
|
||||
QUERY_KEY as atomsConferencingAppsQueryKey,
|
||||
} from "./hooks/useAtomsGetInstalledConferencingApps";
|
||||
import { useConnect } from "./hooks/useConnect";
|
||||
import { useDeleteCredential } from "./hooks/useDeleteCredential";
|
||||
import {
|
||||
useGetDefaultConferencingApp,
|
||||
QUERY_KEY as defaultConferencingAppQueryKey,
|
||||
} from "./hooks/useGetDefaultConferencingApp";
|
||||
import { useUpdateUserDefaultConferencingApp } from "./hooks/useUpdateUserDefaultConferencingApp";
|
||||
|
||||
type ConferencingAppsViewPlatformWrapperProps = {
|
||||
disableToasts?: boolean;
|
||||
returnTo?: string;
|
||||
onErrorReturnTo?: string;
|
||||
};
|
||||
|
||||
type RemoveAppParams = { callback: () => void; app?: App["slug"] };
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
type ModalState = {
|
||||
isOpen: boolean;
|
||||
credentialId: null | number;
|
||||
app: App["slug"] | null;
|
||||
};
|
||||
|
||||
export const ConferencingAppsViewPlatformWrapper = ({
|
||||
disableToasts = false,
|
||||
returnTo,
|
||||
onErrorReturnTo,
|
||||
}: ConferencingAppsViewPlatformWrapperProps) => {
|
||||
const { t } = useLocale();
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const showToast = (message: string, variant: "success" | "warning" | "error") => {
|
||||
if (!disableToasts) {
|
||||
toast({ description: message });
|
||||
}
|
||||
};
|
||||
|
||||
const [modal, updateModal] = useReducer(
|
||||
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
|
||||
{
|
||||
isOpen: false,
|
||||
credentialId: null,
|
||||
app: null,
|
||||
}
|
||||
);
|
||||
|
||||
const handleModelClose = () => {
|
||||
updateModal({ isOpen: false, credentialId: null, app: null });
|
||||
};
|
||||
|
||||
const handleDisconnect = (credentialId: number, app: App["slug"]) => {
|
||||
updateModal({ isOpen: true, credentialId, app });
|
||||
};
|
||||
|
||||
const installedIntegrationsQuery = useAtomsGetInstalledConferencingApps();
|
||||
const { data: defaultConferencingApp } = useGetDefaultConferencingApp();
|
||||
const { data: eventTypesQuery, isFetching: isEventTypesFetching } = useAtomGetEventTypes();
|
||||
|
||||
const deleteCredentialMutation = useDeleteCredential({
|
||||
onSuccess: () => {
|
||||
showToast(t("app_removed_successfully"), "success");
|
||||
handleModelClose();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [atomsConferencingAppsQueryKey],
|
||||
});
|
||||
queryClient.resetQueries({
|
||||
queryKey: [defaultConferencingAppQueryKey],
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_removing_app"), "error");
|
||||
handleModelClose();
|
||||
},
|
||||
});
|
||||
|
||||
const updateDefaultAppMutation = useUpdateUserDefaultConferencingApp({});
|
||||
|
||||
const bulkUpdateEventTypesToDefaultLocation = useAtomBulkUpdateEventTypesToDefaultLocation({});
|
||||
|
||||
const handleRemoveApp = ({ app }: RemoveAppParams) => {
|
||||
!!app && deleteCredentialMutation.mutate(app);
|
||||
};
|
||||
|
||||
const handleUpdateUserDefaultConferencingApp = ({
|
||||
appSlug,
|
||||
onSuccessCallback,
|
||||
onErrorCallback,
|
||||
}: UpdateUsersDefaultConferencingAppParams) => {
|
||||
updateDefaultAppMutation.mutate(appSlug, {
|
||||
onSuccess: () => {
|
||||
showToast("Default app updated successfully", "success");
|
||||
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
onErrorCallback();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkUpdateDefaultLocation = ({ eventTypeIds, callback }: BulkUpdatParams) => {
|
||||
bulkUpdateEventTypesToDefaultLocation.mutate(eventTypeIds, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
|
||||
callback();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleConnectDisconnectIntegrationMenuToggle = () => {
|
||||
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
|
||||
};
|
||||
|
||||
const { connect } = useConnect({
|
||||
onSuccess: () => {
|
||||
showToast("app installed successfully", "success");
|
||||
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
|
||||
},
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
|
||||
showToast(`Error: unable to install app`, "error");
|
||||
},
|
||||
returnTo,
|
||||
onErrorReturnTo,
|
||||
});
|
||||
|
||||
const AddConferencingButtonPlatform = ({ installedApps }: { installedApps?: Array<{ slug: string }> }) => {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button color="secondary" StartIcon="plus">
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{installedApps && !installedApps.find((app) => app.slug == GOOGLE_MEET) && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem color="secondary" onClick={() => connect(GOOGLE_MEET)}>
|
||||
{t("google_meet")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{installedApps && !installedApps?.find((app) => app.slug == ZOOM) && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem color="secondary" onClick={() => connect(ZOOM)}>
|
||||
{t("zoom")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<SettingsHeader
|
||||
title={t("conferencing")}
|
||||
description={t("conferencing_description")}
|
||||
CTA={<AddConferencingButtonPlatform installedApps={installedIntegrationsQuery.data?.items} />}
|
||||
borderInShellHeader={true}>
|
||||
<>
|
||||
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
||||
<QueryCell
|
||||
query={installedIntegrationsQuery}
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
if (!data.items.length) {
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_category_apps", {
|
||||
category: t("conferencing").toLowerCase(),
|
||||
})}
|
||||
description={t("no_category_apps_description_conferencing")}
|
||||
buttonRaw={<AddConferencingButtonPlatform installedApps={data?.items} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AppList
|
||||
listClassName="rounded-lg rounded-t-none border-t-0 max-w-full"
|
||||
handleDisconnect={handleDisconnect}
|
||||
data={data}
|
||||
variant="conferencing"
|
||||
defaultConferencingApp={defaultConferencingApp}
|
||||
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
|
||||
handleBulkUpdateDefaultLocation={handleBulkUpdateDefaultLocation}
|
||||
isBulkUpdateDefaultLocationPending={bulkUpdateEventTypesToDefaultLocation?.isPending}
|
||||
eventTypes={eventTypesQuery?.eventTypes}
|
||||
isEventTypesFetching={isEventTypesFetching}
|
||||
handleConnectDisconnectIntegrationMenuToggle={
|
||||
handleConnectDisconnectIntegrationMenuToggle
|
||||
}
|
||||
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DisconnectIntegrationModal
|
||||
handleModelClose={handleModelClose}
|
||||
isOpen={modal.isOpen}
|
||||
credentialId={modal.credentialId}
|
||||
app={modal.app}
|
||||
handleRemoveApp={handleRemoveApp}
|
||||
/>
|
||||
</>
|
||||
</SettingsHeader>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
+33
-6
@@ -16,8 +16,14 @@ type ConferencingAppsViewWebWrapperProps = {
|
||||
add: string;
|
||||
};
|
||||
|
||||
type UpdateDefaultConferencingAppParams = { appSlug: string; callback: () => void };
|
||||
type BulkUpdatParams = { eventTypeIds: number[]; callback: () => void };
|
||||
export type UpdateUsersDefaultConferencingAppParams = {
|
||||
appSlug: string;
|
||||
appLink?: string;
|
||||
onSuccessCallback: () => void;
|
||||
onErrorCallback: () => void;
|
||||
};
|
||||
|
||||
export type BulkUpdatParams = { eventTypeIds: number[]; callback: () => void };
|
||||
type RemoveAppParams = { credentialId: number; teamId?: number; callback: () => void };
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
@@ -73,6 +79,9 @@ export const ConferencingAppsViewWebWrapper = ({
|
||||
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation();
|
||||
|
||||
const { data: eventTypesQueryData, isFetching: isEventTypesFetching } =
|
||||
trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
|
||||
const handleRemoveApp = ({ credentialId, teamId, callback }: RemoveAppParams) => {
|
||||
deleteCredentialMutation.mutate(
|
||||
{ id: credentialId, teamId },
|
||||
@@ -91,17 +100,31 @@ export const ConferencingAppsViewWebWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdateDefaultConferencingApp = ({ appSlug, callback }: UpdateDefaultConferencingAppParams) => {
|
||||
const handleConnectDisconnectIntegrationMenuToggle = () => {
|
||||
utils.viewer.integrations.invalidate();
|
||||
};
|
||||
|
||||
const handleBulkEditDialogToggle = () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
};
|
||||
|
||||
const handleUpdateUserDefaultConferencingApp = ({
|
||||
appSlug,
|
||||
appLink,
|
||||
onSuccessCallback,
|
||||
onErrorCallback,
|
||||
}: UpdateUsersDefaultConferencingAppParams) => {
|
||||
updateDefaultAppMutation.mutate(
|
||||
{ appSlug },
|
||||
{ appSlug, appLink },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast("Default app updated successfully", "success");
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
callback();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
onErrorCallback();
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -167,9 +190,13 @@ export const ConferencingAppsViewWebWrapper = ({
|
||||
data={data}
|
||||
variant="conferencing"
|
||||
defaultConferencingApp={defaultConferencingApp}
|
||||
handleUpdateDefaultConferencingApp={handleUpdateDefaultConferencingApp}
|
||||
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
|
||||
handleBulkUpdateDefaultLocation={handleBulkUpdateDefaultLocation}
|
||||
isBulkUpdateDefaultLocationPending={updateDefaultAppMutation.isPending}
|
||||
eventTypes={eventTypesQueryData?.eventTypes}
|
||||
isEventTypesFetching={isEventTypesFetching}
|
||||
handleConnectDisconnectIntegrationMenuToggle={handleConnectDisconnectIntegrationMenuToggle}
|
||||
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { EventType } from "@calcom/prisma/client";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "bulk-update-event-types-to-default-location";
|
||||
|
||||
export type UseAtomBulkUpdateEventTypesToDefaultLocationProps = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (err: Error) => void;
|
||||
onSettled?: () => void;
|
||||
};
|
||||
export const useAtomBulkUpdateEventTypesToDefaultLocation = ({
|
||||
onSuccess = () => {
|
||||
return;
|
||||
},
|
||||
onError = () => {
|
||||
return;
|
||||
},
|
||||
onSettled = () => {
|
||||
return;
|
||||
},
|
||||
}: UseAtomBulkUpdateEventTypesToDefaultLocationProps) => {
|
||||
return useMutation({
|
||||
onSuccess,
|
||||
onError,
|
||||
onSettled,
|
||||
mutationFn: (eventTypeIds: number[]) => {
|
||||
if (!eventTypeIds || eventTypeIds.length < 1) throw new Error("Event type ids are required");
|
||||
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/bulk-update-to-default-location`;
|
||||
|
||||
return http?.patch(pathname, { eventTypeIds }).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<EventType>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../../hooks/useAtomsContext";
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-get-event-types";
|
||||
|
||||
type ResponseEventType = { eventTypes: Array<{ id: number; title: string }> };
|
||||
|
||||
export const useAtomGetEventTypes = () => {
|
||||
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/`;
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http?.get<ApiResponse<ResponseEventType>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ResponseEventType>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: isInit && !!accessToken,
|
||||
});
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ConnectedApps } from "@calcom/platform-libraries";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../../hooks/useAtomsContext";
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "get-installed-conferencing-apps";
|
||||
|
||||
export const useAtomsGetInstalledConferencingApps = () => {
|
||||
const pathname = `/atoms/conferencing`;
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http?.get<ApiResponse<ConnectedApps>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ConnectedApps>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: isInit && !!accessToken,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS, ZOOM, GOOGLE_MEET } from "@calcom/platform-constants";
|
||||
import type { ApiErrorResponse, ApiResponse } from "@calcom/platform-types";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export type UseGetOauthAuthUrlProps = {
|
||||
returnTo?: string;
|
||||
onErrorReturnTo?: string;
|
||||
};
|
||||
|
||||
export const useGetOauthAuthUrl = ({ returnTo, onErrorReturnTo }: UseGetOauthAuthUrlProps) => {
|
||||
return useQuery({
|
||||
queryKey: ["get-zoom-auth-url"],
|
||||
staleTime: Infinity,
|
||||
enabled: false,
|
||||
queryFn: () => {
|
||||
return http
|
||||
?.get<ApiResponse<{ url: string }>>(
|
||||
`conferencing/${ZOOM}/oauth/auth-url${returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : ""}${
|
||||
onErrorReturnTo ? `&onErrorReturnTo=${encodeURIComponent(onErrorReturnTo)}` : ""
|
||||
}`
|
||||
)
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.status === SUCCESS_STATUS) {
|
||||
return responseBody.data.url;
|
||||
}
|
||||
if (responseBody.status === ERROR_STATUS) throw new Error(responseBody.error.message);
|
||||
return "";
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type UseConnectGoogleMeetProps = {
|
||||
onSuccess?: (res: ApiResponse) => void;
|
||||
onError?: (err: ApiErrorResponse) => void;
|
||||
returnTo?: string;
|
||||
onErrorReturnTo?: string;
|
||||
};
|
||||
|
||||
export const useConnectNonOauthApp = (
|
||||
{ onSuccess, onError }: UseConnectGoogleMeetProps = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (app: string) => {
|
||||
const pathname = `/conferencing/${app}/connect`;
|
||||
return http?.post(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return { status: res.data };
|
||||
} else {
|
||||
throw new Error(res.data.error.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
};
|
||||
|
||||
export const useConnect = ({ returnTo, onErrorReturnTo, ...props }: UseConnectGoogleMeetProps) => {
|
||||
const { refetch } = useGetOauthAuthUrl({ returnTo, onErrorReturnTo });
|
||||
const connectNonOauthApp = useConnectNonOauthApp(props);
|
||||
|
||||
const connect = async (app: App["slug"]) => {
|
||||
switch (app) {
|
||||
case ZOOM:
|
||||
const redirectUri = await refetch();
|
||||
if (redirectUri.data) {
|
||||
window.location.href = redirectUri.data;
|
||||
}
|
||||
break;
|
||||
|
||||
case GOOGLE_MEET:
|
||||
connectNonOauthApp.mutate(app);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return { connect };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-delete-credential";
|
||||
export type UseDeleteEventTypeProps = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (err: Error) => void;
|
||||
onSettled?: () => void;
|
||||
};
|
||||
export const useDeleteCredential = ({ onSuccess, onError, onSettled }: UseDeleteEventTypeProps) => {
|
||||
return useMutation({
|
||||
onSuccess,
|
||||
onError,
|
||||
onSettled,
|
||||
mutationFn: (app: App["slug"]) => {
|
||||
if (!app) throw new Error("app is required");
|
||||
const pathname = `/conferencing/${app}/disconnect`;
|
||||
return http?.delete(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../../hooks/useAtomsContext";
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-get-default-conferencing-app";
|
||||
|
||||
type ResponseDataType =
|
||||
| {
|
||||
appSlug?: string;
|
||||
appLink?: string;
|
||||
}
|
||||
| undefined;
|
||||
export const useGetDefaultConferencingApp = () => {
|
||||
const pathname = `/conferencing/default`;
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http?.get<ApiResponse<ResponseDataType>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ResponseDataType>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: isInit && !!accessToken,
|
||||
});
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import http from "../../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-update-user-default-conferencing-app";
|
||||
export type UseUpdateUserDefaultConferencingAppProps = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (err: Error) => void;
|
||||
onSettled?: () => void;
|
||||
};
|
||||
export const useUpdateUserDefaultConferencingApp = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
onSettled,
|
||||
}: UseUpdateUserDefaultConferencingAppProps) => {
|
||||
return useMutation({
|
||||
onSuccess,
|
||||
onError,
|
||||
onSettled,
|
||||
mutationFn: (app: App["slug"]) => {
|
||||
if (!app) throw new Error("app is required");
|
||||
const pathname = `/conferencing/${app}/default`;
|
||||
return http?.post(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -25,6 +25,7 @@ export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettin
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
|
||||
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
|
||||
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
|
||||
export { ConferencingAppsViewPlatformWrapper as ConferencingAppsSettings } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export { StripeConnect } from "./connect/stripe/StripeConnect";
|
||||
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
|
||||
export { PaymentForm } from "./event-types/payments/PaymentForm";
|
||||
|
||||
@@ -17,8 +17,12 @@ export const GOOGLE_MEET = "google-meet";
|
||||
export const GOOGLE_MEET_TYPE = "google_video";
|
||||
export const GOOGLE_MEET_ID = "google-meet";
|
||||
|
||||
export const CONFERENCING_APPS = [GOOGLE_MEET];
|
||||
export const ZOOM = "zoom";
|
||||
export const ZOOM_TYPE = "zoom_video";
|
||||
|
||||
export const CAL_VIDEO = "daily-video";
|
||||
|
||||
export const CONFERENCING_APPS = [GOOGLE_MEET, ZOOM];
|
||||
export const APPS_TYPE_ID_MAPPING = {
|
||||
[GOOGLE_CALENDAR_TYPE]: GOOGLE_CALENDAR_ID,
|
||||
[OFFICE_365_CALENDAR_TYPE]: OFFICE_365_CALENDAR_ID,
|
||||
|
||||
@@ -34,6 +34,10 @@ export function Navbar({ username }: { username?: string }) {
|
||||
<li>
|
||||
<Link href="/embed">Embed</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href="/conferencing-apps">Conferencing Apps</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { ConferencingAppsSettings } from "@calcom/atoms";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function ConferencingApps(props: { calUsername: string; calEmail: string }) {
|
||||
const pathname = usePathname();
|
||||
const callbackUri = `${window.location.origin}${pathname}`;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`flex min-h-screen flex-col ${inter.className} main text-default flex min-h-full w-full flex-col items-center overflow-visible`}>
|
||||
<Navbar username={props.calUsername} />
|
||||
<div className="my-8">
|
||||
<ConferencingAppsSettings returnTo={callbackUri} onErrorReturnTo={callbackUri} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booki
|
||||
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import handleDeleteCredential from "@calcom/features/credentials/handleDeleteCredential";
|
||||
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
|
||||
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { handleCreatePhoneCall } from "@calcom/features/handleCreatePhoneCall";
|
||||
@@ -28,6 +29,7 @@ import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleI
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings";
|
||||
import { symmetricEncrypt, symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import getBulkEventTypes from "@calcom/lib/event-types/getBulkEventTypes";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
@@ -81,6 +83,10 @@ export { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleN
|
||||
|
||||
export { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
export type { ConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
|
||||
export { getConnectedApps } from "@calcom/lib/getConnectedApps";
|
||||
export { bulkUpdateEventsToDefaultLocation } from "@calcom/lib/bulkUpdateEventsToDefaultLocation";
|
||||
export type { ConnectedApps } from "@calcom/lib/getConnectedApps";
|
||||
export { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
|
||||
|
||||
export {
|
||||
@@ -218,3 +224,5 @@ export { AttendeeUpdatedEmail };
|
||||
export { OrganizerRequestEmail };
|
||||
|
||||
export { AttendeeRequestEmail };
|
||||
export { handleDeleteCredential };
|
||||
export { getBulkEventTypes };
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import type { TDependencyData } from "@calcom/app-store/_appRegistry";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { getConnectedApps } from "@calcom/lib/getConnectedApps";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { PaymentApp } from "@calcom/types/PaymentService";
|
||||
|
||||
import type { TIntegrationsInputSchema } from "./integrations.schema";
|
||||
|
||||
@@ -39,214 +29,7 @@ export type TeamQuery = Prisma.TeamGetPayload<{
|
||||
};
|
||||
}>;
|
||||
|
||||
// type TeamQueryWithParent = TeamQuery & {
|
||||
// parent?: TeamQuery | null;
|
||||
// };
|
||||
|
||||
export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) => {
|
||||
const { user } = ctx;
|
||||
const {
|
||||
variant,
|
||||
exclude,
|
||||
onlyInstalled,
|
||||
includeTeamInstalledApps,
|
||||
extendsFeature,
|
||||
teamId,
|
||||
sortByMostPopular,
|
||||
appId,
|
||||
} = input;
|
||||
let credentials = await getUsersCredentials(user);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
|
||||
if (includeTeamInstalledApps || teamId) {
|
||||
const teamsQuery = await prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
const parentTeams: TeamQuery[] = [];
|
||||
// Only loop and grab parent teams if a teamId was given. If not then all teams will be queried
|
||||
if (teamId) {
|
||||
teamsQuery.forEach((team) => {
|
||||
if (team?.parent) {
|
||||
const { parent, ...filteredTeam } = team;
|
||||
filteredTeams.push(filteredTeam);
|
||||
// Only add parent team if it's not already in teamsQuery
|
||||
if (!teamsQuery.some((t) => t.id === parent.id)) {
|
||||
parentTeams.push(parent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
userTeams = [...teamsQuery, ...parentTeams];
|
||||
|
||||
const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => {
|
||||
return teamApp.credentials ? teamApp.credentials.flat() : [];
|
||||
});
|
||||
if (!includeTeamInstalledApps || teamId) {
|
||||
credentials = teamAppCredentials;
|
||||
} else {
|
||||
credentials = credentials.concat(teamAppCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
filterOnCredentials: onlyInstalled,
|
||||
...(appId ? { where: { slug: appId } } : {}),
|
||||
});
|
||||
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||
let apps = await Promise.all(
|
||||
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
const userCredentialIds = credentials.filter((c) => c.appId === app.slug && !c.teamId).map((c) => c.id);
|
||||
const invalidCredentialIds = credentials
|
||||
.filter((c) => c.appId === app.slug && c.invalid)
|
||||
.map((c) => c.id);
|
||||
const teams = await Promise.all(
|
||||
credentials
|
||||
.filter((c) => c.appId === app.slug && c.teamId)
|
||||
.map(async (c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
logoUrl: team.logoUrl,
|
||||
credentialId: c.id,
|
||||
isAdmin:
|
||||
team.members[0].role === MembershipRole.ADMIN ||
|
||||
team.members[0].role === MembershipRole.OWNER,
|
||||
};
|
||||
})
|
||||
);
|
||||
// type infer as CredentialOwner
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
};
|
||||
|
||||
// We need to know if app is payment type
|
||||
// undefined it means that app don't require app/setup/page
|
||||
let isSetupAlready = undefined;
|
||||
if (credential && app.categories.includes("payment")) {
|
||||
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null;
|
||||
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
|
||||
const PaymentService = paymentApp.lib.PaymentService;
|
||||
const paymentInstance = new PaymentService(credential);
|
||||
isSetupAlready = paymentInstance.isSetupAlready();
|
||||
}
|
||||
}
|
||||
|
||||
let dependencyData: TDependencyData = [];
|
||||
if (app.dependencies?.length) {
|
||||
dependencyData = app.dependencies.map((dependency) => {
|
||||
const dependencyInstalled = enabledApps.some(
|
||||
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
|
||||
);
|
||||
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
|
||||
const dependencyName = getAppFromSlug(dependency)?.name;
|
||||
return { name: dependencyName, installed: dependencyInstalled };
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...app,
|
||||
...(teams.length && {
|
||||
credentialOwner,
|
||||
}),
|
||||
userCredentialIds,
|
||||
invalidCredentialIds,
|
||||
teams,
|
||||
isInstalled: !!userCredentialIds.length || !!teams.length || app.isGlobal,
|
||||
isSetupAlready,
|
||||
...(app.dependencies && { dependencyData }),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (variant) {
|
||||
// `flatMap()` these work like `.filter()` but infers the types correctly
|
||||
apps = apps
|
||||
// variant check
|
||||
.flatMap((item) => (item.variant.startsWith(variant) ? [item] : []));
|
||||
}
|
||||
|
||||
if (exclude) {
|
||||
// exclusion filter
|
||||
apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true));
|
||||
}
|
||||
|
||||
if (onlyInstalled) {
|
||||
apps = apps.flatMap((item) =>
|
||||
item.userCredentialIds.length > 0 || item.teams.length || item.isGlobal ? [item] : []
|
||||
);
|
||||
}
|
||||
|
||||
if (extendsFeature) {
|
||||
apps = apps
|
||||
.filter((app) => app.extendsFeature?.includes(extendsFeature))
|
||||
.map((app) => ({
|
||||
...app,
|
||||
isInstalled: !!app.userCredentialIds?.length || !!app.teams?.length || app.isGlobal,
|
||||
}));
|
||||
}
|
||||
|
||||
if (sortByMostPopular) {
|
||||
const installCountPerApp = await getInstallCountPerApp();
|
||||
|
||||
// sort the apps array by the most popular apps
|
||||
apps.sort((a, b) => {
|
||||
const aCount = installCountPerApp[a.slug] || 0;
|
||||
const bCount = installCountPerApp[b.slug] || 0;
|
||||
return bCount - aCount;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: apps,
|
||||
};
|
||||
const user = ctx.user;
|
||||
return getConnectedApps({ user, input, prisma });
|
||||
};
|
||||
|
||||
+6
-49
@@ -1,9 +1,5 @@
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import { bulkUpdateEventsToDefaultLocation } from "@calcom/lib/bulkUpdateEventsToDefaultLocation";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TBulkUpdateToDefaultLocationInputSchema } from "./bulkUpdateToDefaultLocation.schema";
|
||||
@@ -15,50 +11,11 @@ type BulkUpdateToDefaultLocationOptions = {
|
||||
input: TBulkUpdateToDefaultLocationInputSchema;
|
||||
};
|
||||
|
||||
export const bulkUpdateToDefaultLocationHandler = async ({
|
||||
ctx,
|
||||
input,
|
||||
}: BulkUpdateToDefaultLocationOptions) => {
|
||||
export const bulkUpdateToDefaultLocationHandler = ({ ctx, input }: BulkUpdateToDefaultLocationOptions) => {
|
||||
const { eventTypeIds } = input;
|
||||
const defaultApp = userMetadataSchema.parse(ctx.user.metadata)?.defaultConferencingApp;
|
||||
|
||||
if (!defaultApp) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Default conferencing app not set",
|
||||
});
|
||||
}
|
||||
|
||||
const foundApp = getAppFromSlug(defaultApp.appSlug);
|
||||
const appType = foundApp?.appData?.location?.type;
|
||||
if (!appType) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Default conferencing app '${defaultApp.appSlug}' doesnt exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
appId: foundApp.slug,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.eventType.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: eventTypeIds,
|
||||
},
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
data: {
|
||||
locations: [
|
||||
{ type: appType, link: defaultApp.appLink, credentialId: credential?.id },
|
||||
] as LocationObject[],
|
||||
},
|
||||
return bulkUpdateEventsToDefaultLocation({
|
||||
eventTypeIds,
|
||||
user: ctx.user,
|
||||
prisma,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user