diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 17a36adcb1..57a99e301e 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@calcom/platform-constants": "*", + "@calcom/platform-libraries-0.0.13": "npm:@calcom/platform-libraries@0.0.13", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4", "@calcom/platform-types": "*", diff --git a/apps/api/v2/src/ee/event-types/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types.module.ts deleted file mode 100644 index 5163794ae9..0000000000 --- a/apps/api/v2/src/ee/event-types/event-types.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EventTypesController } from "@/ee/event-types/controllers/event-types.controller"; -import { EventTypesRepository } from "@/ee/event-types/event-types.repository"; -import { EventTypesService } from "@/ee/event-types/services/event-types.service"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule], - providers: [EventTypesRepository, EventTypesService], - controllers: [EventTypesController], - exports: [EventTypesService, EventTypesRepository], -}) -export class EventTypesModule {} diff --git a/apps/api/v2/src/ee/event-types/constants/constants.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/constants/constants.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/constants/constants.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/constants/constants.ts diff --git a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts similarity index 91% rename from apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts index 8536414e26..5f05750150 100644 --- a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts @@ -1,13 +1,13 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { EventTypesModule } from "@/ee/event-types/event-types.module"; -import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; -import { Editable } from "@/ee/event-types/inputs/enums/editable"; -import { BaseField } from "@/ee/event-types/inputs/enums/field-type"; -import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input"; -import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output"; -import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output"; -import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output"; +import { Editable } from "@/ee/event-types/event-types_2024_04_15//inputs/enums/editable"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type"; +import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; +import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output"; +import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output"; import { HttpExceptionFilter } from "@/filters/http-exception.filter"; import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; @@ -40,7 +40,7 @@ describe("Event types Endpoints", () => { beforeAll(async () => { const moduleRef = await Test.createTestingModule({ providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule, EventTypesModule, TokensModule], + imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule], }) .overrideGuard(PermissionsGuard) .useValue({ @@ -83,7 +83,7 @@ describe("Event types Endpoints", () => { userEmail, Test.createTestingModule({ providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule, EventTypesModule, TokensModule], + imports: [AppModule, UsersModule, EventTypesModule_2024_04_15, TokensModule], }) ) .overrideGuard(PermissionsGuard) @@ -132,7 +132,7 @@ describe("Event types Endpoints", () => { }); it("should create an event type", async () => { - const body: CreateEventTypeInput = { + const body: CreateEventTypeInput_2024_04_15 = { title: "Test Event Type", slug: "test-event-type", description: "A description of the test event type.", @@ -173,7 +173,7 @@ describe("Event types Endpoints", () => { it("should update event type", async () => { const newTitle = "Updated title"; - const body: UpdateEventTypeInput = { + const body: UpdateEventTypeInput_2024_04_15 = { title: newTitle, disableGuests: false, slotInterval: 30, @@ -207,7 +207,7 @@ describe("Event types Endpoints", () => { it("should return 400 if param event type id is null", async () => { const locations = [{ type: "inPerson", address: "123 Main St" }]; - const body: UpdateEventTypeInput = { + const body: UpdateEventTypeInput_2024_04_15 = { locations, }; @@ -217,7 +217,7 @@ describe("Event types Endpoints", () => { it("should update event type locations", async () => { const locations = [{ type: "inPerson", address: "123 Main St" }]; - const body: UpdateEventTypeInput = { + const body: UpdateEventTypeInput_2024_04_15 = { locations, }; @@ -266,7 +266,7 @@ describe("Event types Endpoints", () => { }, ]; - const body: UpdateEventTypeInput = { + const body: UpdateEventTypeInput_2024_04_15 = { bookingFields, }; diff --git a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts similarity index 71% rename from apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts index 32ae9dd9fd..81c03a9066 100644 --- a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts @@ -1,16 +1,19 @@ -import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; -import { EventTypeIdParams } from "@/ee/event-types/inputs/event-type-id.input"; -import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input"; -import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input"; -import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output"; -import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output"; -import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output"; -import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output"; -import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output"; -import { GetEventTypesData, GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output"; -import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output"; -import { EventTypesService } from "@/ee/event-types/services/event-types.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { EventTypeIdParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input"; +import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input"; +import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { CreateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output"; +import { DeleteEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output"; +import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; +import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output"; +import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output"; +import { + GetEventTypesData, + GetEventTypesOutput, +} from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output"; +import { UpdateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output"; +import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; +import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; @@ -42,13 +45,13 @@ import { PrismaClient } from "@calcom/prisma"; @Controller({ path: "/v2/event-types", - version: API_VERSIONS_VALUES, + version: VERSION_2024_04_15_VALUE, }) @UseGuards(PermissionsGuard) @DocsTags("Event types") -export class EventTypesController { +export class EventTypesController_2024_04_15 { constructor( - private readonly eventTypesService: EventTypesService, + private readonly eventTypesService: EventTypesService_2024_04_15, private readonly prismaReadService: PrismaReadService ) {} @@ -56,7 +59,7 @@ export class EventTypesController { @Permissions([EVENT_TYPE_WRITE]) @UseGuards(ApiAuthGuard) async createEventType( - @Body() body: CreateEventTypeInput, + @Body() body: CreateEventTypeInput_2024_04_15, @GetUser() user: UserWithProfile ): Promise { const eventType = await this.eventTypesService.createUserEventType(user, body); @@ -71,9 +74,7 @@ export class EventTypesController { @Permissions([EVENT_TYPE_READ]) @UseGuards(ApiAuthGuard) async getEventType( - @Param() params: EventTypeIdParams, @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @GetUser() user: UserWithProfile ): Promise { const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId)); @@ -109,7 +110,7 @@ export class EventTypesController { async getPublicEventType( @Param("username") username: string, @Param("eventSlug") eventSlug: string, - @Query() queryParams: GetPublicEventTypeQueryParams + @Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15 ): Promise { try { const event = await getPublicEvent( @@ -149,9 +150,9 @@ export class EventTypesController { @UseGuards(ApiAuthGuard) @HttpCode(HttpStatus.OK) async updateEventType( - @Param() params: EventTypeIdParams, + @Param() params: EventTypeIdParams_2024_04_15, @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Body() body: UpdateEventTypeInput, + @Body() body: UpdateEventTypeInput_2024_04_15, @GetUser() user: UserWithProfile ): Promise { const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user); @@ -166,7 +167,7 @@ export class EventTypesController { @Permissions([EVENT_TYPE_WRITE]) @UseGuards(ApiAuthGuard) async deleteEventType( - @Param() params: EventTypeIdParams, + @Param() params: EventTypeIdParams_2024_04_15, @Param("eventTypeId", ParseIntPipe) eventTypeId: number, @GetUser("id") userId: number ): Promise { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts new file mode 100644 index 0000000000..7b54e9c7f7 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts @@ -0,0 +1,17 @@ +import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/controllers/event-types.controller"; +import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; +import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule], + providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15], + controllers: [EventTypesController_2024_04_15], + exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15], +}) +export class EventTypesModule_2024_04_15 {} diff --git a/apps/api/v2/src/ee/event-types/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts similarity index 88% rename from apps/api/v2/src/ee/event-types/event-types.repository.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts index c879aa080e..5d9c3c5dd2 100644 --- a/apps/api/v2/src/ee/event-types/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts @@ -1,4 +1,4 @@ -import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UserWithProfile } from "@/modules/users/users.repository"; @@ -8,12 +8,12 @@ import { getEventTypeById } from "@calcom/platform-libraries-0.0.2"; import type { PrismaClient } from "@calcom/prisma"; @Injectable() -export class EventTypesRepository { +export class EventTypesRepository_2024_04_15 { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} async createUserEventType( userId: number, - body: Pick + body: Pick ) { return this.dbWrite.prisma.eventType.create({ data: { diff --git a/apps/api/v2/src/ee/event-types/inputs/create-event-type.input.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts similarity index 87% rename from apps/api/v2/src/ee/event-types/inputs/create-event-type.input.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts index ca9ceb04db..9bd7e346bc 100644 --- a/apps/api/v2/src/ee/event-types/inputs/create-event-type.input.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts @@ -1,4 +1,4 @@ -import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input"; +import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { @@ -20,7 +20,7 @@ export const CREATE_EVENT_DESCRIPTION_EXAMPLE = // note(Lauris): We will gradually expose more properties if any customer needs them. // Just uncomment any below when requested. -export class CreateEventTypeInput { +export class CreateEventTypeInput_2024_04_15 { @IsNumber() @Min(1) @DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE }) @@ -46,9 +46,9 @@ export class CreateEventTypeInput { @IsOptional() @ValidateNested({ each: true }) - @Type(() => EventTypeLocation) + @Type(() => EventTypeLocation_2024_04_15) @IsArray() - locations?: EventTypeLocation[]; + locations?: EventTypeLocation_2024_04_15[]; @IsBoolean() @IsOptional() diff --git a/apps/api/v2/src/ee/event-types/inputs/enums/editable.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/editable.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/inputs/enums/editable.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/editable.ts diff --git a/apps/api/v2/src/ee/event-types/inputs/enums/field-type.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/field-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/inputs/enums/field-type.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/field-type.ts diff --git a/apps/api/v2/src/ee/event-types/inputs/enums/frequency.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/frequency.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/inputs/enums/frequency.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/frequency.ts diff --git a/apps/api/v2/src/ee/event-types/inputs/enums/period-type.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/period-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/inputs/enums/period-type.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/period-type.ts diff --git a/apps/api/v2/src/ee/event-types/inputs/enums/scheduling-type.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/inputs/enums/scheduling-type.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts diff --git a/apps/api/v2/src/ee/event-types/inputs/event-type-id.input.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts similarity index 68% rename from apps/api/v2/src/ee/event-types/inputs/event-type-id.input.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts index 392f330b00..37d108c746 100644 --- a/apps/api/v2/src/ee/event-types/inputs/event-type-id.input.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts @@ -1,6 +1,6 @@ import { IsNumberString } from "class-validator"; -export class EventTypeIdParams { +export class EventTypeIdParams_2024_04_15 { @IsNumberString() eventTypeId!: number; } diff --git a/apps/api/v2/src/ee/event-types/inputs/event-type-location.input.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts similarity index 95% rename from apps/api/v2/src/ee/event-types/inputs/event-type-location.input.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts index a2584ec20e..96ca332e84 100644 --- a/apps/api/v2/src/ee/event-types/inputs/event-type-location.input.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts @@ -4,7 +4,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato // note(Lauris): We will gradually expose more properties if any customer needs them. // Just uncomment any below when requested. -export class EventTypeLocation { +export class EventTypeLocation_2024_04_15 { @IsString() @DocsProperty({ example: "link" }) type!: string; diff --git a/apps/api/v2/src/ee/event-types/inputs/get-public-event-type-query-params.input.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts similarity index 88% rename from apps/api/v2/src/ee/event-types/inputs/get-public-event-type-query-params.input.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts index 1e149f3a73..02fc93647f 100644 --- a/apps/api/v2/src/ee/event-types/inputs/get-public-event-type-query-params.input.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts @@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Transform } from "class-transformer"; import { IsBoolean, IsOptional, IsString } from "class-validator"; -export class GetPublicEventTypeQueryParams { +export class GetPublicEventTypeQueryParams_2024_04_15 { @Transform(({ value }: { value: string }) => value === "true") @IsBoolean() @IsOptional() diff --git a/apps/api/v2/src/ee/event-types/inputs/update-event-type.input.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts similarity index 89% rename from apps/api/v2/src/ee/event-types/inputs/update-event-type.input.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts index 8c112db433..cf9c3c901c 100644 --- a/apps/api/v2/src/ee/event-types/inputs/update-event-type.input.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts @@ -1,7 +1,7 @@ -import { Editable } from "@/ee/event-types/inputs/enums/editable"; -import { BaseField } from "@/ee/event-types/inputs/enums/field-type"; -import { Frequency } from "@/ee/event-types/inputs/enums/frequency"; -import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input"; +import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable"; +import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type"; +import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency"; +import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; import { Type } from "class-transformer"; import { IsString, @@ -105,7 +105,7 @@ class VariantsConfig { variants!: Record; } -export class BookingField { +export class BookingField_2024_04_15 { @IsEnum(BaseField) type!: BaseField; @@ -179,7 +179,7 @@ export class BookingField { sources?: Source[]; } -export class RecurringEvent { +export class RecurringEvent_2024_04_15 { @IsDate() @IsOptional() dtstart?: Date; @@ -202,7 +202,7 @@ export class RecurringEvent { tzid?: string; } -export class IntervalLimits { +export class IntervalLimits_2024_04_15 { @IsNumber() @IsOptional() PER_DAY?: number; @@ -220,7 +220,7 @@ export class IntervalLimits { PER_YEAR?: number; } -export class UpdateEventTypeInput { +export class UpdateEventTypeInput_2024_04_15 { @IsInt() @Min(1) @IsOptional() @@ -243,9 +243,9 @@ export class UpdateEventTypeInput { hidden?: boolean; @ValidateNested({ each: true }) - @Type(() => EventTypeLocation) + @Type(() => EventTypeLocation_2024_04_15) @IsOptional() - locations?: EventTypeLocation[]; + locations?: EventTypeLocation_2024_04_15[]; // @IsInt() // @IsOptional() @@ -278,8 +278,8 @@ export class UpdateEventTypeInput { @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => BookingField) - bookingFields?: BookingField[]; + @Type(() => BookingField_2024_04_15) + bookingFields?: BookingField_2024_04_15[]; // @IsString() // @IsOptional() diff --git a/apps/api/v2/src/ee/event-types/outputs/create-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts similarity index 85% rename from apps/api/v2/src/ee/event-types/outputs/create-event-type.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts index 1a77102d4c..c7675708fe 100644 --- a/apps/api/v2/src/ee/event-types/outputs/create-event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output"; +import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/outputs/delete-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts similarity index 92% rename from apps/api/v2/src/ee/event-types/outputs/delete-event-type.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts index 738ab32499..76ddc81563 100644 --- a/apps/api/v2/src/ee/event-types/outputs/delete-event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts @@ -2,7 +2,7 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/inputs/create-event-type.input"; +} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; diff --git a/apps/api/v2/src/ee/event-types/outputs/event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts similarity index 78% rename from apps/api/v2/src/ee/event-types/outputs/event-type.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts index cd87c5d98e..b95fe6ac80 100644 --- a/apps/api/v2/src/ee/event-types/outputs/event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts @@ -3,15 +3,15 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/inputs/create-event-type.input"; -import { PeriodType } from "@/ee/event-types/inputs/enums/period-type"; -import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type"; -import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input"; +} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { PeriodType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/period-type"; +import { SchedulingType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type"; +import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; import { - BookingField, - IntervalLimits, - RecurringEvent, -} from "@/ee/event-types/inputs/update-event-type.input"; + BookingField_2024_04_15, + IntervalLimits_2024_04_15, + RecurringEvent_2024_04_15, +} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { @@ -53,9 +53,9 @@ export class EventTypeOutput { hidden!: boolean; @ValidateNested({ each: true }) - @Type(() => EventTypeLocation) + @Type(() => EventTypeLocation_2024_04_15) @IsArray() - locations!: EventTypeLocation[] | null; + locations!: EventTypeLocation_2024_04_15[] | null; @IsInt() @ApiHideProperty() @@ -91,9 +91,9 @@ export class EventTypeOutput { @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => BookingField) + @Type(() => BookingField_2024_04_15) @ApiHideProperty() - bookingFields!: BookingField[] | null; + bookingFields!: BookingField_2024_04_15[] | null; @IsString() @ApiHideProperty() @@ -132,10 +132,10 @@ export class EventTypeOutput { requiresBookerEmailVerification!: boolean; @ValidateNested() - @Type(() => RecurringEvent) + @Type(() => RecurringEvent_2024_04_15) @IsOptional() @ApiHideProperty() - recurringEvent!: RecurringEvent | null; + recurringEvent!: RecurringEvent_2024_04_15 | null; @IsBoolean() @ApiHideProperty() @@ -203,15 +203,15 @@ export class EventTypeOutput { successRedirectUrl!: string | null; @ValidateNested() - @Type(() => IntervalLimits) + @Type(() => IntervalLimits_2024_04_15) @IsOptional() @ApiHideProperty() - bookingLimits!: IntervalLimits; + bookingLimits!: IntervalLimits_2024_04_15; @ValidateNested() - @Type(() => IntervalLimits) + @Type(() => IntervalLimits_2024_04_15) @ApiHideProperty() - durationLimits!: IntervalLimits; + durationLimits!: IntervalLimits_2024_04_15; @IsBoolean() @ApiHideProperty() diff --git a/apps/api/v2/src/ee/event-types/outputs/get-event-type-public.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/outputs/get-event-type-public.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts diff --git a/apps/api/v2/src/ee/event-types/outputs/get-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts similarity index 87% rename from apps/api/v2/src/ee/event-types/outputs/get-event-type.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts index 605be799e9..4d0cc43eb4 100644 --- a/apps/api/v2/src/ee/event-types/outputs/get-event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output"; +import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/outputs/get-event-types-public.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts similarity index 93% rename from apps/api/v2/src/ee/event-types/outputs/get-event-types-public.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts index c3789fce15..1fecef2ffa 100644 --- a/apps/api/v2/src/ee/event-types/outputs/get-event-types-public.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts @@ -2,7 +2,7 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/inputs/create-event-type.input"; +} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; diff --git a/apps/api/v2/src/ee/event-types/outputs/get-event-types.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts similarity index 89% rename from apps/api/v2/src/ee/event-types/outputs/get-event-types.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts index 5f164a659f..de791d0a7c 100644 --- a/apps/api/v2/src/ee/event-types/outputs/get-event-types.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output"; +import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsEnum, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/outputs/update-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts similarity index 85% rename from apps/api/v2/src/ee/event-types/outputs/update-event-type.output.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts index 642be558a6..52b68f4399 100644 --- a/apps/api/v2/src/ee/event-types/outputs/update-event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output"; +import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts similarity index 87% rename from apps/api/v2/src/ee/event-types/services/event-types.service.ts rename to apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts index 83c5e264fc..926abc0cdb 100644 --- a/apps/api/v2/src/ee/event-types/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -1,8 +1,8 @@ -import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants"; -import { EventTypesRepository } from "@/ee/event-types/event-types.repository"; -import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; -import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input"; -import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output"; +import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants"; +import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; @@ -14,16 +14,19 @@ import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-librarie import { EventType } from "@calcom/prisma/client"; @Injectable() -export class EventTypesService { +export class EventTypesService_2024_04_15 { constructor( - private readonly eventTypesRepository: EventTypesRepository, + private readonly eventTypesRepository: EventTypesRepository_2024_04_15, private readonly membershipsRepository: MembershipsRepository, private readonly usersRepository: UsersRepository, private readonly selectedCalendarsRepository: SelectedCalendarsRepository, private readonly dbWrite: PrismaWriteService ) {} - async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput): Promise { + async createUserEventType( + user: UserWithProfile, + body: CreateEventTypeInput_2024_04_15 + ): Promise { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); const { eventType } = await createEventType({ @@ -38,7 +41,7 @@ export class EventTypesService { return eventType as EventTypeOutput; } - async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) { + async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_04_15) { const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); if (existsWithSlug) { throw new BadRequestException("User already has an event type with this slug."); @@ -114,7 +117,7 @@ export class EventTypesService { return defaultEventTypes; } - async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) { + async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) { this.checkCanUpdateEventType(user.id, eventTypeId); const eventTypeUser = await this.getUserToUpdateEvent(user); await updateEventType({ diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts new file mode 100644 index 0000000000..690c1b4dec --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts @@ -0,0 +1,16 @@ +export const DEFAULT_EVENT_TYPES = { + thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" }, + thirtyMinutesVideo: { + length: 30, + slug: "thirty-minutes-video", + title: "30 Minutes", + locations: [{ type: "integrations:daily" }], + }, + sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" }, + sixtyMinutesVideo: { + length: 60, + slug: "sixty-minutes-video", + title: "60 Minutes", + locations: [{ type: "integrations:daily" }], + }, +}; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts new file mode 100644 index 0000000000..c53d259472 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -0,0 +1,295 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { HttpExceptionFilter } from "@/filters/http-exception.filter"; +import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { PlatformOAuthClient, Team, User } from "@prisma/client"; +import * as request from "supertest"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants"; +import { + ApiSuccessResponse, + CreateEventTypeInput_2024_06_14, + EventTypeOutput_2024_06_14, + UpdateEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; + +describe("Event types Endpoints", () => { + describe("Not authenticated", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter], + imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule], + }) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + await app.init(); + }); + + it(`/GET/:id`, () => { + return request(app.getHttpServer()).get("/api/v2/event-types/100").expect(401); + }); + + afterAll(async () => { + await app.close(); + }); + }); + + describe("User Authenticated", () => { + let app: INestApplication; + + let oAuthClient: PlatformOAuthClient; + let organization: Team; + let userRepositoryFixture: UserRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + + const userEmail = "event-types-test-e2e@api.com"; + const name = "bob-the-builder"; + const username = name; + let eventType: EventTypeOutput_2024_06_14; + let user: User; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter], + imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + + organization = await teamRepositoryFixture.create({ name: "organization" }); + oAuthClient = await createOAuthClient(organization.id); + user = await userRepositoryFixture.create({ + email: userEmail, + name, + username, + }); + + await app.init(); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["redirect-uri"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + it("should be defined", () => { + expect(oauthClientRepositoryFixture).toBeDefined(); + expect(userRepositoryFixture).toBeDefined(); + expect(oAuthClient).toBeDefined(); + expect(user).toBeDefined(); + }); + + it("should create an event type", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class", + slug: "coding-class", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + bookingFields: [ + { + type: "select", + label: "select which language you want to learn", + slug: "select-language", + required: true, + placeholder: "select language", + options: ["javascript", "python", "cobol"], + }, + ], + }; + + return request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(201) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + const createdEventType = responseBody.data; + expect(createdEventType).toHaveProperty("id"); + expect(createdEventType.title).toEqual(body.title); + expect(createdEventType.description).toEqual(body.description); + expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); + expect(createdEventType.locations).toEqual(body.locations); + expect(createdEventType.bookingFields).toEqual(body.bookingFields); + expect(createdEventType.ownerId).toEqual(user.id); + + eventType = responseBody.data; + }); + }); + + it("should update event type", async () => { + const newTitle = "Coding class in Italian!"; + + const body: UpdateEventTypeInput_2024_06_14 = { + title: newTitle, + }; + + return request(app.getHttpServer()) + .patch(`/api/v2/event-types/${eventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(200) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + const updatedEventType = responseBody.data; + expect(updatedEventType.title).toEqual(body.title); + + expect(updatedEventType.id).toEqual(eventType.id); + expect(updatedEventType.title).toEqual(newTitle); + expect(updatedEventType.description).toEqual(eventType.description); + expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes); + expect(updatedEventType.locations).toEqual(eventType.locations); + expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields); + expect(updatedEventType.ownerId).toEqual(user.id); + + eventType.title = newTitle; + }); + }); + + it(`/GET/:id`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types/${eventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + const fetchedEventType = responseBody.data; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(fetchedEventType.id).toEqual(eventType.id); + expect(fetchedEventType.title).toEqual(eventType.title); + expect(fetchedEventType.description).toEqual(eventType.description); + expect(fetchedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes); + expect(fetchedEventType.locations).toEqual(eventType.locations); + expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields); + expect(fetchedEventType.ownerId).toEqual(user.id); + }); + + it(`/GET/even-types by username`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types?username=${username}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data?.length).toEqual(1); + + const fetchedEventType = responseBody.data?.[0]; + + expect(fetchedEventType?.id).toEqual(eventType.id); + expect(fetchedEventType?.title).toEqual(eventType.title); + expect(fetchedEventType?.description).toEqual(eventType.description); + expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes); + expect(fetchedEventType?.locations).toEqual(eventType.locations); + expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields); + expect(fetchedEventType?.ownerId).toEqual(user.id); + }); + + it(`/GET/event-types by username and eventSlug`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + const fetchedEventType = responseBody.data[0]; + + expect(fetchedEventType?.id).toEqual(eventType.id); + expect(fetchedEventType?.title).toEqual(eventType.title); + expect(fetchedEventType?.description).toEqual(eventType.description); + expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes); + expect(fetchedEventType?.locations).toEqual(eventType.locations); + expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields); + expect(fetchedEventType?.ownerId).toEqual(user.id); + }); + + it(`/GET/:id not existing`, async () => { + await request(app.getHttpServer()) + .get(`/api/v2/event-types/1000`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(404); + }); + + it("should delete schedule", async () => { + return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200); + }); + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + try { + await eventTypesRepositoryFixture.delete(eventType.id); + } catch (e) { + // Event type might have been deleted by the test + } + try { + await userRepositoryFixture.delete(user.id); + } catch (e) { + // User might have been deleted by the test + } + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts new file mode 100644 index 0000000000..0b8f581c33 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -0,0 +1,128 @@ +import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output"; +import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output"; +import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output"; +import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output"; +import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output"; +import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; +import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { UserWithProfile } from "@/modules/users/users.repository"; +import { + Controller, + UseGuards, + Get, + Param, + Post, + Body, + NotFoundException, + Patch, + HttpCode, + HttpStatus, + Delete, + Query, +} from "@nestjs/common"; +import { ApiTags as DocsTags } from "@nestjs/swagger"; + +import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + CreateEventTypeInput_2024_06_14, + UpdateEventTypeInput_2024_06_14, + GetEventTypesQuery_2024_06_14, +} from "@calcom/platform-types"; + +@Controller({ + path: "/v2/event-types", + version: VERSION_2024_06_14_VALUE, +}) +@UseGuards(PermissionsGuard) +@DocsTags("Event types") +export class EventTypesController_2024_06_14 { + constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {} + + @Post("/") + @Permissions([EVENT_TYPE_WRITE]) + @UseGuards(ApiAuthGuard) + async createEventType( + @Body() body: CreateEventTypeInput_2024_06_14, + @GetUser() user: UserWithProfile + ): Promise { + const eventType = await this.eventTypesService.createUserEventType(user, body); + + return { + status: SUCCESS_STATUS, + data: eventType, + }; + } + + @Get("/:eventTypeId") + @Permissions([EVENT_TYPE_READ]) + @UseGuards(ApiAuthGuard) + async getEventTypeById( + @Param("eventTypeId") eventTypeId: string, + @GetUser() user: UserWithProfile + ): Promise { + const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId)); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + + return { + status: SUCCESS_STATUS, + data: eventType, + }; + } + + @Get("/") + @Permissions([EVENT_TYPE_READ]) + async getEventTypes( + @Query() queryParams: GetEventTypesQuery_2024_06_14 + ): Promise { + const eventTypes = await this.eventTypesService.getEventTypes(queryParams); + + return { + status: SUCCESS_STATUS, + data: eventTypes, + }; + } + + @Patch("/:eventTypeId") + @Permissions([EVENT_TYPE_WRITE]) + @UseGuards(ApiAuthGuard) + @HttpCode(HttpStatus.OK) + async updateEventType( + @Param("eventTypeId") eventTypeId: number, + @Body() body: UpdateEventTypeInput_2024_06_14, + @GetUser() user: UserWithProfile + ): Promise { + const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user); + + return { + status: SUCCESS_STATUS, + data: eventType, + }; + } + + @Delete("/:eventTypeId") + @Permissions([EVENT_TYPE_WRITE]) + @UseGuards(ApiAuthGuard) + async deleteEventType( + @Param("eventTypeId") eventTypeId: number, + @GetUser("id") userId: number + ): Promise { + const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId); + + return { + status: SUCCESS_STATUS, + data: { + id: eventType.id, + lengthInMinutes: eventType.length, + slug: eventType.slug, + title: eventType.title, + }, + }; + } +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts new file mode 100644 index 0000000000..cc28b9d0d7 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts @@ -0,0 +1,27 @@ +import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; +import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; +import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UsersRepository } from "@/modules/users/users.repository"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule], + providers: [ + EventTypesRepository_2024_06_14, + EventTypesService_2024_06_14, + InputEventTypesService_2024_06_14, + OutputEventTypesService_2024_06_14, + UsersRepository, + UsersService, + ], + controllers: [EventTypesController_2024_06_14], + exports: [EventTypesService_2024_06_14, EventTypesRepository_2024_06_14], +}) +export class EventTypesModule_2024_06_14 {} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts new file mode 100644 index 0000000000..6532bceae7 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -0,0 +1,103 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { UserWithProfile } from "@/modules/users/users.repository"; +import { Injectable } from "@nestjs/common"; + +import { + getEventTypeById, + transformApiEventTypeBookingFields, + transformApiEventTypeLocations, +} from "@calcom/platform-libraries-0.0.13"; +import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; +import type { PrismaClient } from "@calcom/prisma"; + +type InputEventTransformed = Omit< + CreateEventTypeInput_2024_06_14, + "lengthInMinutes" | "locations" | "bookingFields" +> & { + length: number; + slug: string; + locations?: ReturnType; + bookingFields?: ReturnType; +}; + +@Injectable() +export class EventTypesRepository_2024_06_14 { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async createUserEventType(userId: number, body: InputEventTransformed) { + return this.dbWrite.prisma.eventType.create({ + data: { + ...body, + userId, + locations: body.locations, + bookingFields: body.bookingFields, + users: { connect: { id: userId } }, + }, + }); + } + + async getEventTypeWithSeats(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + select: { users: { select: { id: true } }, seatsPerTimeSlot: true }, + }); + } + + async getUserEventType(userId: number, eventTypeId: number) { + return this.dbRead.prisma.eventType.findFirst({ + where: { + id: eventTypeId, + userId, + }, + include: { users: true, schedule: true }, + }); + } + + async getUserEventTypes(userId: number) { + return this.dbRead.prisma.eventType.findMany({ + where: { + userId, + }, + include: { users: true, schedule: true }, + }); + } + + async getUserEventTypeForAtom( + user: UserWithProfile, + isUserOrganizationAdmin: boolean, + eventTypeId: number + ) { + return await getEventTypeById({ + currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId, + eventTypeId, + userId: user.id, + prisma: this.dbRead.prisma as unknown as PrismaClient, + isUserOrganizationAdmin, + isTrpcCall: true, + }); + } + + async getEventTypeById(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + include: { users: true, schedule: true }, + }); + } + + async getUserEventTypeBySlug(userId: number, slug: string) { + return this.dbRead.prisma.eventType.findUnique({ + where: { + userId_slug: { + userId: userId, + slug: slug, + }, + }, + include: { users: true, schedule: true }, + }); + } + + async deleteEventType(eventTypeId: number) { + return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } }); + } +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts new file mode 100644 index 0000000000..89ff167e82 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +export class CreateEventTypeOutput_2024_06_14 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsIn([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: EventTypeOutput_2024_06_14, + }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => EventTypeOutput_2024_06_14) + data!: EventTypeOutput_2024_06_14; +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts new file mode 100644 index 0000000000..df64142ce4 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, IsInt, IsString } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE } from "@calcom/platform-types"; + +class DeleteData_2024_06_14 { + @IsInt() + @DocsProperty({ example: 1 }) + id!: number; + + @IsInt() + @DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE }) + lengthInMinutes!: number; + + @IsString() + slug!: string; + + @IsString() + @DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE }) + title!: string; +} + +export class DeleteEventTypeOutput_2024_06_14 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsIn([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @Type(() => DeleteData_2024_06_14) + data!: DeleteData_2024_06_14; +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts new file mode 100644 index 0000000000..b42034fd9c --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +export class GetEventTypeOutput_2024_06_14 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsIn([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: EventTypeOutput_2024_06_14, + }) + @ValidateNested() + @Type(() => EventTypeOutput_2024_06_14) + data!: EventTypeOutput_2024_06_14 | null; +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts new file mode 100644 index 0000000000..918b845d0e --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsArray, IsIn, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +export class GetEventTypesOutput_2024_06_14 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsIn([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested({ each: true }) + @Type(() => EventTypeOutput_2024_06_14) + @IsArray() + data!: EventTypeOutput_2024_06_14[]; +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts new file mode 100644 index 0000000000..7170de9f81 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, IsNotEmptyObject, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +export class UpdateEventTypeOutput_2024_06_14 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsIn([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: EventTypeOutput_2024_06_14, + }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => EventTypeOutput_2024_06_14) + data!: EventTypeOutput_2024_06_14; +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts new file mode 100644 index 0000000000..bb1975a9fc --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -0,0 +1,257 @@ +import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; +import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; + +import { createEventType, updateEventType } from "@calcom/platform-libraries"; +import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries"; +import { dynamicEvent } from "@calcom/platform-libraries-0.0.13"; +import { + CreateEventTypeInput_2024_06_14, + UpdateEventTypeInput_2024_06_14, + GetEventTypesQuery_2024_06_14, + EventTypeOutput_2024_06_14, +} from "@calcom/platform-types"; +import { EventType } from "@calcom/prisma/client"; + +@Injectable() +export class EventTypesService_2024_06_14 { + constructor( + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly inputEventTypesService: InputEventTypesService_2024_06_14, + private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, + private readonly membershipsRepository: MembershipsRepository, + private readonly usersRepository: UsersRepository, + private readonly usersService: UsersService, + private readonly selectedCalendarsRepository: SelectedCalendarsRepository, + private readonly dbWrite: PrismaWriteService + ) {} + + async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) { + await this.checkCanCreateEventType(user.id, body); + const eventTypeUser = await this.getUserToCreateEvent(user); + const bodyTransformed = this.inputEventTypesService.transformInputCreateEventType(body); + const { eventType: eventTypeCreated } = await createEventType({ + input: bodyTransformed, + ctx: { + user: eventTypeUser, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prisma: this.dbWrite.prisma, + }, + }); + + const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`); + } + + return this.outputEventTypesService.getResponseEventType(user.id, eventType); + } + + async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) { + const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); + if (existsWithSlug) { + throw new BadRequestException("User already has an event type with this slug."); + } + } + + async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) { + const user = await this.usersRepository.findByUsername(username); + if (!user) { + return null; + } + + const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug); + + if (!eventType) { + return null; + } + + return this.outputEventTypesService.getResponseEventType(user.id, eventType); + } + + async getEventTypesByUsername(username: string) { + const user = await this.usersRepository.findByUsername(username); + if (!user) { + return []; + } + return this.getUserEventTypes(user.id); + } + + async getUserToCreateEvent(user: UserWithProfile) { + const organizationId = user.movedToProfile?.organizationId || user.organizationId; + const isOrgAdmin = organizationId + ? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId) + : false; + const profileId = user.movedToProfile?.id || null; + return { + id: user.id, + organizationId: user.organizationId, + organization: { isOrgAdmin }, + profile: { id: profileId }, + metadata: user.metadata, + }; + } + + async getUserEventType(userId: number, eventTypeId: number) { + const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId); + + if (!eventType) { + return null; + } + + this.checkUserOwnsEventType(userId, eventType); + return this.outputEventTypesService.getResponseEventType(userId, eventType); + } + + async getUserEventTypes(userId: number) { + const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId); + + const eventTypePromises = eventTypes.map(async (eventType) => { + return await this.outputEventTypesService.getResponseEventType(userId, eventType); + }); + + return await Promise.all(eventTypePromises); + } + + async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) { + const organizationId = user.movedToProfile?.organizationId || user.organizationId; + + const isUserOrganizationAdmin = organizationId + ? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId) + : false; + + const eventType = await this.eventTypesRepository.getUserEventTypeForAtom( + user, + isUserOrganizationAdmin, + eventTypeId + ); + + if (!eventType) { + return null; + } + + this.checkUserOwnsEventType(user.id, eventType.eventType); + return eventType; + } + + async getEventTypesPublicByUsername(username: string): Promise { + const user = await this.usersRepository.findByUsername(username); + if (!user) { + throw new NotFoundException(`User with username "${username}" not found`); + } + + return await getEventTypesPublic(user.id); + } + + async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise { + const { username, eventSlug, usernames } = queryParams; + + if (username && eventSlug) { + const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug); + return eventType ? [eventType] : []; + } + + if (username) { + return await this.getEventTypesByUsername(username); + } + + if (usernames) { + const dynamicEventType = await this.getDynamicEventType(usernames); + return [dynamicEventType]; + } + + return []; + } + + async getDynamicEventType(usernames: string[]) { + const users = await this.usersService.getByUsernames(usernames); + const usersFiltered: UserWithProfile[] = []; + for (const user of users) { + if (user) { + usersFiltered.push(user); + } + } + + return this.outputEventTypesService.getResponseEventType(0, { + ...dynamicEvent, + users: usersFiltered, + isInstantEvent: false, + }); + } + + async createUserDefaultEventTypes(userId: number) { + const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES; + + const defaultEventTypes = await Promise.all([ + this.eventTypesRepository.createUserEventType(userId, thirtyMinutes), + this.eventTypesRepository.createUserEventType(userId, sixtyMinutes), + this.eventTypesRepository.createUserEventType(userId, thirtyMinutesVideo), + this.eventTypesRepository.createUserEventType(userId, sixtyMinutesVideo), + ]); + + return defaultEventTypes; + } + + async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) { + this.checkCanUpdateEventType(user.id, eventTypeId); + const eventTypeUser = await this.getUserToUpdateEvent(user); + const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body); + await updateEventType({ + input: { id: eventTypeId, ...bodyTransformed }, + ctx: { + user: eventTypeUser, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prisma: this.dbWrite.prisma, + }, + }); + + const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + + return this.outputEventTypesService.getResponseEventType(user.id, eventType); + } + + async checkCanUpdateEventType(userId: number, eventTypeId: number) { + const existingEventType = await this.getUserEventType(userId, eventTypeId); + if (!existingEventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + this.checkUserOwnsEventType(userId, { id: eventTypeId, userId: existingEventType.ownerId }); + } + + async getUserToUpdateEvent(user: UserWithProfile) { + const profileId = user.movedToProfile?.id || null; + const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id); + return { ...user, profile: { id: profileId }, selectedCalendars }; + } + + async deleteEventType(eventTypeId: number, userId: number) { + const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); + if (!existingEventType) { + throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`); + } + + this.checkUserOwnsEventType(userId, existingEventType); + + return this.eventTypesRepository.deleteEventType(eventTypeId); + } + + checkUserOwnsEventType(userId: number, eventType: Pick) { + if (userId !== eventType.userId) { + throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`); + } + } +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts new file mode 100644 index 0000000000..3f463459f1 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from "@nestjs/common"; + +import { + transformApiEventTypeBookingFields, + transformApiEventTypeLocations, +} from "@calcom/platform-libraries-0.0.13"; +import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; + +@Injectable() +export class InputEventTypesService_2024_06_14 { + transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) { + const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [ + { + type: "integration", + integration: "cal-video", + }, + ]; + + const { lengthInMinutes, locations, bookingFields, ...rest } = inputEventType; + + const eventType = { + ...rest, + length: lengthInMinutes, + locations: this.transformInputLocations(locations || defaultLocations), + bookingFields: this.transformInputBookingFields(bookingFields), + }; + + return eventType; + } + + transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) { + const { locations, bookingFields, ...rest } = inputEventType; + + const eventType = { + ...rest, + locations: locations ? this.transformInputLocations(locations) : undefined, + bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined, + }; + + return eventType; + } + + transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) { + return transformApiEventTypeLocations(inputLocations); + } + + transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { + return transformApiEventTypeBookingFields(inputBookingFields); + } +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts new file mode 100644 index 0000000000..09b829ac72 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -0,0 +1,150 @@ +import { Injectable } from "@nestjs/common"; +import type { EventType, User, Schedule } from "@prisma/client"; + +import { + EventTypeMetaDataSchema, + userMetadata, + getResponseEventTypeLocations, + getResponseEventTypeBookingFields, + parseRecurringEvent, + TransformedLocationsSchema, + BookingFieldsSchema, +} from "@calcom/platform-libraries-0.0.13"; + +type EventTypeRelations = { users: User[]; schedule: Schedule | null }; +type DatabaseEventType = EventType & EventTypeRelations; + +type Input = Pick< + DatabaseEventType, + | "id" + | "length" + | "title" + | "description" + | "disableGuests" + | "slotInterval" + | "minimumBookingNotice" + | "beforeEventBuffer" + | "afterEventBuffer" + | "slug" + | "schedulingType" + | "requiresConfirmation" + | "price" + | "currency" + | "lockTimeZoneToggleOnBookingPage" + | "seatsPerTimeSlot" + | "forwardParamsSuccessRedirect" + | "successRedirectUrl" + | "seatsShowAvailabilityCount" + | "isInstantEvent" + | "locations" + | "bookingFields" + | "recurringEvent" + | "metadata" + | "users" + | "schedule" +>; + +@Injectable() +export class OutputEventTypesService_2024_06_14 { + async getResponseEventType(ownerId: number, databaseEventType: Input) { + const { + id, + length, + title, + description, + disableGuests, + slotInterval, + minimumBookingNotice, + beforeEventBuffer, + afterEventBuffer, + slug, + schedulingType, + requiresConfirmation, + price, + currency, + lockTimeZoneToggleOnBookingPage, + seatsPerTimeSlot, + forwardParamsSuccessRedirect, + successRedirectUrl, + seatsShowAvailabilityCount, + isInstantEvent, + } = databaseEventType; + + const locations = this.transformLocations(databaseEventType.locations); + const bookingFields = this.transformBookingFields(databaseEventType.bookingFields); + const recurringEvent = this.transformRecurringEvent(databaseEventType.recurringEvent); + const metadata = this.transformMetadata(databaseEventType.metadata) || {}; + const schedule = await this.getSchedule(databaseEventType); + const users = this.transformUsers(databaseEventType.users); + + return { + id, + ownerId, + lengthInMinutes: length, + title, + slug, + description: description || "", + locations, + bookingFields, + recurringEvent, + disableGuests, + slotInterval, + minimumBookingNotice, + beforeEventBuffer, + afterEventBuffer, + schedulingType, + metadata, + requiresConfirmation, + price, + currency, + lockTimeZoneToggleOnBookingPage, + seatsPerTimeSlot, + forwardParamsSuccessRedirect, + successRedirectUrl, + seatsShowAvailabilityCount, + isInstantEvent, + users, + schedule, + }; + } + + transformLocations(locations: any) { + if (!locations) return []; + return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations)); + } + + transformBookingFields(inputBookingFields: any) { + if (!inputBookingFields) return []; + return getResponseEventTypeBookingFields(BookingFieldsSchema.parse(inputBookingFields)); + } + + transformRecurringEvent(recurringEvent: any) { + if (!recurringEvent) return null; + return parseRecurringEvent(recurringEvent); + } + + transformMetadata(metadata: any) { + if (!metadata) return {}; + return EventTypeMetaDataSchema.parse(metadata); + } + + async getSchedule(databaseEventType: Input) { + return databaseEventType.schedule || null; + } + + transformUsers(users: User[]) { + return users.map((user) => { + const metadata = user.metadata ? userMetadata.parse(user.metadata) : {}; + return { + id: user.id, + name: user.name, + username: user.username, + avatarUrl: user.avatarUrl, + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + weekStart: user.weekStart, + metadata: metadata || {}, + }; + }); + } +} diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts index 67c72ab70b..0e7df6730a 100644 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ b/apps/api/v2/src/ee/platform-endpoints-module.ts @@ -1,6 +1,7 @@ import { BookingsModule } from "@/ee/bookings/bookings.module"; import { CalendarsModule } from "@/ee/calendars/calendars.module"; -import { EventTypesModule } from "@/ee/event-types/event-types.module"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; import { GcalModule } from "@/ee/gcal/gcal.module"; import { MeModule } from "@/ee/me/me.module"; import { ProviderModule } from "@/ee/provider/provider.module"; @@ -17,7 +18,8 @@ import { Module } from "@nestjs/common"; SchedulesModule_2024_04_15, SchedulesModule_2024_06_11, MeModule, - EventTypesModule, + EventTypesModule_2024_04_15, + EventTypesModule_2024_06_14, CalendarsModule, BookingsModule, SlotsModule, diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts index fe89e47f7b..bb60283f07 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts @@ -1,5 +1,5 @@ import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { VERSION_2024_06_11_VALUE } from "@/lib/api-versions"; +import { VERSION_2024_06_14, VERSION_2024_06_11 } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; @@ -34,7 +34,7 @@ import { @Controller({ path: "/v2/schedules", - version: VERSION_2024_06_11_VALUE, + version: [VERSION_2024_06_14, VERSION_2024_06_11], }) @UseGuards(ApiAuthGuard, PermissionsGuard) @DocsTags("Schedules") diff --git a/apps/api/v2/src/lib/api-versions.ts b/apps/api/v2/src/lib/api-versions.ts index 4d079367ae..f4352e3ef9 100644 --- a/apps/api/v2/src/lib/api-versions.ts +++ b/apps/api/v2/src/lib/api-versions.ts @@ -1,7 +1,16 @@ import { VersionValue } from "@nestjs/common/interfaces"; -import { API_VERSIONS, VERSION_2024_04_15, VERSION_2024_06_11 } from "@calcom/platform-constants"; +import { + API_VERSIONS, + VERSION_2024_04_15, + VERSION_2024_06_11, + VERSION_2024_06_14, +} from "@calcom/platform-constants"; export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue; +export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unknown as VersionValue; export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue; export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue; + +export { VERSION_2024_06_11 }; +export { VERSION_2024_06_14 }; diff --git a/apps/api/v2/src/modules/endpoints.module.ts b/apps/api/v2/src/modules/endpoints.module.ts index 3d8b402e2d..3134bd2bd5 100644 --- a/apps/api/v2/src/modules/endpoints.module.ts +++ b/apps/api/v2/src/modules/endpoints.module.ts @@ -5,8 +5,10 @@ import { TimezoneModule } from "@/modules/timezones/timezones.module"; import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; import { Module } from "@nestjs/common"; +import { UsersModule } from "./users/users.module"; + @Module({ - imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule], + imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule, UsersModule], }) export class EndpointsModule implements NestModule { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 59cc6b2cea..0db6a794d0 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -1,6 +1,6 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants"; +import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants"; import { HttpExceptionFilter } from "@/filters/http-exception.filter"; import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; import { diff --git a/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts b/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts index 04393d27e7..8ec2c656ec 100644 --- a/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts +++ b/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts @@ -1,4 +1,4 @@ -import { EventTypesModule } from "@/ee/event-types/event-types.module"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { AuthModule } from "@/modules/auth/auth.module"; import { BillingModule } from "@/modules/billing/billing.module"; @@ -28,7 +28,7 @@ import { Global, Module } from "@nestjs/common"; UsersModule, TokensModule, MembershipsModule, - EventTypesModule, + EventTypesModule_2024_04_15, OrganizationsModule, StripeModule, BillingModule, diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 691f33c59b..3ee2dbcf79 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -1,4 +1,4 @@ -import { EventTypesService } from "@/ee/event-types/services/event-types.service"; +import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; @@ -14,7 +14,7 @@ export class OAuthClientUsersService { constructor( private readonly userRepository: UsersRepository, private readonly tokensRepository: TokensRepository, - private readonly eventTypesService: EventTypesService, + private readonly eventTypesService: EventTypesService_2024_04_15, private readonly schedulesService: SchedulesService_2024_04_15 ) {} diff --git a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts index 17ba7bb70d..9b4621aea5 100644 --- a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts @@ -1,7 +1,6 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { SlotsService } from "@/modules/slots/services/slots.service"; -import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common"; +import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { Response as ExpressResponse, Request as ExpressRequest } from "express"; diff --git a/apps/api/v2/src/modules/slots/services/slots.service.ts b/apps/api/v2/src/modules/slots/services/slots.service.ts index 1d17340ec8..4ba2e2c37c 100644 --- a/apps/api/v2/src/modules/slots/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/services/slots.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository } from "@/ee/event-types/event-types.repository"; +import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; import { SlotsRepository } from "@/modules/slots/slots.repository"; import { Injectable, NotFoundException } from "@nestjs/common"; import { v4 as uuid } from "uuid"; @@ -8,7 +8,7 @@ import { ReserveSlotInput } from "@calcom/platform-types"; @Injectable() export class SlotsService { constructor( - private readonly eventTypeRepo: EventTypesRepository, + private readonly eventTypeRepo: EventTypesRepository_2024_04_15, private readonly slotsRepo: SlotsRepository ) {} diff --git a/apps/api/v2/src/modules/slots/slots.module.ts b/apps/api/v2/src/modules/slots/slots.module.ts index 94e7c0d27f..e85b49af6a 100644 --- a/apps/api/v2/src/modules/slots/slots.module.ts +++ b/apps/api/v2/src/modules/slots/slots.module.ts @@ -1,4 +1,4 @@ -import { EventTypesModule } from "@/ee/event-types/event-types.module"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SlotsController } from "@/modules/slots/controllers/slots.controller"; import { SlotsService } from "@/modules/slots/services/slots.service"; @@ -6,7 +6,7 @@ import { SlotsRepository } from "@/modules/slots/slots.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, EventTypesModule], + imports: [PrismaModule, EventTypesModule_2024_04_15], providers: [SlotsRepository, SlotsService], controllers: [SlotsController], exports: [SlotsService], diff --git a/apps/api/v2/src/modules/users/services/users.service.ts b/apps/api/v2/src/modules/users/services/users.service.ts new file mode 100644 index 0000000000..c8fbb44c6b --- /dev/null +++ b/apps/api/v2/src/modules/users/services/users.service.ts @@ -0,0 +1,24 @@ +import { UsersRepository } from "@/modules/users/users.repository"; +import { Injectable } from "@nestjs/common"; + +import { User } from "@calcom/prisma/client"; + +@Injectable() +export class UsersService { + constructor(private readonly usersRepository: UsersRepository) {} + + async getByUsernames(usernames: string[]) { + const users = await Promise.all( + usernames.map((username) => this.usersRepository.findByUsername(username)) + ); + const usersFiltered: User[] = []; + + for (const user of users) { + if (user) { + usersFiltered.push(user); + } + } + + return users; + } +} diff --git a/apps/api/v2/src/modules/users/users.module.ts b/apps/api/v2/src/modules/users/users.module.ts index 932f4e0ea9..a7eed6c99d 100644 --- a/apps/api/v2/src/modules/users/users.module.ts +++ b/apps/api/v2/src/modules/users/users.module.ts @@ -1,10 +1,13 @@ +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule], - providers: [UsersRepository], - exports: [UsersRepository], + imports: [PrismaModule, EventTypesModule_2024_06_14, TokensModule], + providers: [UsersRepository, UsersService], + exports: [UsersRepository, UsersService], }) export class UsersModule {} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 3b8089a753..2bfed78bce 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -577,14 +577,14 @@ }, "/v2/event-types": { "post": { - "operationId": "EventTypesController_createEventType", + "operationId": "EventTypesController_2024_04_15_createEventType", "parameters": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateEventTypeInput" + "$ref": "#/components/schemas/CreateEventTypeInput_2024_04_15" } } } @@ -606,7 +606,7 @@ ] }, "get": { - "operationId": "EventTypesController_getEventTypes", + "operationId": "EventTypesController_2024_04_15_getEventTypes", "parameters": [], "responses": { "200": { @@ -627,7 +627,7 @@ }, "/v2/event-types/{eventTypeId}": { "get": { - "operationId": "EventTypesController_getEventType", + "operationId": "EventTypesController_2024_04_15_getEventType", "parameters": [ { "name": "eventTypeId", @@ -655,7 +655,7 @@ ] }, "patch": { - "operationId": "EventTypesController_updateEventType", + "operationId": "EventTypesController_2024_04_15_updateEventType", "parameters": [ { "name": "eventTypeId", @@ -671,7 +671,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateEventTypeInput" + "$ref": "#/components/schemas/UpdateEventTypeInput_2024_04_15" } } } @@ -693,7 +693,7 @@ ] }, "delete": { - "operationId": "EventTypesController_deleteEventType", + "operationId": "EventTypesController_2024_04_15_deleteEventType", "parameters": [ { "name": "eventTypeId", @@ -723,7 +723,7 @@ }, "/v2/event-types/{username}/{eventSlug}/public": { "get": { - "operationId": "EventTypesController_getPublicEventType", + "operationId": "EventTypesController_2024_04_15_getPublicEventType", "parameters": [ { "name": "username", @@ -778,7 +778,7 @@ }, "/v2/event-types/{username}/public": { "get": { - "operationId": "EventTypesController_getPublicEventTypes", + "operationId": "EventTypesController_2024_04_15_getPublicEventTypes", "parameters": [ { "name": "username", @@ -2146,7 +2146,179 @@ "refreshToken" ] }, - "EventTypeLocation": { + "CreateEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + } + }, + "required": [ + "lengthInMinutes", + "title", + "description" + ] + }, + "EventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + } + }, + "required": [ + "id" + ] + }, + "CreateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypesOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateEventTypeInput_2024_06_14": { + "type": "object", + "properties": {} + }, + "UpdateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug" + ] + }, + "DeleteEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeleteData_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeLocation_2024_04_15": { "type": "object", "properties": { "type": { @@ -2162,7 +2334,7 @@ "type" ] }, - "CreateEventTypeInput": { + "CreateEventTypeInput_2024_04_15": { "type": "object", "properties": { "length": { @@ -2185,7 +2357,7 @@ "locations": { "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeLocation" + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" } }, "disableGuests": { @@ -2242,7 +2414,7 @@ "nullable": true, "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeLocation" + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" } } }, @@ -2864,7 +3036,143 @@ "data" ] }, - "UpdateEventTypeInput": { + "Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ] + }, + "VariantsConfig": { + "type": "object", + "properties": { + "variants": { + "type": "object" + } + }, + "required": [ + "variants" + ] + }, + "View": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "label" + ] + }, + "BookingField_2024_04_15": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "number", + "boolean", + "address", + "name", + "text", + "textarea", + "email", + "phone", + "multiemail", + "select", + "multiselect", + "checkbox", + "radio", + "radioInput" + ] + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Option" + } + }, + "label": { + "type": "string" + }, + "labelAsSafeHtml": { + "type": "string" + }, + "defaultLabel": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "getOptionsAt": { + "type": "string" + }, + "optionsInputs": { + "type": "object" + }, + "variant": { + "type": "string" + }, + "variantsConfig": { + "$ref": "#/components/schemas/VariantsConfig" + }, + "views": { + "type": "array", + "items": { + "$ref": "#/components/schemas/View" + } + }, + "hideWhenJustOneOption": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "editable": { + "type": "string", + "enum": [ + "system", + "system-but-optional", + "system-but-hidden", + "user", + "user-readonly" + ] + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + } + } + }, + "required": [ + "type", + "name" + ] + }, + "UpdateEventTypeInput_2024_04_15": { "type": "object", "properties": { "length": { @@ -2886,13 +3194,13 @@ "locations": { "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeLocation" + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" } }, "bookingFields": { "type": "array", "items": { - "$ref": "#/components/schemas/BookingField" + "$ref": "#/components/schemas/BookingField_2024_04_15" } }, "disableGuests": { diff --git a/apps/api/v2/test/fixtures/repository/event-types.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/event-types.repository.fixture.ts index 46e3cc0c03..4d0d15fd18 100644 --- a/apps/api/v2/test/fixtures/repository/event-types.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/event-types.repository.fixture.ts @@ -1,4 +1,4 @@ -import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { TestingModule } from "@nestjs/testing"; @@ -21,7 +21,10 @@ export class EventTypesRepositoryFixture { }); } - async create(data: Pick, userId: number) { + async create( + data: Pick, + userId: number + ) { return this.prismaWriteClient.eventType.create({ data: { ...data, diff --git a/packages/app-store/_utils/getEventTypeAppData.ts b/packages/app-store/_utils/getEventTypeAppData.ts index fed3e225d3..08c60d3d18 100644 --- a/packages/app-store/_utils/getEventTypeAppData.ts +++ b/packages/app-store/_utils/getEventTypeAppData.ts @@ -1,13 +1,13 @@ import type { z } from "zod"; -import type { EventTypeModel } from "@calcom/prisma/zod"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; export type EventTypeApps = NonNullable>["apps"]>; export type EventTypeAppsList = keyof EventTypeApps; export const getEventTypeAppData = ( - eventType: Pick, "price" | "currency" | "metadata">, + eventType: Pick, appId: T, forcedGet?: boolean ): EventTypeApps[T] => { diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx index 0d112b861b..574bb11536 100644 --- a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx +++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import dayjs from "@calcom/dayjs"; import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; import { classNames } from "@calcom/lib"; @@ -19,7 +19,9 @@ type AvailableTimeSlotsProps = { isLoading: boolean; seatsPerTimeSlot?: number | null; showAvailableSeatsCount?: boolean | null; - event: useEventReturnType; + event: { + data?: Pick | null; + }; customClassNames?: { availableTimeSlotsContainer?: string; availableTimeSlotsTitle?: string; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index d5d09ebc7e..15e1c15cb7 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -4,13 +4,13 @@ import Link from "next/link"; import { useMemo, useState } from "react"; import type { FieldError } from "react-hook-form"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Button, EmptyScreen, Form } from "@calcom/ui"; import { useBookerStore } from "../../store"; -import type { useEventReturnType } from "../../utils/event"; import type { UseBookingFormReturnType } from "../hooks/useBookingForm"; import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBookings"; import { BookingFields } from "./BookingFields"; @@ -43,7 +43,11 @@ export const BookEventForm = ({ extraOptions, isPlatform = false, }: Omit & { - eventQuery: useEventReturnType; + eventQuery: { + isError: boolean; + isPending: boolean; + data?: Pick | null; + }; rescheduleUid: string | null; }) => { const eventType = eventQuery.data; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx index b3d2016103..888e156253 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import React from "react"; -import { useIsPlatform, useGetEventTypeById } from "@calcom/atoms/monorepo"; +import { useEventTypeById, useIsPlatform } from "@calcom/atoms/monorepo"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Badge, Dialog, DialogContent } from "@calcom/ui"; @@ -25,14 +25,10 @@ const PlatformBookEventFormWrapper = ({ children: ReactNode; }) => { const eventId = useBookerStore((state) => state.eventId); - const { data } = useGetEventTypeById(eventId); + const { data } = useEventTypeById(eventId); return ( - + ); }; diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx index b833b8cde0..fa07508125 100644 --- a/packages/features/bookings/Booker/components/DatePicker.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -6,9 +6,10 @@ import { default as DatePickerComponent } from "@calcom/features/calendars/DateP import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { User } from "@calcom/prisma/client"; import { useBookerStore } from "../store"; -import type { useEventReturnType, useScheduleForEventReturnType } from "../utils/event"; +import type { useScheduleForEventReturnType } from "../utils/event"; export const DatePicker = ({ event, @@ -16,7 +17,9 @@ export const DatePicker = ({ classNames, scrollToTimeSlots, }: { - event: useEventReturnType; + event: { + data?: { users: Pick[] } | null; + }; schedule: useScheduleForEventReturnType; classNames?: { datePickerContainer?: string; diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 8806037d46..164881ac77 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -9,12 +9,12 @@ import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calc import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { useTimePreferences } from "@calcom/features/bookings/lib"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { fadeInUp } from "../config"; import { useBookerStore } from "../store"; import { FromToTime } from "../utils/dates"; -import type { useEventReturnType } from "../utils/event"; const WebTimezoneSelect = dynamic( () => import("@calcom/ui/components/form/timezone-select/TimezoneSelect").then((mod) => mod.TimezoneSelect), @@ -29,8 +29,27 @@ export const EventMeta = ({ isPlatform = true, classNames, }: { - event: useEventReturnType["data"]; - isPending: useEventReturnType["isPending"]; + event?: Pick< + BookerEvent, + | "lockTimeZoneToggleOnBookingPage" + | "schedule" + | "seatsPerTimeSlot" + | "users" + | "length" + | "schedulingType" + | "profile" + | "entity" + | "description" + | "title" + | "metadata" + | "locations" + | "currency" + | "requiresConfirmation" + | "recurringEvent" + | "price" + | "isDynamic" + > | null; + isPending: boolean; isPlatform?: boolean; classNames?: { eventMetaContainer?: string; diff --git a/packages/features/bookings/Booker/components/InstantBooking.tsx b/packages/features/bookings/Booker/components/InstantBooking.tsx index 11db897e5a..ef0a9c9162 100644 --- a/packages/features/bookings/Booker/components/InstantBooking.tsx +++ b/packages/features/bookings/Booker/components/InstantBooking.tsx @@ -1,11 +1,14 @@ -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { User } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { Button, UserAvatarGroupWithOrg } from "@calcom/ui"; interface IInstantBookingProps { onConnectNow: () => void; - event: NonNullable; + event: Pick & { + users: (Pick & { bookerUrl: string })[]; + }; } export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => { diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx index ab7b0d2196..64e3a025da 100644 --- a/packages/features/bookings/Booker/components/LargeCalendar.tsx +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -1,13 +1,14 @@ import { useMemo, useEffect } from "react"; import dayjs from "@calcom/dayjs"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { Calendar } from "@calcom/features/calendars/weeklyview"; import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events"; import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; import { localStorage } from "@calcom/lib/webstorage"; import { useBookerStore } from "../store"; -import type { useEventReturnType, useScheduleForEventReturnType } from "../utils/event"; +import type { useScheduleForEventReturnType } from "../utils/event"; import { getQueryParam } from "../utils/query-param"; import { useOverlayCalendarStore } from "./OverlayCalendar/store"; @@ -20,7 +21,9 @@ export const LargeCalendar = ({ extraDays: number; schedule?: useScheduleForEventReturnType["data"]; isLoading: boolean; - event: useEventReturnType; + event: { + data?: Pick | null; + }; }) => { const selectedDate = useBookerStore((state) => state.selectedDate); const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); diff --git a/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts b/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts index c6130ee1dd..56c3df6f4e 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts @@ -2,6 +2,7 @@ import { useEffect, useRef } from "react"; import { shallow } from "zustand/shallow"; import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import type { BookerLayouts } from "@calcom/prisma/zod-utils"; import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; @@ -9,13 +10,12 @@ import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; import { extraDaysConfig } from "../../config"; import { useBookerStore } from "../../store"; import type { BookerLayout } from "../../types"; -import type { useEventReturnType } from "../../utils/event"; import { validateLayout } from "../../utils/layout"; import { getQueryParam } from "../../utils/query-param"; export type UseBookerLayoutType = ReturnType; -export const useBookerLayout = (event: useEventReturnType["data"]) => { +export const useBookerLayout = (event: Pick | undefined | null) => { const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const isEmbed = useIsEmbed(); const isMobile = useMediaQuery("(max-width: 768px)"); diff --git a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts index ea6e644953..8a6ec51528 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts @@ -5,14 +5,14 @@ import { z } from "zod"; import type { EventLocationType } from "@calcom/app-store/locations"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useInitialFormValues } from "./useInitialFormValues"; export interface IUseBookingForm { - event: useEventReturnType["data"]; + event?: Pick | null; sessionEmail?: string | null; sessionName?: string | null; sessionUsername?: string | null; diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index f0b15f4817..59fb8b5442 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -7,9 +7,9 @@ import { useHandleBookEvent } from "@calcom/atoms/monorepo"; import dayjs from "@calcom/dayjs"; import { sdkActionManager } from "@calcom/embed-core/embed-iframe"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param"; import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { getFullName } from "@calcom/features/form-builder/utils"; import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -21,7 +21,29 @@ import { showToast } from "@calcom/ui"; import type { UseBookingFormReturnType } from "./useBookingForm"; export interface IUseBookings { - event: useEventReturnType; + event: { + data?: + | (Pick< + BookerEvent, + | "id" + | "slug" + | "hosts" + | "requiresConfirmation" + | "isDynamic" + | "metadata" + | "forwardParamsSuccessRedirect" + | "successRedirectUrl" + | "length" + | "recurringEvent" + | "schedulingType" + > & { + users: Pick< + BookerEvent["users"][number], + "name" | "username" | "avatarUrl" | "weekStart" | "profile" | "bookerUrl" + >[]; + }) + | null; + }; hashedLink?: string | null; bookingForm: UseBookingFormReturnType["bookingForm"]; metadata: Record; diff --git a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts index a4aeca2fed..8650851ec7 100644 --- a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts +++ b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts @@ -2,14 +2,14 @@ import { useEffect, useState } from "react"; import type { z } from "zod"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; -import type { useEvent } from "@calcom/features/bookings/Booker/utils/event"; import type getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; +import type { BookerEvent } from "@calcom/features/bookings/types"; export type useInitialFormValuesReturnType = ReturnType; type UseInitialFormValuesProps = { - eventType: ReturnType["data"]; + eventType?: Pick | null; rescheduleUid: string | null; isRescheduling: boolean; email?: string | null; diff --git a/packages/features/bookings/Booker/components/hooks/useSlots.ts b/packages/features/bookings/Booker/components/hooks/useSlots.ts index f578ae9c1a..f7a30442bc 100644 --- a/packages/features/bookings/Booker/components/hooks/useSlots.ts +++ b/packages/features/bookings/Booker/components/hooks/useSlots.ts @@ -4,13 +4,13 @@ import { shallow } from "zustand/shallow"; import dayjs from "@calcom/dayjs"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { MINUTES_TO_BOOK } from "@calcom/lib/constants"; import { trpc } from "@calcom/trpc"; export type UseSlotsReturnType = ReturnType; -export const useSlots = (event: useEventReturnType) => { +export const useSlots = (event: { data?: Pick | null }) => { const selectedDuration = useBookerStore((state) => state.selectedDuration); const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( (state) => [state.selectedTimeslot, state.setSelectedTimeslot], diff --git a/packages/features/bookings/Booker/types.ts b/packages/features/bookings/Booker/types.ts index 07dbf62bf2..be0dc82cdd 100644 --- a/packages/features/bookings/Booker/types.ts +++ b/packages/features/bookings/Booker/types.ts @@ -5,10 +5,8 @@ import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/co import type { UseSlotsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useSlots"; import type { UseVerifyCodeReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; import type { UseVerifyEmailReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; -import type { - useEventReturnType, - useScheduleForEventReturnType, -} from "@calcom/features/bookings/Booker/utils/event"; +import type { useScheduleForEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; +import type { BookerEventQuery } from "@calcom/features/bookings/types"; import type { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { GetBookingType } from "../lib/get-booking"; @@ -106,7 +104,7 @@ export type WrappedBookerPropsMain = { slots: UseSlotsReturnType; calendars: UseCalendarsReturnType; bookerForm: UseBookingFormReturnType; - event: useEventReturnType; + event: BookerEventQuery; schedule: useScheduleForEventReturnType; bookerLayout: UseBookerLayoutType; verifyEmail: UseVerifyEmailReturnType; diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 5d29fa8e60..abbc52d317 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -7,6 +7,7 @@ import { useIsPlatform } from "@calcom/atoms/monorepo"; import type { IOutOfOfficeData } from "@calcom/core/getUserAvailability"; import dayjs from "@calcom/dayjs"; import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import type { Slots } from "@calcom/features/schedules"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -15,7 +16,6 @@ import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slot import { Button, Icon, SkeletonText } from "@calcom/ui"; import { useBookerStore } from "../Booker/store"; -import type { useEventReturnType } from "../Booker/utils/event"; import { getQueryParam } from "../Booker/utils/query-param"; import { useTimePreferences } from "../lib"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; @@ -36,7 +36,9 @@ type AvailableTimesProps = { showTimeFormatToggle?: boolean; className?: string; selectedSlots?: string[]; - event: useEventReturnType; + event: { + data?: Pick | null; + }; customClassNames?: string; }; @@ -54,7 +56,9 @@ const SlotItem = ({ selectedSlots?: string[]; onTimeSelect: TOnTimeSelect; showAvailableSeatsCount?: boolean | null; - event: useEventReturnType; + event: { + data?: Pick | null; + }; customClassNames?: string; }) => { const { t } = useLocale(); diff --git a/packages/features/bookings/components/event-meta/Details.tsx b/packages/features/bookings/components/event-meta/Details.tsx index 2c5b824d38..f59408c552 100644 --- a/packages/features/bookings/components/event-meta/Details.tsx +++ b/packages/features/bookings/components/event-meta/Details.tsx @@ -2,12 +2,12 @@ import React, { Fragment } from "react"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { PriceIcon } from "@calcom/features/bookings/components/event-meta/PriceIcon"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import classNames from "@calcom/lib/classNames"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Icon, type IconName } from "@calcom/ui"; -import type { PublicEvent } from "../../types"; import { EventDetailBlocks } from "../../types"; import { AvailableEventLocations } from "./AvailableEventLocations"; import { EventDuration } from "./Duration"; @@ -15,7 +15,17 @@ import { EventOccurences } from "./Occurences"; import { Price } from "./Price"; type EventDetailsPropsBase = { - event: PublicEvent; + event: Pick< + BookerEvent, + | "currency" + | "price" + | "locations" + | "requiresConfirmation" + | "recurringEvent" + | "length" + | "metadata" + | "isDynamic" + >; className?: string; }; diff --git a/packages/features/bookings/components/event-meta/Duration.tsx b/packages/features/bookings/components/event-meta/Duration.tsx index e6d1e67a39..dbd3cdc545 100644 --- a/packages/features/bookings/components/event-meta/Duration.tsx +++ b/packages/features/bookings/components/event-meta/Duration.tsx @@ -3,12 +3,11 @@ import { useEffect } from "react"; import { useIsPlatform } from "@calcom/atoms/monorepo"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Badge } from "@calcom/ui"; -import type { PublicEvent } from "../../types"; - /** Render X mins as X hours or X hours Y mins instead of in minutes once >= 60 minutes */ export const getDurationFormatted = (mins: number | undefined, t: TFunction) => { if (!mins) return null; @@ -36,7 +35,11 @@ export const getDurationFormatted = (mins: number | undefined, t: TFunction) => return hourStr || minStr; }; -export const EventDuration = ({ event }: { event: PublicEvent }) => { +export const EventDuration = ({ + event, +}: { + event: Pick; +}) => { const { t } = useLocale(); const isPlatform = useIsPlatform(); const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [ diff --git a/packages/features/bookings/components/event-meta/EventMeta.stories.tsx b/packages/features/bookings/components/event-meta/EventMeta.stories.tsx index 9bb5d2c284..e9f753b62a 100644 --- a/packages/features/bookings/components/event-meta/EventMeta.stories.tsx +++ b/packages/features/bookings/components/event-meta/EventMeta.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import type { PublicEvent } from "bookings/types"; import type { ComponentProps } from "react"; import { Examples, Example, VariantsTable, VariantRow } from "@calcom/storybook/components"; @@ -49,9 +48,15 @@ export const AllVariants: Story = { Quick catch-up diff --git a/packages/features/bookings/components/event-meta/Locations.tsx b/packages/features/bookings/components/event-meta/Locations.tsx index 81594d026b..c201ed3c38 100644 --- a/packages/features/bookings/components/event-meta/Locations.tsx +++ b/packages/features/bookings/components/event-meta/Locations.tsx @@ -1,18 +1,18 @@ import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Tooltip } from "@calcom/ui"; -import type { PublicEvent } from "../../types"; import { EventMetaBlock } from "./Details"; -export const EventLocations = ({ event }: { event: PublicEvent }) => { +export const EventLocations = ({ event }: { event: BookerEvent }) => { const { t } = useLocale(); const locations = event.locations; if (!locations?.length) return null; - const getLocationToDisplay = (location: PublicEvent["locations"][number]) => { + const getLocationToDisplay = (location: BookerEvent["locations"][number]) => { const eventLocationType = getEventLocationType(location.type); const translatedLocation = getTranslatedLocation(location, eventLocationType, t); diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx index 6e718d5990..53117b8e3b 100644 --- a/packages/features/bookings/components/event-meta/Members.tsx +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -1,22 +1,21 @@ import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { AvatarGroup } from "@calcom/ui"; -import type { PublicEvent } from "../../types"; - export interface EventMembersProps { /** * Used to determine whether all members should be shown or not. * In case of Round Robin type, members aren't shown. */ - schedulingType: PublicEvent["schedulingType"]; - users: PublicEvent["users"]; - profile: PublicEvent["profile"]; - entity: PublicEvent["entity"]; + schedulingType: BookerEvent["schedulingType"]; + users: BookerEvent["users"]; + profile: BookerEvent["profile"]; + entity: BookerEvent["entity"]; } export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => { diff --git a/packages/features/bookings/components/event-meta/Occurences.tsx b/packages/features/bookings/components/event-meta/Occurences.tsx index 38cbdadcdd..1a0c716007 100644 --- a/packages/features/bookings/components/event-meta/Occurences.tsx +++ b/packages/features/bookings/components/event-meta/Occurences.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { parseRecurringDates } from "@calcom/lib/parse-dates"; import { getRecurringFreq } from "@calcom/lib/recurringStrings"; @@ -8,9 +9,8 @@ import { Tooltip, Alert } from "@calcom/ui"; import { Input } from "@calcom/ui"; import { useTimePreferences } from "../../lib"; -import type { PublicEvent } from "../../types"; -export const EventOccurences = ({ event }: { event: PublicEvent }) => { +export const EventOccurences = ({ event }: { event: Pick }) => { const maxOccurences = event.recurringEvent?.count || null; const { t, i18n } = useLocale(); const [setRecurringEventCount, recurringEventCount, setOccurenceCount, occurenceCount] = useBookerStore( diff --git a/packages/features/bookings/components/event-meta/event.mock.ts b/packages/features/bookings/components/event-meta/event.mock.ts index 100e1b0fe0..af95d26471 100644 --- a/packages/features/bookings/components/event-meta/event.mock.ts +++ b/packages/features/bookings/components/event-meta/event.mock.ts @@ -1,6 +1,6 @@ -import type { PublicEvent } from "bookings/types"; +import type { BookerEvent } from "bookings/types"; -export const mockEvent: PublicEvent = { +export const mockEvent: BookerEvent = { id: 1, title: "Quick check-in", slug: "quick-check-in", @@ -8,10 +8,10 @@ export const mockEvent: PublicEvent = { description: "Use this event for a quick 15 minute catchup. Visit this long url to test the component https://cal.com/averylongurlwithoutspacesthatshouldntbreaklayout", users: [ - { name: "Pro example", username: "pro", weekStart: "Sunday", organizationId: null }, - { name: "Team example", username: "team", weekStart: "Sunday", organizationId: 1 }, + { name: "Pro example", username: "pro", weekStart: "Sunday", avatarUrl: "", profile: null }, + { name: "Team example", username: "team", weekStart: "Sunday", avatarUrl: "", profile: null }, ], schedulingType: null, length: 30, locations: [{ type: "integrations:google:meet" }, { type: "integrations:zoom" }], -} as PublicEvent; // TODO: complete mock and remove type assertion +}; diff --git a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx index 206b073113..54c0aac930 100644 --- a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx +++ b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx @@ -3,11 +3,11 @@ import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { parseRecurringDates } from "@calcom/lib/parse-dates"; -import type { PublicEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../types"; +import type { BookerEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../types"; export type BookingOptions = { values: Record; - event: PublicEvent; + event: Pick; date: string; // @NOTE: duration is not validated in this function duration: number | undefined | null; diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts index c327a5b554..09b85c3812 100644 --- a/packages/features/bookings/types.ts +++ b/packages/features/bookings/types.ts @@ -6,6 +6,53 @@ import type { RouterOutputs } from "@calcom/trpc/react"; import type { AppsStatus } from "@calcom/types/Calendar"; export type PublicEvent = NonNullable; + +export type BookerEventQuery = { + isSuccess: boolean; + isError: boolean; + isPending: boolean; + data?: BookerEvent | null; +}; + +type BookerEventUser = Pick< + PublicEvent["users"][number], + "name" | "username" | "avatarUrl" | "weekStart" | "profile" +> & { + metadata?: undefined; + brandColor?: string | null; + darkBrandColor?: string | null; + bookerUrl: string; +}; + +type BookerEventProfile = Pick; + +export type BookerEvent = Pick< + PublicEvent, + | "id" + | "length" + | "slug" + | "schedulingType" + | "recurringEvent" + | "entity" + | "locations" + | "metadata" + | "isDynamic" + | "requiresConfirmation" + | "price" + | "currency" + | "lockTimeZoneToggleOnBookingPage" + | "schedule" + | "seatsPerTimeSlot" + | "title" + | "description" + | "forwardParamsSuccessRedirect" + | "successRedirectUrl" + | "hosts" + | "bookingFields" + | "seatsShowAvailabilityCount" + | "isInstantEvent" +> & { users: BookerEventUser[] } & { profile: BookerEventProfile }; + export type ValidationErrors = { key: FieldPath; error: ErrorOption }[]; export type EventPrice = { currency: string; price: number; displayAlternateSymbol?: boolean }; diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 38e1b35f4d..0d6dcb4497 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -108,7 +108,7 @@ const commons = { secondaryEmail: null, }; -const dynamicEvent = { +export const dynamicEvent = { length: 30, slug: "dynamic", title: "Group Meeting", @@ -120,7 +120,7 @@ const dynamicEvent = { metadata: EventTypeMetaDataSchema.parse({ multipleDuration: [15, 30, 45, 60, 90] }), }; -const defaultEvents = [dynamicEvent]; +export const defaultEvents = [dynamicEvent]; export const getDynamicEventDescription = (dynamicUsernames: string[], slug: string): string => { return `Book a ${slug} min event with ${dynamicUsernames.join(", ")}`; diff --git a/packages/lib/event-types/index.ts b/packages/lib/event-types/index.ts new file mode 100644 index 0000000000..a8401e8101 --- /dev/null +++ b/packages/lib/event-types/index.ts @@ -0,0 +1 @@ +export * from "./transformers"; diff --git a/packages/lib/event-types/transformers/api-request.spec.ts b/packages/lib/event-types/transformers/api-request.spec.ts new file mode 100644 index 0000000000..f86944ca9f --- /dev/null +++ b/packages/lib/event-types/transformers/api-request.spec.ts @@ -0,0 +1,540 @@ +import { describe, expect, it } from "vitest"; + +import type { BookingField_2024_06_14, Location_2024_06_14 } from "@calcom/platform-types"; + +import type { CommonField, OptionsField } from "./api-request"; +import { + transformApiEventTypeLocations, + transformApiEventTypeBookingFields, + transformSelectOptions, +} from "./api-request"; + +describe("transformApiEventTypeLocations", () => { + it("should transform address", () => { + const input: Location_2024_06_14[] = [ + { + type: "address", + address: "London road 10-1", + public: true, + }, + ]; + + const expectedOutput = [{ type: "inPerson", address: "London road 10-1", displayLocationPublicly: true }]; + + const result = transformApiEventTypeLocations(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform link", () => { + const input: Location_2024_06_14[] = [ + { + type: "link", + link: "https://customvideo.com/join/123456", + public: true, + }, + ]; + + const expectedOutput = [ + { type: "link", link: "https://customvideo.com/join/123456", displayLocationPublicly: true }, + ]; + + const result = transformApiEventTypeLocations(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform integration", () => { + const input: Location_2024_06_14[] = [ + { + type: "integration", + integration: "cal-video", + }, + ]; + + const expectedOutput = [{ type: "integrations:daily" }]; + + const result = transformApiEventTypeLocations(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform phone", () => { + const input: Location_2024_06_14[] = [ + { + type: "phone", + phone: "+37120993151", + public: true, + }, + ]; + + const expectedOutput = [ + { type: "userPhone", hostPhoneNumber: "+37120993151", displayLocationPublicly: true }, + ]; + + const result = transformApiEventTypeLocations(input); + + expect(result).toEqual(expectedOutput); + }); +}); + +describe("transformApiEventTypeBookingFields", () => { + it("should transform name field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "name", + slug: "name", + label: "Your name", + required: true, + placeholder: "alice", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform email field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "email", + slug: "email", + label: "Your email", + required: true, + placeholder: "example@example.com", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform phone field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "phone", + slug: "phone", + label: "Your phone number", + required: true, + placeholder: "123456789", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform address field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "address", + slug: "address", + label: "Your address", + required: true, + placeholder: "1234 Main St", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform text field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "text", + slug: "text", + label: "Your text", + required: true, + placeholder: "Enter your text", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform number field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "number", + slug: "number", + label: "Your number", + required: true, + placeholder: "100", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform textarea field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "textarea", + slug: "textarea", + label: "Your detailed information", + required: true, + placeholder: "Detailed description here...", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform select field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "select", + slug: "select", + label: "Your selection", + required: true, + placeholder: "Select...", + options: ["Option 1", "Option 2"], + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: OptionsField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + options: transformSelectOptions(bookingField.options), + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform multiselect field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "multiselect", + slug: "multiselect", + label: "Your multiple selections", + required: true, + options: ["Option 1", "Option 2"], + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: OptionsField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: "", + options: transformSelectOptions(bookingField.options), + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform multiemail field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "multiemail", + slug: "multiemail", + label: "Your multiple emails", + required: true, + placeholder: "example@example.com", + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: bookingField.placeholder, + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform checkbox field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "checkbox", + slug: "checkbox", + label: "Your checkboxes", + required: true, + options: ["Checkbox 1", "Checkbox 2"], + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: OptionsField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: "", + options: transformSelectOptions(bookingField.options), + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform radio field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "radio", + slug: "radio", + label: "Your radio buttons", + required: true, + options: ["Radio 1", "Radio 2"], + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: OptionsField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: "", + options: transformSelectOptions(bookingField.options), + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); + + it("should transform boolean field", () => { + const bookingField: BookingField_2024_06_14 = { + type: "boolean", + slug: "boolean", + label: "Agree to terms?", + required: true, + }; + + const input: BookingField_2024_06_14[] = [bookingField]; + + const expectedOutput: CommonField[] = [ + { + name: bookingField.slug, + type: bookingField.type, + label: bookingField.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: bookingField.required, + placeholder: "", + }, + ]; + + const result = transformApiEventTypeBookingFields(input); + + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/packages/lib/event-types/transformers/api-request.ts b/packages/lib/event-types/transformers/api-request.ts new file mode 100644 index 0000000000..85994dcaed --- /dev/null +++ b/packages/lib/event-types/transformers/api-request.ts @@ -0,0 +1,204 @@ +import { z } from "zod"; + +import type { CreateEventTypeInput_2024_06_14, Integration_2024_06_14 } from "@calcom/platform-types"; + +const integrationsMapping: Record = { + "cal-video": "integrations:daily", +}; + +function transformApiEventTypeLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) { + if (!inputLocations) { + return []; + } + + return inputLocations.map((location) => { + const { type } = location; + switch (type) { + case "address": + return { type: "inPerson", address: location.address, displayLocationPublicly: location.public }; + case "link": + return { type: "link", link: location.link, displayLocationPublicly: location.public }; + case "integration": + const integrationLabel = integrationsMapping[location.integration]; + return { type: integrationLabel }; + case "phone": + return { + type: "userPhone", + hostPhoneNumber: location.phone, + displayLocationPublicly: location.public, + }; + default: + throw new Error(`Unsupported location type '${type}'`); + } + }); +} + +const integrationsMappingSchema = { + "cal-video": z.literal("integrations:daily"), +}; + +const InPersonSchema = z.object({ + type: z.literal("inPerson"), + address: z.string(), + displayLocationPublicly: z.boolean().default(false), +}); + +const LinkSchema = z.object({ + type: z.literal("link"), + link: z.string().url(), + displayLocationPublicly: z.boolean().default(false), +}); + +const IntegrationSchema = z.object({ + type: z.union([integrationsMappingSchema["cal-video"], integrationsMappingSchema["cal-video"]]), +}); + +const UserPhoneSchema = z.object({ + type: z.literal("userPhone"), + hostPhoneNumber: z.string(), + displayLocationPublicly: z.boolean().default(false), +}); + +const TransformedLocationSchema = z.union([InPersonSchema, LinkSchema, IntegrationSchema, UserPhoneSchema]); +export const TransformedLocationsSchema = z.array(TransformedLocationSchema); + +function transformApiEventTypeBookingFields( + inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"] +) { + if (!inputBookingFields) { + return []; + } + + const customBookingFields = inputBookingFields.map((field) => { + const commonFields: CommonField = { + name: field.slug, + type: field.type, + label: field.label, + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: field.required, + placeholder: "placeholder" in field && field.placeholder ? field.placeholder : "", + }; + + const options = "options" in field && field.options ? transformSelectOptions(field.options) : undefined; + + if (!options) { + return commonFields; + } + + return { + ...commonFields, + options, + }; + }); + + return customBookingFields; +} + +export function transformSelectOptions(options: string[]) { + return options.map((option) => ({ + label: option, + value: option, + })); +} + +const FieldTypeEnum = z.enum([ + "number", + "boolean", + "address", + "name", + "text", + "textarea", + "email", + "phone", + "multiemail", + "select", + "multiselect", + "checkbox", + "radio", + "radioInput", +]); + +const CommonFieldsSchema = z.object({ + name: z.string(), + type: FieldTypeEnum, + label: z.string(), + sources: z.array( + z.object({ + id: z.literal("user"), + type: z.literal("user"), + label: z.literal("User"), + fieldRequired: z.literal(true), + }) + ), + editable: z.literal("user"), + required: z.boolean(), + placeholder: z.string().optional(), +}); + +const SystemFieldsSchema = z.object({ + name: z.string(), + type: FieldTypeEnum, + defaultLabel: z.string(), + labe: z.string().optional(), + editable: z.enum(["system-but-optional", "system"]), + sources: z.array( + z.object({ + id: z.literal("default"), + type: z.literal("default"), + label: z.literal("Default"), + }) + ), + views: z + .array( + z.object({ + id: z.enum(["reschedule"]), + label: z.string(), + }) + ) + .optional(), + defaultPlaceholder: z.enum(["", "share_additional_notes", "email", "reschedule_placeholder"]).optional(), + hidden: z.boolean().optional(), + required: z.boolean(), + hideWhenJustOneOption: z.boolean().optional(), + getOptionsAt: z.enum(["locations"]).optional(), + optionsInputs: z + .object({ + attendeeInPerson: z.object({ + type: z.literal("address"), + required: z.boolean(), + placeholder: z.string(), + }), + phone: z.object({ + type: z.literal("phone"), + required: z.boolean(), + placeholder: z.string(), + }), + }) + .optional(), +}); + +export type SystemField = z.infer; + +export type CommonField = z.infer; + +const OptionSchema = z.object({ + label: z.string(), + value: z.string(), +}); + +const BookingFieldSchema = CommonFieldsSchema.extend({ + options: z.array(OptionSchema).optional(), +}); +export type OptionsField = z.infer; + +export const BookingFieldsSchema = z.array(BookingFieldSchema); + +export { transformApiEventTypeLocations, transformApiEventTypeBookingFields }; diff --git a/packages/lib/event-types/transformers/api-response.spec.ts b/packages/lib/event-types/transformers/api-response.spec.ts new file mode 100644 index 0000000000..ddb304b0cc --- /dev/null +++ b/packages/lib/event-types/transformers/api-response.spec.ts @@ -0,0 +1,568 @@ +import { describe, expect, it } from "vitest"; + +import type { + AddressLocation_2024_06_14, + LinkLocation_2024_06_14, + PhoneLocation_2024_06_14, + IntegrationLocation_2024_06_14, +} from "@calcom/platform-types"; + +import type { CommonField, OptionsField } from "./api-request"; +import { getResponseEventTypeLocations, getResponseEventTypeBookingFields } from "./api-response"; + +describe("getResponseEventTypeLocations", () => { + it("should reverse transform address location", () => { + const transformedLocation = [ + { + type: "inPerson", + address: "1234 Main St", + displayLocationPublicly: true, + }, + ]; + + const expectedOutput: AddressLocation_2024_06_14[] = [ + { + type: "address", + address: "1234 Main St", + public: true, + }, + ]; + + const result = getResponseEventTypeLocations(transformedLocation); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform link location", () => { + const transformedLocation = [ + { + type: "link", + link: "https://example.com", + displayLocationPublicly: true, + }, + ]; + + const expectedOutput: LinkLocation_2024_06_14[] = [ + { + type: "link", + link: "https://example.com", + public: true, + }, + ]; + + const result = getResponseEventTypeLocations(transformedLocation); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform phone location", () => { + const transformedLocation = [ + { + type: "userPhone", + hostPhoneNumber: "123456789", + displayLocationPublicly: true, + }, + ]; + + const expectedOutput: PhoneLocation_2024_06_14[] = [ + { + type: "phone", + phone: "123456789", + public: true, + }, + ]; + + const result = getResponseEventTypeLocations(transformedLocation); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform integration location", () => { + const transformedLocation = [ + { + type: "integrations:daily", + }, + ]; + + const expectedOutput: IntegrationLocation_2024_06_14[] = [ + { + type: "integration", + integration: "cal-video", + }, + ]; + + const result = getResponseEventTypeLocations(transformedLocation); + + expect(result).toEqual(expectedOutput); + }); +}); + +describe("getResponseEventTypeBookingFields", () => { + it("should reverse transform name field", () => { + const transformedField: CommonField[] = [ + { + name: "your-name", + type: "name", + label: "Your name", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "alice", + }, + ]; + + const expectedOutput = [ + { + type: "name", + slug: "your-name", + label: "Your name", + required: true, + placeholder: "alice", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform email field", () => { + const transformedField: CommonField[] = [ + { + name: "your-email", + type: "email", + label: "Your email", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "example@example.com", + }, + ]; + + const expectedOutput = [ + { + type: "email", + slug: "your-email", + label: "Your email", + required: true, + placeholder: "example@example.com", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform phone field", () => { + const transformedField: CommonField[] = [ + { + name: "your-phone", + type: "phone", + label: "Your phone number", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "123456789", + }, + ]; + + const expectedOutput = [ + { + type: "phone", + slug: "your-phone", + label: "Your phone number", + required: true, + placeholder: "123456789", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform address field", () => { + const transformedField: CommonField[] = [ + { + name: "your-address", + type: "address", + label: "Your address", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "1234 Main St", + }, + ]; + + const expectedOutput = [ + { + type: "address", + slug: "your-address", + label: "Your address", + required: true, + placeholder: "1234 Main St", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform text field", () => { + const transformedField: CommonField[] = [ + { + name: "your-text", + type: "text", + label: "Your text", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "Enter your text", + }, + ]; + + const expectedOutput = [ + { + type: "text", + slug: "your-text", + label: "Your text", + required: true, + placeholder: "Enter your text", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform number field", () => { + const transformedField: CommonField[] = [ + { + name: "your-number", + type: "number", + label: "Your number", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "100", + }, + ]; + + const expectedOutput = [ + { + type: "number", + slug: "your-number", + label: "Your number", + required: true, + placeholder: "100", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform textarea field", () => { + const transformedField: CommonField[] = [ + { + name: "your-textarea", + type: "textarea", + label: "Your detailed information", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "Detailed description here...", + }, + ]; + + const expectedOutput = [ + { + type: "textarea", + slug: "your-textarea", + label: "Your detailed information", + required: true, + placeholder: "Detailed description here...", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform select field", () => { + const transformedField: OptionsField[] = [ + { + name: "your-select", + type: "select", + label: "Your selection", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "Select...", + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + ], + }, + ]; + + const expectedOutput = [ + { + type: "select", + slug: "your-select", + label: "Your selection", + required: true, + placeholder: "Select...", + options: ["Option 1", "Option 2"], + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform multiselect field", () => { + const transformedField: OptionsField[] = [ + { + name: "your-multiselect", + type: "multiselect", + label: "Your multiple selections", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + ], + }, + ]; + + const expectedOutput = [ + { + type: "multiselect", + slug: "your-multiselect", + label: "Your multiple selections", + required: true, + options: ["Option 1", "Option 2"], + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform multiemail field", () => { + const transformedField: CommonField[] = [ + { + name: "your-multiemail", + type: "multiemail", + label: "Your multiple emails", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "example@example.com", + }, + ]; + + const expectedOutput = [ + { + type: "multiemail", + slug: "your-multiemail", + label: "Your multiple emails", + required: true, + placeholder: "example@example.com", + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform checkbox field", () => { + const transformedField: OptionsField[] = [ + { + name: "your-checkbox", + type: "checkbox", + label: "Your checkboxes", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + options: [ + { label: "Checkbox 1", value: "Checkbox 1" }, + { label: "Checkbox 2", value: "Checkbox 2" }, + ], + }, + ]; + + const expectedOutput = [ + { + type: "checkbox", + slug: "your-checkbox", + label: "Your checkboxes", + required: true, + options: ["Checkbox 1", "Checkbox 2"], + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform radio field", () => { + const transformedField: OptionsField[] = [ + { + name: "your-radio", + type: "radio", + label: "Your radio buttons", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + options: [ + { label: "Radio 1", value: "Radio 1" }, + { label: "Radio 2", value: "Radio 2" }, + ], + }, + ]; + + const expectedOutput = [ + { + type: "radio", + slug: "your-radio", + label: "Your radio buttons", + required: true, + options: ["Radio 1", "Radio 2"], + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); + + it("should reverse transform boolean field", () => { + const transformedField: CommonField[] = [ + { + name: "agree-to-terms", + type: "boolean", + label: "Agree to terms?", + sources: [ + { + id: "user", + type: "user", + label: "User", + fieldRequired: true, + }, + ], + editable: "user", + required: true, + placeholder: "", + }, + ]; + + const expectedOutput = [ + { + type: "boolean", + slug: "agree-to-terms", + label: "Agree to terms?", + required: true, + }, + ]; + + const result = getResponseEventTypeBookingFields(transformedField); + + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/packages/lib/event-types/transformers/api-response.ts b/packages/lib/event-types/transformers/api-response.ts new file mode 100644 index 0000000000..3ce3ace4e8 --- /dev/null +++ b/packages/lib/event-types/transformers/api-response.ts @@ -0,0 +1,191 @@ +import type { + AddressLocation_2024_06_14, + IntegrationLocation_2024_06_14, + LinkLocation_2024_06_14, + PhoneLocation_2024_06_14, + Integration_2024_06_14, +} from "@calcom/platform-types"; + +import type { transformApiEventTypeBookingFields, transformApiEventTypeLocations } from "./api-request"; + +const reverseIntegrationsMapping: Record = { + "integrations:daily": "cal-video", +}; + +function getResponseEventTypeLocations( + transformedLocations: ReturnType +) { + if (!transformedLocations) { + return []; + } + + return transformedLocations.map((location) => { + switch (location.type) { + case "inPerson": { + if (!location.address) { + throw new Error("Address location must have an address"); + } + const addressLocation: AddressLocation_2024_06_14 = { + type: "address", + address: location.address, + public: location.displayLocationPublicly, + }; + return addressLocation; + } + case "link": { + if (!location.link) { + throw new Error("Link location must have a link"); + } + const linkLocation: LinkLocation_2024_06_14 = { + type: "link", + link: location.link, + public: location.displayLocationPublicly, + }; + return linkLocation; + } + case "userPhone": { + if (!location.hostPhoneNumber) { + throw new Error("Phone location must have a phone number"); + } + const phoneLocation: PhoneLocation_2024_06_14 = { + type: "phone", + phone: location.hostPhoneNumber, + public: location.displayLocationPublicly, + }; + return phoneLocation; + } + default: { + const integrationType = reverseIntegrationsMapping[location.type]; + if (!integrationType) { + throw new Error(`Unsupported integration type '${location.type}'.`); + } + const integration: IntegrationLocation_2024_06_14 = { + type: "integration", + integration: integrationType, + }; + return integration; + } + } + }); +} + +function getResponseEventTypeBookingFields( + transformedBookingFields: ReturnType +) { + if (!transformedBookingFields) { + return []; + } + + return transformedBookingFields.map((field) => { + switch (field.type) { + case "name": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "email": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "phone": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "address": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "text": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "number": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "textarea": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "multiemail": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + }; + case "boolean": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + }; + case "select": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + placeholder: field.placeholder, + options: "options" in field ? field.options?.map((option) => option.value) : [], + }; + case "multiselect": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + options: "options" in field ? field.options?.map((option) => option.value) : [], + }; + case "checkbox": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + options: "options" in field ? field.options?.map((option) => option.value) : [], + }; + case "radio": + return { + type: field.type, + slug: field.name, + label: field.label, + required: field.required, + options: "options" in field ? field.options?.map((option) => option.value) : [], + }; + default: + throw new Error(`Unsupported booking field type '${field.type}'.`); + } + }); +} + +export { getResponseEventTypeLocations, getResponseEventTypeBookingFields }; diff --git a/packages/lib/event-types/transformers/index.ts b/packages/lib/event-types/transformers/index.ts new file mode 100644 index 0000000000..4c7116be33 --- /dev/null +++ b/packages/lib/event-types/transformers/index.ts @@ -0,0 +1,2 @@ +export * from "./api-request"; +export * from "./api-response"; diff --git a/packages/lib/getPaymentAppData.ts b/packages/lib/getPaymentAppData.ts index 08e32182e0..0064321e7f 100644 --- a/packages/lib/getPaymentAppData.ts +++ b/packages/lib/getPaymentAppData.ts @@ -4,9 +4,10 @@ import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppDat import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; import type { appDataSchema, paymentOptionEnum } from "@calcom/app-store/stripepayment/zod"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; +import type { BookerEvent } from "@calcom/features/bookings/types"; export default function getPaymentAppData( - eventType: Parameters[0], + eventType: Pick, forcedGet?: boolean ) { const metadataApps = eventType?.metadata?.apps as unknown as EventTypeAppsList; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 6943b61262..5d28b67b54 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -5,3 +5,4 @@ export * from "./isBookingLimits"; export * from "./isDurationLimits"; export * from "./validateIntervalLimitOrder"; export * from "./schedules"; +export * from "./event-types"; diff --git a/packages/lib/jest.config.json b/packages/lib/jest.config.json new file mode 100644 index 0000000000..53b778bff5 --- /dev/null +++ b/packages/lib/jest.config.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.ts$": "ts-jest" + } +} diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index bf774d68a6..feb9a9e2c1 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -22,6 +22,8 @@ import type { } from "@calcom/platform-types"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; +import { transformApiEventTypeForAtom } from "../event-types/atom-api-transformers/transformApiEventTypeForAtom"; +import { useEventType } from "../hooks/event-types/public/useEventType"; import { useAtomsContext } from "../hooks/useAtomsContext"; import { useAvailableSlots } from "../hooks/useAvailableSlots"; import { useCalendarsBusyTimes } from "../hooks/useCalendarsBusyTimes"; @@ -36,7 +38,6 @@ import { } from "../hooks/useGetBookingForReschedule"; import { useHandleBookEvent } from "../hooks/useHandleBookEvent"; import { useMe } from "../hooks/useMe"; -import { usePublicEvent } from "../hooks/usePublicEvent"; import { useSlots } from "../hooks/useSlots"; import { AtomsWrapper } from "../src/components/atoms-wrapper"; @@ -109,6 +110,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => setBookingData(null); } }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); setSelectedDuration(props.duration ?? null); @@ -118,11 +120,22 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => return getUsernameList(username ?? "").length > 1; }, [username]); - const event = usePublicEvent({ - username, - eventSlug: props.eventSlug, - isDynamic, - }); + const { isSuccess, isError, isPending, data } = useEventType(username, props.eventSlug); + + const event = useMemo(() => { + return { + isSuccess, + isError, + isPending, + data: data && data.length > 0 ? transformApiEventTypeForAtom(data[0], props.entity) : undefined, + }; + }, [isSuccess, isError, isPending, data, props.entity]); + + if (isDynamic && props.duration && event.data) { + // note(Lauris): Mandatory - In case of "dynamic" event type default event duration returned by the API is 30, + // but we are re-using the dynamic event type as a team event, so we must set the event length to whatever the event length is. + event.data.length = props.duration; + } const bookerLayout = useBookerLayout(event.data); useInitializeBookerStore({ @@ -322,7 +335,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => onClickOverlayContinue={function (): void { throw new Error("Function not implemented."); }} - onOverlaySwitchStateChange={function (state: boolean): void { + onOverlaySwitchStateChange={function (): void { throw new Error("Function not implemented."); }} extraOptions={extraOptions ?? {}} diff --git a/packages/platform/atoms/cal-provider/CalProvider.tsx b/packages/platform/atoms/cal-provider/CalProvider.tsx index 760e64d630..6ec7e7b887 100644 --- a/packages/platform/atoms/cal-provider/CalProvider.tsx +++ b/packages/platform/atoms/cal-provider/CalProvider.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, type ReactNode } from "react"; import type { API_VERSIONS_ENUM } from "@calcom/platform-constants"; -import { VERSION_2024_06_11 } from "@calcom/platform-constants"; +import { VERSION_2024_06_14 } from "@calcom/platform-constants"; import http from "../lib/http"; import { BaseCalProvider } from "./BaseCalProvider"; @@ -40,7 +40,7 @@ export function CalProvider({ children, autoUpdateTimezone = true, onTimezoneChange, - version = VERSION_2024_06_11, + version = VERSION_2024_06_14, }: CalProviderProps) { useEffect(() => { http.setVersionHeader(version); diff --git a/packages/platform/atoms/event-types/atom-api-transformers/transformApiEventTypeForAtom.ts b/packages/platform/atoms/event-types/atom-api-transformers/transformApiEventTypeForAtom.ts new file mode 100644 index 0000000000..bd71610c5f --- /dev/null +++ b/packages/platform/atoms/event-types/atom-api-transformers/transformApiEventTypeForAtom.ts @@ -0,0 +1,257 @@ +import type { BookerProps } from "@calcom/features/bookings/Booker"; +import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier"; +import { defaultEvents } from "@calcom/lib/defaultEvents"; +import type { CommonField, OptionsField, SystemField } from "@calcom/lib/event-types/transformers"; +import { + transformApiEventTypeLocations, + transformApiEventTypeBookingFields, +} from "@calcom/lib/event-types/transformers"; +import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; +import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import { + bookerLayoutOptions, + BookerLayouts, + bookerLayouts as bookerLayoutsSchema, + userMetadata as userMetadataSchema, + eventTypeBookingFields, +} from "@calcom/prisma/zod-utils"; + +export function transformApiEventTypeForAtom( + eventType: Omit, + entity: BookerProps["entity"] | undefined +) { + const { lengthInMinutes, locations, bookingFields, users, ...rest } = eventType; + + const isDefault = isDefaultEvent(rest.title); + const user = users[0]; + + const defaultEventBookerLayouts = { + enabledLayouts: [...bookerLayoutOptions], + defaultLayout: BookerLayouts.MONTH_VIEW, + }; + const firstUsersMetadata = userMetadataSchema.parse(user.metadata || {}); + const bookerLayouts = bookerLayoutsSchema.parse( + firstUsersMetadata?.defaultBookerLayouts || defaultEventBookerLayouts + ); + + return { + ...rest, + length: lengthInMinutes, + locations: getLocations(locations), + bookingFields: getBookingFields(bookingFields), + isDefault, + isDynamic: false, + profile: { + username: user.username, + name: user.name, + weekStart: user.weekStart, + image: "", + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + theme: null, + bookerLayouts, + }, + entity: entity + ? { + ...entity, + orgSlug: entity.orgSlug || null, + teamSlug: entity.teamSlug || null, + fromRedirectOfNonOrgLink: true, + name: entity.name || null, + logoUrl: entity.logoUrl || undefined, + } + : { + fromRedirectOfNonOrgLink: true, + considerUnpublished: false, + orgSlug: null, + teamSlug: null, + name: null, + logoUrl: undefined, + }, + hosts: [], + users: users.map((user) => ({ + ...user, + metadata: undefined, + bookerUrl: getBookerBaseUrlSync(null), + profile: { + username: user.username || "", + name: user.name, + weekStart: user.weekStart, + image: "", + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + theme: null, + organization: null, + id: user.id, + organizationId: null, + userId: user.id, + upId: `usr-${user.id}`, + }, + })), + }; +} + +function isDefaultEvent(eventSlug: string) { + const foundInDefaults = defaultEvents.find((obj) => { + return obj.slug === eventSlug; + }); + return !!foundInDefaults; +} + +function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) { + const transformed = transformApiEventTypeLocations(locations); + + const withPrivateHidden = transformed.map((location) => { + const { displayLocationPublicly, type } = location; + + switch (type) { + case "address": + return displayLocationPublicly ? location : { ...location, address: "" }; + case "link": + return displayLocationPublicly ? location : { ...location, link: "" }; + case "phone": + return displayLocationPublicly + ? location + : { + ...location, + hostPhoneNumber: "", + }; + default: + return location; + } + }); + + return withPrivateHidden; +} + +function getBookingFields(bookingFields: EventTypeOutput_2024_06_14["bookingFields"]) { + const transformedBookingFields: (CommonField | SystemField | OptionsField)[] = + transformApiEventTypeBookingFields(bookingFields); + + // These fields should be added before other user fields + const systemBeforeFields: SystemField[] = [ + { + type: "name", + // This is the `name` of the main field + name: "name", + editable: "system", + // This Label is used in Email only as of now. + defaultLabel: "your_name", + required: true, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "email_address", + type: "email", + name: "email", + required: true, + editable: "system", + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "location", + type: "radioInput", + name: "location", + editable: "system", + hideWhenJustOneOption: true, + required: false, + getOptionsAt: "locations", + optionsInputs: { + attendeeInPerson: { + type: "address", + required: true, + placeholder: "", + }, + phone: { + type: "phone", + required: true, + placeholder: "", + }, + }, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + ]; + + // These fields should be added after other user fields + const systemAfterFields: SystemField[] = [ + { + defaultLabel: "reason_for_reschedule", + type: "textarea", + editable: "system-but-optional", + name: "rescheduleReason", + defaultPlaceholder: "reschedule_placeholder", + required: false, + views: [ + { + id: "reschedule", + label: "Reschedule View", + }, + ], + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + ]; + + const missingSystemBeforeFields: SystemField[] = []; + + for (const field of systemBeforeFields) { + const existingBookingFieldIndex = transformedBookingFields.findIndex( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name) + ); + // Only do a push, we must not update existing system fields as user could have modified any property in it, + if (existingBookingFieldIndex === -1) { + missingSystemBeforeFields.push(field); + } else { + // Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field + transformedBookingFields[existingBookingFieldIndex] = { + ...field, + ...transformedBookingFields[existingBookingFieldIndex], + }; + } + } + + transformedBookingFields.push(...missingSystemBeforeFields); + + const missingSystemAfterFields: SystemField[] = []; + for (const field of systemAfterFields) { + const existingBookingFieldIndex = transformedBookingFields.findIndex( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name) + ); + // Only do a push, we must not update existing system fields as user could have modified any property in it, + if (existingBookingFieldIndex === -1) { + missingSystemAfterFields.push(field); + } else { + transformedBookingFields[existingBookingFieldIndex] = { + // Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field + ...field, + ...transformedBookingFields[existingBookingFieldIndex], + }; + } + } + + transformedBookingFields.push(...missingSystemAfterFields); + return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(transformedBookingFields); +} diff --git a/packages/platform/atoms/hooks/event-types/private/useEventTypeById.ts b/packages/platform/atoms/hooks/event-types/private/useEventTypeById.ts new file mode 100644 index 0000000000..53f7b2ffd6 --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/private/useEventTypeById.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; + +import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; + +import http from "../../../lib/http"; + +export const QUERY_KEY = "use-event-by-id"; +export const useEventTypeById = (id: number | null) => { + const pathname = `/${V2_ENDPOINTS.eventTypes}/${id}`; + + return useQuery({ + queryKey: [QUERY_KEY, id], + queryFn: () => { + return http?.get>(pathname).then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/event-types/public/useEventType.ts b/packages/platform/atoms/hooks/event-types/public/useEventType.ts new file mode 100644 index 0000000000..fe4dcda1db --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/public/useEventType.ts @@ -0,0 +1,59 @@ +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { shallow } from "zustand/shallow"; + +import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; +import { SUCCESS_STATUS, V2_ENDPOINTS } from "@calcom/platform-constants"; +import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import type { ApiResponse } from "@calcom/platform-types"; + +import http from "../../../lib/http"; + +export const QUERY_KEY = "use-event-type"; +export type UsePublicEventReturnType = ReturnType; + +export const useEventType = (username: string, eventSlug: string) => { + const [stateUsername, stateEventSlug] = useBookerStore( + (state) => [state.username, state.eventSlug], + shallow + ); + + const requestUsername = stateUsername ?? username; + const requestEventSlug = stateEventSlug ?? eventSlug; + + const isDynamic = useMemo(() => { + return getUsernameList(requestUsername ?? "").length > 1; + }, [requestUsername]); + + const event = useQuery({ + queryKey: [QUERY_KEY, stateUsername ?? username, stateEventSlug ?? eventSlug], + queryFn: async () => { + if (isDynamic) { + return http + .get>( + `/${V2_ENDPOINTS.eventTypes}?usernames=${encodeURIComponent(getUsernameList(username).join(","))}` + ) + .then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return res.data.data; + } + throw new Error(res.data.error.message); + }); + } + + return http + .get>( + `/${V2_ENDPOINTS.eventTypes}?username=${requestUsername}&eventSlug=${requestEventSlug}` + ) + .then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return res.data.data; + } + throw new Error(res.data.error.message); + }); + }, + }); + + return event; +}; diff --git a/packages/platform/atoms/hooks/event-types/public/useEventTypes.ts b/packages/platform/atoms/hooks/event-types/public/useEventTypes.ts new file mode 100644 index 0000000000..ff5c434109 --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/public/useEventTypes.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; + +import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; + +import http from "../../../lib/http"; + +export const QUERY_KEY = "use-event-types"; +export const useEventTypes = (username: string) => { + const pathname = `/${V2_ENDPOINTS.eventTypes}?username=${username}`; + + return useQuery({ + queryKey: [QUERY_KEY, username], + queryFn: () => { + return http?.get>(pathname).then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts b/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts deleted file mode 100644 index 56b0601c93..0000000000 --- a/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { EventTypesByViewer } from "@calcom/platform-libraries"; -import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; - -import http from "../../lib/http"; - -export const QUERY_KEY = "get-private-events"; -export const useEventTypesPrivate = () => { - const pathname = `/${V2_ENDPOINTS.eventTypes}`; - - return useQuery({ - queryKey: [QUERY_KEY], - queryFn: () => { - return http?.get>(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return (res.data as ApiSuccessResponse).data; - } - throw new Error(res.data.error.message); - }); - }, - }); -}; diff --git a/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts b/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts deleted file mode 100644 index a67f76b6ec..0000000000 --- a/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { EventTypesPublic } from "@calcom/platform-libraries"; -import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; - -import http from "../../lib/http"; - -export const QUERY_KEY = "get-public-events"; -export const useEventTypesPublic = (username: string) => { - const pathname = `/${V2_ENDPOINTS.eventTypes}/${username}/public`; - - return useQuery({ - queryKey: [QUERY_KEY, username], - queryFn: () => { - return http?.get>(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return (res.data as ApiSuccessResponse).data; - } - throw new Error(res.data.error.message); - }); - }, - }); -}; diff --git a/packages/platform/atoms/hooks/event-types/useGetEventTypeById.ts b/packages/platform/atoms/hooks/event-types/useGetEventTypeById.ts deleted file mode 100644 index 9d7bb19139..0000000000 --- a/packages/platform/atoms/hooks/event-types/useGetEventTypeById.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { EventType as AtomEventType } from "@calcom/platform-libraries"; -import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; - -import http from "../../lib/http"; - -export const QUERY_KEY = "get-event-by-id"; -export const useGetEventTypeById = (id: number | null) => { - const pathname = `/${V2_ENDPOINTS.eventTypes}/${id}?for=atom`; - - return useQuery({ - queryKey: [QUERY_KEY, id], - queryFn: () => { - return http?.get>(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return (res.data as ApiSuccessResponse).data; - } - throw new Error(res.data.error.message); - }); - }, - }); -}; diff --git a/packages/platform/atoms/hooks/useHandleBookEvent.ts b/packages/platform/atoms/hooks/useHandleBookEvent.ts index 164e2cbe6c..fd6952548e 100644 --- a/packages/platform/atoms/hooks/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/useHandleBookEvent.ts @@ -1,11 +1,11 @@ import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; import { useTimePreferences, mapBookingToMutationInput, mapRecurringBookingToMutationInput, } from "@calcom/features/bookings/lib"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; @@ -13,7 +13,12 @@ import type { UseCreateBookingInput } from "./useCreateBooking"; type UseHandleBookingProps = { bookingForm: UseBookingFormReturnType["bookingForm"]; - event: useEventReturnType; + event?: { + data?: Pick< + BookerEvent, + "id" | "isDynamic" | "metadata" | "recurringEvent" | "length" | "slug" | "schedulingType" + > | null; + }; metadata: Record; hashedLink?: string | null; teamMemberEmail?: string; diff --git a/packages/platform/atoms/hooks/useSlots.ts b/packages/platform/atoms/hooks/useSlots.ts index 0a1104d86c..53e70a703e 100644 --- a/packages/platform/atoms/hooks/useSlots.ts +++ b/packages/platform/atoms/hooks/useSlots.ts @@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow"; import dayjs from "@calcom/dayjs"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId"; -import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; +import type { BookerEvent } from "@calcom/features/bookings/types"; import { MINUTES_TO_BOOK } from "@calcom/lib/constants"; import { useDeleteSelectedSlot } from "./useDeleteSelectedSlot"; @@ -12,7 +12,7 @@ import { useReserveSlot } from "./useReserveSlot"; export type UseSlotsReturnType = ReturnType; -export const useSlots = (event: useEventReturnType) => { +export const useSlots = (event: { data?: Pick | null }) => { const selectedDuration = useBookerStore((state) => state.selectedDuration); const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( (state) => [state.selectedTimeslot, state.setSelectedTimeslot], diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index 502377b231..804132c2cc 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -5,9 +5,9 @@ export { BookerPlatformWrapper as Booker } from "./booker"; export { useIsPlatform } from "./hooks/useIsPlatform"; export { useAtomsContext } from "./hooks/useAtomsContext"; export { useConnectedCalendars } from "./hooks/useConnectedCalendars"; -export { useEventTypesPublic } from "./hooks/event-types/useEventTypesPublic"; -export { usePublicEvent } from "./hooks/usePublicEvent"; -export { useGetEventTypeById } from "./hooks/event-types/useGetEventTypeById"; +export { useEventTypes } from "./hooks/event-types/public/useEventTypes"; +export { useEventType as useEvent } from "./hooks/event-types/public/useEventType"; +export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById"; export { useCancelBooking } from "./hooks/useCancelBooking"; export { useGetBooking } from "./hooks/useGetBooking"; export { useGetBookings } from "./hooks/useGetBookings"; diff --git a/packages/platform/atoms/monorepo.ts b/packages/platform/atoms/monorepo.ts index 39decbda05..3a285354ae 100644 --- a/packages/platform/atoms/monorepo.ts +++ b/packages/platform/atoms/monorepo.ts @@ -3,7 +3,7 @@ export { AvailabilitySettingsWebWrapper as AvailabilitySettings } from "./availa export { CalProvider } from "./cal-provider/CalProvider"; export { useIsPlatform } from "./hooks/useIsPlatform"; export { useAtomsContext } from "./hooks/useAtomsContext"; -export { useGetEventTypeById } from "./hooks/event-types/useGetEventTypeById"; +export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById"; export { useHandleBookEvent } from "./hooks/useHandleBookEvent"; export * as Dialog from "./src/components/ui/dialog"; export { Timezone } from "./timezone"; diff --git a/packages/platform/constants/api.ts b/packages/platform/constants/api.ts index adbf719ef6..4a3e2b9c36 100644 --- a/packages/platform/constants/api.ts +++ b/packages/platform/constants/api.ts @@ -51,9 +51,12 @@ export const X_CAL_CLIENT_ID = "x-cal-client-id"; // HTTP status codes export const HTTP_CODE_TOKEN_EXPIRED = 498; +export const VERSION_2024_06_14 = "2024-06-14"; export const VERSION_2024_06_11 = "2024-06-11"; export const VERSION_2024_04_15 = "2024-04-15"; -export const API_VERSIONS = [VERSION_2024_06_11, VERSION_2024_04_15] as const; + +export const API_VERSIONS = [VERSION_2024_06_14, VERSION_2024_06_11, VERSION_2024_04_15] as const; + export type API_VERSIONS_ENUM = (typeof API_VERSIONS)[number]; export type API_VERSIONS_TYPE = typeof API_VERSIONS; export const CAL_API_VERSION_HEADER = "cal-api-version"; diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx index 049e8a136b..2ba49a5602 100644 --- a/packages/platform/examples/base/src/pages/booking.tsx +++ b/packages/platform/examples/base/src/pages/booking.tsx @@ -4,7 +4,7 @@ import { Inter } from "next/font/google"; import { useRouter } from "next/router"; import { useState } from "react"; -import { Booker, useEventTypesPublic } from "@calcom/atoms"; +import { Booker, useEventTypes } from "@calcom/atoms"; const inter = Inter({ subsets: ["latin"] }); @@ -13,7 +13,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string const [eventTypeSlug, setEventTypeSlug] = useState(null); const [eventTypeDuration, setEventTypeDuration] = useState(null); const router = useRouter(); - const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypesPublic(props.calUsername); + const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypes(props.calUsername); const rescheduleUid = (router.query.rescheduleUid as string) ?? ""; const eventTypeSlugQueryParam = (router.query.eventTypeSlug as string) ?? ""; @@ -28,26 +28,28 @@ export default function Bookings(props: { calUsername: string; calEmail: string {!isLoadingEvents && !eventTypeSlug && Boolean(eventTypes?.length) && !rescheduleUid && (
- {eventTypes?.map((event: { id: number; slug: string; title: string; length: number }) => { - const formatEventSlug = event.slug - .split("-") - .map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`) - .join(" "); + {eventTypes?.map( + (event: { id: number; slug: string; title: string; lengthInMinutes: number }) => { + const formatEventSlug = event.slug + .split("-") + .map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`) + .join(" "); - return ( -
{ - setEventTypeSlug(event.slug); - setEventTypeDuration(event.length); - }} - className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4" - key={event.id}> -

{formatEventSlug}

-

{`/${event.slug}`}

- {event?.length} -
- ); - })} + return ( +
{ + setEventTypeSlug(event.slug); + setEventTypeDuration(event.lengthInMinutes); + }} + className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4" + key={event.id}> +

{formatEventSlug}

+

{`/${event.slug}`}

+ {event?.lengthInMinutes} +
+ ); + } + )}
)} diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 36c73383ad..54a7d84437 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -1,3 +1,4 @@ +import { CalendarService } from "@calcom/app-store/applecalendar/lib"; import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking"; import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; @@ -5,6 +6,7 @@ import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooki import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent"; import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleInstantMeeting"; import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings"; +import { symmetricEncrypt } from "@calcom/lib/crypto"; import { updateHandler as updateScheduleHandler } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.handler"; import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; import { createNewUsersConnectToOrgIfExists } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils"; @@ -78,3 +80,20 @@ export { getBookingInfo }; export { handleCancelBooking }; export { eventTypeBookingFields, eventTypeLocations } from "@calcom/prisma/zod-utils"; + +export { EventTypeMetaDataSchema, userMetadata } from "@calcom/prisma/zod-utils"; + +export { + transformApiEventTypeBookingFields, + transformApiEventTypeLocations, + getResponseEventTypeLocations, + getResponseEventTypeBookingFields, + TransformedLocationsSchema, + BookingFieldsSchema, +} from "@calcom/lib/event-types/transformers"; + +export { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; +export { dynamicEvent } from "@calcom/lib/defaultEvents"; + +export { symmetricEncrypt }; +export { CalendarService }; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/index.ts b/packages/platform/types/event-types/event-types_2024_06_14/index.ts new file mode 100644 index 0000000000..775f90ba48 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/index.ts @@ -0,0 +1,2 @@ +export * from "./inputs"; +export * from "./outputs"; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts new file mode 100644 index 0000000000..2dbd1098fe --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts @@ -0,0 +1,416 @@ +import { BadRequestException } from "@nestjs/common"; +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; +import { plainToInstance } from "class-transformer"; +import { IsString, IsBoolean, IsArray, IsIn, IsOptional } from "class-validator"; +import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator"; +import { registerDecorator, validate, ValidatorConstraint } from "class-validator"; + +const bookingFields = [ + "name", + "email", + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean", +] as const; + +export class NameField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "name"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty() + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + placeholder?: string; +} +export class EmailField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "email"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty() + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class PhoneField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "phone"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty() + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class AddressField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "address"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please enter your address" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + @DocsProperty({ example: "e.g., 1234 Main St" }) + placeholder?: string; +} + +export class TextField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "text"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please enter your text" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @DocsProperty({ example: "e.g., Enter text here" }) + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class NumberField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "number"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please enter a number" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @DocsProperty({ example: "e.g., 100" }) + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class TextAreaField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "textarea"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please enter detailed information" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @DocsProperty({ example: "e.g., Detailed description here..." }) + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class SelectField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "select"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please select an option" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @DocsProperty({ example: "Select..." }) + @IsOptional() + @DocsProperty() + placeholder?: string; + + @IsArray() + @DocsProperty({ type: [String], example: ["Option 1", "Option 2"] }) + options!: string[]; +} + +export class MultiSelectField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "multiselect"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please select multiple options" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsArray() + @DocsProperty({ type: [String], example: ["Option 1", "Option 2"] }) + options!: string[]; +} + +export class MultiEmailField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "multiemail"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Please enter multiple emails" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @DocsProperty({ example: "e.g., example@example.com" }) + @IsOptional() + @DocsProperty() + placeholder?: string; +} + +export class CheckboxGroupField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "checkbox"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Select all that apply" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsArray() + @DocsProperty({ type: [String], example: ["Checkbox 1", "Checkbox 2"] }) + options!: string[]; +} + +export class RadioGroupField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "radio"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Select one option" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsArray() + @DocsProperty({ type: [String], example: ["Radio 1", "Radio 2"] }) + options!: string[]; +} + +export class BooleanField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "boolean"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty({ example: "Agree to terms?" }) + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; +} + +export type BookingField_2024_06_14 = + | NameField_2024_06_14 + | EmailField_2024_06_14 + | PhoneField_2024_06_14 + | AddressField_2024_06_14 + | TextField_2024_06_14 + | NumberField_2024_06_14 + | TextAreaField_2024_06_14 + | SelectField_2024_06_14 + | MultiSelectField_2024_06_14 + | MultiEmailField_2024_06_14 + | CheckboxGroupField_2024_06_14 + | RadioGroupField_2024_06_14 + | BooleanField_2024_06_14; + +@ValidatorConstraint({ async: true }) +class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface { + private classTypeMap: { [key: string]: new () => BookingField_2024_06_14 } = { + name: NameField_2024_06_14, + email: EmailField_2024_06_14, + phone: PhoneField_2024_06_14, + address: AddressField_2024_06_14, + text: TextField_2024_06_14, + number: NumberField_2024_06_14, + textarea: TextAreaField_2024_06_14, + select: SelectField_2024_06_14, + multiselect: MultiSelectField_2024_06_14, + multiemail: MultiEmailField_2024_06_14, + checkbox: CheckboxGroupField_2024_06_14, + radio: RadioGroupField_2024_06_14, + boolean: BooleanField_2024_06_14, + }; + + async validate(bookingFields: { type: string; slug: string }[]) { + if (!Array.isArray(bookingFields)) { + throw new BadRequestException(`'bookingFields' must be an array.`); + } + + if (!bookingFields.length) { + throw new BadRequestException(`'bookingFields' must contain at least 1 booking field.`); + } + + const slugs: string[] = []; + for (const field of bookingFields) { + const { type, slug } = field; + if (!type) { + throw new BadRequestException(`Each booking field must have a 'type' property.`); + } + + if (!slug) { + throw new BadRequestException(`Each booking field must have a 'slug' property.`); + } + + if (slugs.includes(slug)) { + throw new BadRequestException( + `Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.` + ); + } + slugs.push(slug); + + const ClassType = this.classTypeMap[type]; + if (!ClassType) { + throw new BadRequestException(`Unsupported booking field type '${type}'.`); + } + + const instance = plainToInstance(ClassType, field); + const errors = await validate(instance); + if (errors.length > 0) { + const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", "); + throw new BadRequestException(`Validation failed for ${type} booking field: ${message}`); + } + } + + return true; + } + + defaultMessage() { + return `Validation failed for one or more booking fields.`; + } +} + +export function ValidateBookingFields_2024_06_14(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (object: any, propertyName: string) { + registerDecorator({ + name: "ValidateBookingFields", + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: new BookingFieldValidator_2024_06_14(), + }); + }; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts new file mode 100644 index 0000000000..f456d9fb49 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts @@ -0,0 +1,59 @@ +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; +import { IsString, IsInt, IsBoolean, IsOptional, Min } from "class-validator"; + +import type { BookingField_2024_06_14 } from "./booking-fields.input"; +import { ValidateBookingFields_2024_06_14 } from "./booking-fields.input"; +import { ValidateLocations_2024_06_14 } from "./locations.input"; +import type { Location_2024_06_14 } from "./locations.input"; + +export const CREATE_EVENT_LENGTH_EXAMPLE = 60; +export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!"; +export const CREATE_EVENT_DESCRIPTION_EXAMPLE = + "Discover the culinary wonders of the Argentina by making the best flan ever!"; +export class CreateEventTypeInput_2024_06_14 { + @IsInt() + @Min(1) + @DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE }) + lengthInMinutes!: number; + + @IsString() + @DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE }) + title!: string; + + @IsString() + slug!: string; + + @IsOptional() + @IsString() + @DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE }) + description?: string; + + @IsOptional() + @ValidateLocations_2024_06_14() + locations?: Location_2024_06_14[]; + + @IsOptional() + @ValidateBookingFields_2024_06_14() + bookingFields?: BookingField_2024_06_14[]; + + @IsBoolean() + @IsOptional() + disableGuests?: boolean; + + @IsInt() + @IsOptional() + slotInterval?: number; + + @IsInt() + @Min(0) + @IsOptional() + minimumBookingNotice?: number; + + @IsInt() + @IsOptional() + beforeEventBuffer?: number; + + @IsInt() + @IsOptional() + afterEventBuffer?: number; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/editable.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/editable.ts new file mode 100644 index 0000000000..819f010f12 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/editable.ts @@ -0,0 +1,7 @@ +export enum Editable { + system = "system", + systemButOptional = "system-but-optional", + systemButHidden = "system-but-hidden", + user = "user", + userReadonly = "user-readonly", +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/field-type.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/field-type.ts new file mode 100644 index 0000000000..79eb2ee988 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/field-type.ts @@ -0,0 +1,18 @@ +export enum BaseFieldEnum { + name = "name", + number = "number", + boolean = "boolean", + address = "address", + text = "text", + textarea = "textarea", + email = "email", + phone = "phone", + multiemail = "multiemail", + select = "select", + multiselect = "multiselect", + checkbox = "checkbox", + radio = "radio", + radioInput = "radioInput", +} + +export type BaseFieldType = keyof typeof BaseFieldEnum; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/frequency.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/frequency.ts new file mode 100644 index 0000000000..830bb7abfc --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/frequency.ts @@ -0,0 +1,9 @@ +export enum Frequency { + YEARLY = 0, + MONTHLY = 1, + WEEKLY = 2, + DAILY = 3, + HOURLY = 4, + MINUTELY = 5, + SECONDLY = 6, +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/period-type.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/period-type.ts new file mode 100644 index 0000000000..95c0e138b7 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/period-type.ts @@ -0,0 +1,5 @@ +export enum PeriodType { + UNLIMITED = "UNLIMITED", + ROLLING = "ROLLING", + RANGE = "RANGE", +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/scheduling-type.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/scheduling-type.ts new file mode 100644 index 0000000000..cc581daa4f --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/enums/scheduling-type.ts @@ -0,0 +1,5 @@ +export enum SchedulingType { + ROUND_ROBIN = "ROUND_ROBIN", + COLLECTIVE = "COLLECTIVE", + MANAGED = "MANAGED", +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts new file mode 100644 index 0000000000..d1cbf025e2 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts @@ -0,0 +1,25 @@ +import { Transform } from "class-transformer"; +import { IsOptional, IsString } from "class-validator"; + +export class GetEventTypesQuery_2024_06_14 { + @IsOptional() + @IsString() + username?: string; + + @IsString() + @IsOptional() + eventSlug?: string; + + @IsOptional() + @TransformUsernames() + usernames?: string[]; +} + +function TransformUsernames() { + return Transform(({ value }) => { + if (typeof value === "string") { + return value.split(",").map((username) => username.trim()); + } + return value; + }); +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts new file mode 100644 index 0000000000..348e5c7aef --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts @@ -0,0 +1,6 @@ +export * from "./create-event-type.input"; +export * from "./locations.input"; +export * from "./booking-fields.input"; +export * from "./recurring-event.input"; +export * from "./update-event-type.input"; +export * from "./get-event-types-query.input"; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts new file mode 100644 index 0000000000..2a41643f99 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts @@ -0,0 +1,122 @@ +import { BadRequestException } from "@nestjs/common"; +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; +import { plainToInstance } from "class-transformer"; +import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean } from "class-validator"; +import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator"; +import { registerDecorator, validate, ValidatorConstraint } from "class-validator"; + +const locations = ["address", "link", "integration", "phone"] as const; + +export class AddressLocation_2024_06_14 { + @IsIn(locations) + type!: "address"; + + @IsString() + @DocsProperty({ example: "123 Example St, City, Country" }) + address!: string; + + @IsBoolean() + public!: boolean; +} + +export class LinkLocation_2024_06_14 { + @IsIn(locations) + type!: "link"; + + @IsUrl() + @DocsProperty({ example: "https://customvideo.com/join/123456" }) + link!: string; + + @IsBoolean() + public!: boolean; +} + +const integrations = ["cal-video"] as const; +export type Integration_2024_06_14 = (typeof integrations)[number]; + +export class IntegrationLocation_2024_06_14 { + @IsIn(locations) + type!: "integration"; + + @IsIn(integrations) + @DocsProperty({ example: integrations[0] }) + integration!: Integration_2024_06_14; +} + +export class PhoneLocation_2024_06_14 { + @IsIn(locations) + type!: "phone"; + + @IsPhoneNumber() + @DocsProperty({ example: "+37120993151" }) + phone!: string; + + @IsBoolean() + public!: boolean; +} + +export type Location_2024_06_14 = + | AddressLocation_2024_06_14 + | LinkLocation_2024_06_14 + | IntegrationLocation_2024_06_14 + | PhoneLocation_2024_06_14; + +@ValidatorConstraint({ async: true }) +class LocationValidator_2024_06_14 implements ValidatorConstraintInterface { + private classTypeMap: { [key: string]: new () => Location_2024_06_14 } = { + address: AddressLocation_2024_06_14, + link: LinkLocation_2024_06_14, + integration: IntegrationLocation_2024_06_14, + phone: PhoneLocation_2024_06_14, + }; + + async validate(locations: { type: string }[]) { + if (!Array.isArray(locations)) { + throw new BadRequestException(`'locations' must be an array.`); + } + + if (!locations.length) { + throw new BadRequestException(`'locations' must contain at least 1 location.`); + } + + for (const location of locations) { + const { type } = location; + if (!type) { + throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`); + } + + const ClassType = this.classTypeMap[type]; + if (!ClassType) { + throw new BadRequestException( + `Unsupported location type '${type}'. Valid types are address, link, integration, and phone.` + ); + } + + const instance = plainToInstance(ClassType, location); + const errors = await validate(instance); + if (errors.length > 0) { + const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", "); + throw new BadRequestException(`Validation failed for ${type} location: ${message}`); + } + } + + return true; + } + + defaultMessage() { + return `Validation failed for one or more location entries.`; + } +} + +export function ValidateLocations_2024_06_14(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (object: any, propertyName: string) { + registerDecorator({ + name: "ValidateLocation", + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: new LocationValidator_2024_06_14(), + }); + }; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/recurring-event.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/recurring-event.input.ts new file mode 100644 index 0000000000..7d05bfc62d --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/recurring-event.input.ts @@ -0,0 +1,34 @@ +import { IsInt, IsEnum, IsOptional, IsString, IsDateString } from "class-validator"; + +export enum Frequency { + YEARLY = 0, + MONTHLY = 1, + WEEKLY = 2, + DAILY = 3, + HOURLY = 4, + MINUTELY = 5, + SECONDLY = 6, +} + +export class RecurringEvent_2024_06_14 { + @IsOptional() + @IsDateString() + dtstart?: Date; + + @IsInt() + interval!: number; + + @IsInt() + count!: number; + + @IsEnum(Frequency) + freq!: Frequency; + + @IsOptional() + @IsDateString() + until?: Date; + + @IsOptional() + @IsString() + tzid?: string; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts new file mode 100644 index 0000000000..2aeee77184 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts @@ -0,0 +1,50 @@ +import { IsString, IsInt, IsBoolean, IsOptional, Min } from "class-validator"; + +import type { BookingField_2024_06_14 } from "./booking-fields.input"; +import { ValidateBookingFields_2024_06_14 } from "./booking-fields.input"; +import { ValidateLocations_2024_06_14 } from "./locations.input"; +import type { Location_2024_06_14 } from "./locations.input"; + +export class UpdateEventTypeInput_2024_06_14 { + @IsOptional() + @IsInt() + @Min(1) + lengthInMinutes?: number; + + @IsOptional() + @IsString() + title?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @ValidateLocations_2024_06_14() + locations?: Location_2024_06_14[]; + + @IsOptional() + @ValidateBookingFields_2024_06_14() + bookingFields?: BookingField_2024_06_14[]; + + @IsBoolean() + @IsOptional() + disableGuests?: boolean; + + @IsInt() + @IsOptional() + slotInterval?: number; + + @IsInt() + @Min(0) + @IsOptional() + minimumBookingNotice?: number; + + @IsInt() + @IsOptional() + beforeEventBuffer?: number; + + @IsInt() + @IsOptional() + afterEventBuffer?: number; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts new file mode 100644 index 0000000000..abf460c388 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts @@ -0,0 +1,137 @@ +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, Min } from "class-validator"; + +import type { Location_2024_06_14, BookingField_2024_06_14 } from "../inputs"; +import { RecurringEvent_2024_06_14 } from "../inputs"; +import { ValidateBookingFields_2024_06_14 } from "../inputs/booking-fields.input"; +import { ValidateLocations_2024_06_14 } from "../inputs/locations.input"; + +enum SchedulingTypeEnum { + ROUND_ROBIN = "ROUND_ROBIN", + COLLECTIVE = "COLLECTIVE", + MANAGED = "MANAGED", +} + +export type SchedulingType = "ROUND_ROBIN" | "COLLECTIVE" | "MANAGED"; + +class Schedule_2024_06_14 { + @IsInt() + id!: number; + + @IsString() + timeZone!: string | null; +} + +class User_2024_06_14 { + @IsInt() + id!: number; + + @IsString() + name!: string | null; + + @IsString() + username!: string | null; + + @IsString() + avatarUrl!: string | null; + + @IsString() + weekStart!: string; + + @IsString() + brandColor!: string | null; + + @IsString() + darkBrandColor!: string | null; + + metadata!: Record; +} + +export class EventTypeOutput_2024_06_14 { + @IsInt() + @DocsProperty({ example: 1 }) + id!: number; + + @IsInt() + ownerId!: number; + + @IsInt() + @Min(1) + lengthInMinutes!: number; + + @IsString() + title!: string; + + @IsString() + slug!: string; + + @IsString() + description!: string; + + @ValidateLocations_2024_06_14() + locations!: Location_2024_06_14[]; + + @ValidateBookingFields_2024_06_14() + bookingFields!: BookingField_2024_06_14[]; + + @IsBoolean() + disableGuests!: boolean; + + @IsInt() + @IsOptional() + slotInterval?: number | null; + + @IsInt() + @Min(0) + @IsOptional() + minimumBookingNotice?: number; + + @IsInt() + @IsOptional() + beforeEventBuffer?: number; + + @IsInt() + @IsOptional() + afterEventBuffer?: number; + + @IsEnum(SchedulingTypeEnum) + schedulingType!: SchedulingType | null; + + @Type(() => RecurringEvent_2024_06_14) + recurringEvent!: RecurringEvent_2024_06_14 | null; + + @Type(() => Object) + metadata!: Record; + + @IsBoolean() + requiresConfirmation!: boolean; + + @IsInt() + price!: number; + + @IsString() + currency!: string; + + @IsBoolean() + lockTimeZoneToggleOnBookingPage!: boolean; + + @IsInt() + seatsPerTimeSlot!: number | null; + + @IsBoolean() + forwardParamsSuccessRedirect!: boolean | null; + + @IsString() + successRedirectUrl!: string | null; + + @IsBoolean() + seatsShowAvailabilityCount!: boolean | null; + + @IsBoolean() + isInstantEvent!: boolean; + + users!: User_2024_06_14[]; + + schedule!: Schedule_2024_06_14 | null; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts new file mode 100644 index 0000000000..4fd4043827 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts @@ -0,0 +1 @@ +export * from "./event-type.output"; diff --git a/packages/platform/types/event-types/index.ts b/packages/platform/types/event-types/index.ts new file mode 100644 index 0000000000..ee023b5283 --- /dev/null +++ b/packages/platform/types/event-types/index.ts @@ -0,0 +1 @@ +export * from "./event-types_2024_06_14"; diff --git a/packages/platform/types/index.ts b/packages/platform/types/index.ts index 91b03c3138..e148e7eb29 100644 --- a/packages/platform/types/index.ts +++ b/packages/platform/types/index.ts @@ -6,3 +6,4 @@ export * from "./calendars"; export * from "./bookings"; export * from "./billing"; export * from "./schedules"; +export * from "./event-types"; diff --git a/packages/ui/components/avatar/UserAvatarGroupWithOrg.tsx b/packages/ui/components/avatar/UserAvatarGroupWithOrg.tsx index cb898760f4..2d19532db7 100644 --- a/packages/ui/components/avatar/UserAvatarGroupWithOrg.tsx +++ b/packages/ui/components/avatar/UserAvatarGroupWithOrg.tsx @@ -3,13 +3,11 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; import type { Team, User } from "@calcom/prisma/client"; -import type { UserProfile } from "@calcom/types/UserProfile"; import { AvatarGroup } from "@calcom/ui"; type UserAvatarProps = Omit, "items"> & { users: (Pick & { bookerUrl: string; - profile: UserProfile; })[]; organization: Pick; }; diff --git a/yarn.lock b/yarn.lock index 54ce330abf..6f496fee47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4065,6 +4065,7 @@ __metadata: resolution: "@calcom/api-v2@workspace:apps/api/v2" dependencies: "@calcom/platform-constants": "*" + "@calcom/platform-libraries-0.0.13": "npm:@calcom/platform-libraries@0.0.13" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" "@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4" "@calcom/platform-types": "*" @@ -4216,7 +4217,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/atoms@*, @calcom/atoms@workspace:^, @calcom/atoms@workspace:packages/platform/atoms": +"@calcom/atoms@*, @calcom/atoms@workspace:packages/platform/atoms": version: 0.0.0-use.local resolution: "@calcom/atoms@workspace:packages/platform/atoms" dependencies: @@ -5015,6 +5016,17 @@ __metadata: languageName: unknown linkType: soft +"@calcom/platform-libraries-0.0.13@npm:@calcom/platform-libraries@0.0.13": + version: 0.0.13 + resolution: "@calcom/platform-libraries@npm:0.0.13" + dependencies: + "@calcom/core": "*" + "@calcom/features": "*" + "@calcom/lib": "*" + checksum: 53b92db84c669c2ff179acc4056ebc3c7426779aa0b9b7578f1bdd1abab5426bb45b9cb0b4858f0f8c8760c352e0a6f96e2062428a9e9cf0c695601d0f210349 + languageName: node + linkType: hard + "@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2, @calcom/platform-libraries@npm:0.0.2": version: 0.0.2 resolution: "@calcom/platform-libraries@npm:0.0.2" @@ -5378,6 +5390,15 @@ __metadata: languageName: unknown linkType: soft +"@calcom/twipla@workspace:packages/app-store/twipla": + version: 0.0.0-use.local + resolution: "@calcom/twipla@workspace:packages/app-store/twipla" + dependencies: + "@calcom/lib": "*" + "@calcom/types": "*" + languageName: unknown + linkType: soft + "@calcom/typeform@workspace:packages/app-store/typeform": version: 0.0.0-use.local resolution: "@calcom/typeform@workspace:packages/app-store/typeform" @@ -5433,6 +5454,15 @@ __metadata: languageName: unknown linkType: soft +"@calcom/umami@workspace:packages/app-store/umami": + version: 0.0.0-use.local + resolution: "@calcom/umami@workspace:packages/app-store/umami" + dependencies: + "@calcom/lib": "*" + "@calcom/types": "*" + languageName: unknown + linkType: soft + "@calcom/vimcal@workspace:packages/app-store/vimcal": version: 0.0.0-use.local resolution: "@calcom/vimcal@workspace:packages/app-store/vimcal" @@ -5636,7 +5666,6 @@ __metadata: dependencies: "@algora/sdk": ^0.1.2 "@calcom/app-store": "*" - "@calcom/atoms": "workspace:^" "@calcom/config": "*" "@calcom/dayjs": "*" "@calcom/embed-react": "workspace:^" @@ -5682,7 +5711,6 @@ __metadata: "@vercel/og": ^0.5.0 autoprefixer: ^10.4.12 bcryptjs: ^2.4.3 - class-variance-authority: ^0.7.0 clsx: ^1.2.1 cobe: ^0.4.1 concurrently: ^7.6.0 @@ -5695,7 +5723,6 @@ __metadata: env-cmd: ^10.1.0 eslint: ^8.34.0 fathom-client: ^3.5.0 - framer-motion: ^11.0.25 globby: ^13.1.3 graphql: ^16.8.0 graphql-codegen: ^0.4.0 @@ -5705,7 +5732,7 @@ __metadata: i18n-unused: ^0.13.0 iframe-resizer-react: ^1.1.0 keen-slider: ^6.8.0 - lucide-react: ^0.364.0 + lucide-react: ^0.171.0 micro: ^10.0.1 next: ^14.1.3 next-auth: ^4.22.1 @@ -5717,7 +5744,7 @@ __metadata: prism-react-renderer: ^1.3.5 react: ^18.2.0 react-confetti: ^6.0.1 - react-datocms: ^5.0.3 + react-datocms: ^3.1.0 react-device-detect: ^2.2.2 react-dom: ^18.2.0 react-fast-marquee: ^1.6.4 @@ -5727,10 +5754,8 @@ __metadata: react-live-chat-loader: ^2.8.1 react-markdown: ^9.0.1 react-merge-refs: 1.1.0 - react-parallax-tilt: ^1.7.226 react-resize-detector: ^9.1.0 react-twemoji: ^0.3.0 - react-twitter-embed: ^4.0.4 react-use-measure: ^2.1.1 react-wrap-balancer: ^1.0.0 remark: ^14.0.2 @@ -9402,59 +9427,6 @@ __metadata: languageName: node linkType: hard -"@mux/mux-player-react@npm:*": - version: 2.7.0 - resolution: "@mux/mux-player-react@npm:2.7.0" - dependencies: - "@mux/mux-player": 2.7.0 - "@mux/playback-core": 0.24.0 - prop-types: ^15.7.2 - peerDependencies: - "@types/react": ^17.0.0 || ^18 - react: ^17.0.2 || ^18 - react-dom: ^17.0.2 || ^18 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: 15ea8a8b9b2c82252e0647c22f8a1fbe7a04efaf1eb7aa61d10e4665b3dad45057c97c47cd04b8da42d02aad8288555feb9de98a8b9059b730420a8d12cada73 - languageName: node - linkType: hard - -"@mux/mux-player@npm:2.7.0": - version: 2.7.0 - resolution: "@mux/mux-player@npm:2.7.0" - dependencies: - "@mux/mux-video": 0.19.0 - "@mux/playback-core": 0.24.0 - media-chrome: ~3.2.1 - checksum: d3254c5d82f42d26e4ec3c27e50b6438a7dc74863381ddd9003bdc92947f6db4fc45b8caee21de9125aa80e71536268426d7159617dbace739de99f8f0ed259f - languageName: node - linkType: hard - -"@mux/mux-video@npm:0.19.0": - version: 0.19.0 - resolution: "@mux/mux-video@npm:0.19.0" - dependencies: - "@mux/playback-core": 0.24.0 - castable-video: ~1.0.6 - custom-media-element: ~1.2.3 - media-tracks: ~0.3.0 - checksum: 342d6f29fc60a9ea9694a8d794f679306e194f5de5d5cf85ed67391dce8d70330c9d78ee2681aee0e332ab1d2583a8a0174addf1182d0cf1c08fb85a01da849d - languageName: node - linkType: hard - -"@mux/playback-core@npm:0.24.0": - version: 0.24.0 - resolution: "@mux/playback-core@npm:0.24.0" - dependencies: - hls.js: ~1.5.8 - mux-embed: ~5.2.0 - checksum: b9783c05b1c009fb296a84de61d7b693fd81996bd35ef26aca7342a09eee476e4d8840fde1ef89273b01db2d4f3b57692101ae7bcc4cb5b15d4d6786bd0ba961 - languageName: node - linkType: hard - "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.9 resolution: "@ndelangen/get-tarball@npm:3.0.9" @@ -21434,15 +21406,6 @@ __metadata: languageName: node linkType: hard -"castable-video@npm:~1.0.6": - version: 1.0.10 - resolution: "castable-video@npm:1.0.10" - dependencies: - custom-media-element: ~1.3.2 - checksum: 5b7a27aacf305f40c6867e96e773f4290260f6eb8b01fe1f209506f8f6dec4cd099f3e35ee57de098829e4bcc688ccdc2cbb2fc86c509712dd573eadf66d42cc - languageName: node - linkType: hard - "ccount@npm:^2.0.0": version: 2.0.1 resolution: "ccount@npm:2.0.1" @@ -21907,15 +21870,6 @@ __metadata: languageName: node linkType: hard -"class-variance-authority@npm:^0.7.0": - version: 0.7.0 - resolution: "class-variance-authority@npm:0.7.0" - dependencies: - clsx: 2.0.0 - checksum: e7fd1fab433ef06f52a1b7b241b70b4a185864deef199d3b0a2c3412f1cc179517288264c383f3b971a00d76811625fc8f7ffe709e6170219e88cd7368f08a20 - languageName: node - linkType: hard - "classnames@npm:^2.2.5, classnames@npm:^2.2.6": version: 2.3.2 resolution: "classnames@npm:2.3.2" @@ -22188,13 +22142,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e - languageName: node - linkType: hard - "clsx@npm:^1.1.1": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -23500,20 +23447,6 @@ __metadata: languageName: node linkType: hard -"custom-media-element@npm:~1.2.3": - version: 1.2.3 - resolution: "custom-media-element@npm:1.2.3" - checksum: da43680c35c870cd80ea02b9fce3efe25e9502c7088de5ab0e47ef2734aa9ce7e147c95f78a10abd4c13c47a81eae4712a7651ad18f87e023bc371082ba09842 - languageName: node - linkType: hard - -"custom-media-element@npm:~1.3.2": - version: 1.3.2 - resolution: "custom-media-element@npm:1.3.2" - checksum: 9ec2ff88c3c5ba7d20b9484c0359b4cfe08ad6804f3116934ffbae2f2e0e04d2a832beb6f0bec56027a969ab366727d217406f750890613c988de6de5330cb4f - languageName: node - linkType: hard - "d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6": version: 3.2.3 resolution: "d3-array@npm:3.2.3" @@ -27554,26 +27487,6 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^11.0.25": - version: 11.2.10 - resolution: "framer-motion@npm:11.2.10" - dependencies: - tslib: ^2.4.0 - peerDependencies: - "@emotion/is-prop-valid": "*" - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - "@emotion/is-prop-valid": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: d124bba0d59856b5b17db2d55f6a627d8035e7b7728a610669e6f8dd215b8bbe1be1ee4a0d62d7ae365496bc2ebe8761b2bf4cf2e9c4760b4bfd7fdfd47a43f1 - languageName: node - linkType: hard - "fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -29246,13 +29159,6 @@ __metadata: languageName: node linkType: hard -"hls.js@npm:~1.5.8": - version: 1.5.11 - resolution: "hls.js@npm:1.5.11" - checksum: 04ce982633d33911005fe0d536ae11ffe966ee045fa29c3a06a700aa166d8743e9e500cfc768bf0562be6d124166ae1fb39d3acdae2bfbc69931d3c73ae85d2a - languageName: node - linkType: hard - "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -34044,6 +33950,15 @@ __metadata: languageName: node linkType: hard +"lucide-react@npm:^0.171.0": + version: 0.171.0 + resolution: "lucide-react@npm:0.171.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + checksum: 768ffe368c52a518ee339203d86ff4479989ab4d79c0716f721900c4bb7392ef6ff7a14807f6a685abd74d27f4c1778170bff77a0ab4c3e06c17944b557d8300 + languageName: node + linkType: hard + "lucide-react@npm:^0.364.0": version: 0.364.0 resolution: "lucide-react@npm:0.364.0" @@ -34612,20 +34527,6 @@ __metadata: languageName: node linkType: hard -"media-chrome@npm:~3.2.1": - version: 3.2.3 - resolution: "media-chrome@npm:3.2.3" - checksum: 073757ff6952045052f9c63fbb688ac8ff9f54787f5e26eaa52f47d72a8c53df65a1b62a30e2c7d90631c8c59a0ec2b960e746ab3e6dca389a3f936b42c858f2 - languageName: node - linkType: hard - -"media-tracks@npm:~0.3.0": - version: 0.3.3 - resolution: "media-tracks@npm:0.3.3" - checksum: 4795af3f171d7ad3a68d1ac1c1e8166a735244fe57d3fc0ec53b1c7410799e524756fc0bfb389632aebb148436b03baade36ca34a3f5377776c3968f6d2cc580 - languageName: node - linkType: hard - "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -35994,13 +35895,6 @@ __metadata: languageName: node linkType: hard -"mux-embed@npm:~5.2.0": - version: 5.2.1 - resolution: "mux-embed@npm:5.2.1" - checksum: ec34d3e003e2520be6d1386ca2383daa198a1fa3a469ca3f06c11a742d6ab136eef4944c37e8f2dcaa0585e1b0a6dfc119e3cf2302c7eb0e7f2ebc7c1fc95fbd - languageName: node - linkType: hard - "mysql2@npm:3.9.1": version: 3.9.1 resolution: "mysql2@npm:3.9.1" @@ -40303,21 +40197,20 @@ __metadata: languageName: node linkType: hard -"react-datocms@npm:^5.0.3": - version: 5.0.3 - resolution: "react-datocms@npm:5.0.3" +"react-datocms@npm:^3.1.0": + version: 3.1.4 + resolution: "react-datocms@npm:3.1.4" dependencies: - "@mux/mux-player-react": "*" datocms-listen: ^0.1.9 datocms-structured-text-generic-html-renderer: ^2.0.1 datocms-structured-text-utils: ^2.0.1 - react-intersection-observer: ^9.4.3 + react-intersection-observer: ^8.33.1 react-string-replace: ^1.1.0 universal-base64: ^2.1.0 use-deep-compare-effect: ^1.6.1 peerDependencies: react: ">= 16.12.0" - checksum: 22c20152afb54424acfe967a2c8c525cd9f132a33374f2aba0231f16ea64dade389b096e2dac8de9ffded612bc32e9891d725609ee947639fe1cef907cb143f5 + checksum: 54aba12aef4937175c2011548a8a576c96c8d8a596e84d191826910624c1d596e76a49782689dc236388a10803b02e700ac820cb7500cca7fd147a81f6c544c3 languageName: node linkType: hard @@ -40569,16 +40462,12 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.4.3": - version: 9.10.3 - resolution: "react-intersection-observer@npm:9.10.3" +"react-intersection-observer@npm:^8.33.1": + version: 8.34.0 + resolution: "react-intersection-observer@npm:8.34.0" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - react-dom: - optional: true - checksum: 482c89a432e582749f3cb3dd696e08638a92e41fbcb81bcb3dc3cadebcf8b40bc47e7a52d2a7e8c4f9eb2a3c1c29b4cb0f21007c1540da05893b5abb11d7a761 + react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0 + checksum: 7713fecfd1512c7f5a60f9f0bf15403b8f8bbd4110bcafaeaea6de36a0e0eb60368c3638f99e9c97b75ad8fc787ea48c241dcb5c694f821d7f2976f709082cc5 languageName: node linkType: hard @@ -40688,16 +40577,6 @@ __metadata: languageName: node linkType: hard -"react-parallax-tilt@npm:^1.7.226": - version: 1.7.228 - resolution: "react-parallax-tilt@npm:1.7.228" - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 07ad03a35138efeafc991ed470577a0d2a1f816bd730f4f3dea10869d42936eb296cfc2e4035f7871e0637b12805ac9f4cf9cc583fd69582f228bf8591157089 - languageName: node - linkType: hard - "react-phone-input-2@npm:^2.15.1": version: 2.15.1 resolution: "react-phone-input-2@npm:2.15.1" @@ -41112,18 +40991,6 @@ __metadata: languageName: node linkType: hard -"react-twitter-embed@npm:^4.0.4": - version: 4.0.4 - resolution: "react-twitter-embed@npm:4.0.4" - dependencies: - scriptjs: ^2.5.9 - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: cdb3c5bd04c4da0efa767476be47c0a3865fb6335f2a1b9e242170167b51615c38164223278cef60c77143c4bac27ba582cbea054d0af3f138104fa5ec537c4c - languageName: node - linkType: hard - "react-universal-interface@npm:^0.6.2": version: 0.6.2 resolution: "react-universal-interface@npm:0.6.2" @@ -42948,13 +42815,6 @@ __metadata: languageName: node linkType: hard -"scriptjs@npm:^2.5.9": - version: 2.5.9 - resolution: "scriptjs@npm:2.5.9" - checksum: fc84cb6b60b6fb9aa6f1b3bc59fc94b233bd5241ed3a04233579014382b5eb60640269c87d8657902acc09f9b785ee33230c218627cea00e653564bda8f5acb6 - languageName: node - linkType: hard - "scuid@npm:^1.1.0": version: 1.1.0 resolution: "scuid@npm:1.1.0"