From 5ee3744015804d06c75084e46d729d1acd3993e2 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Mon, 20 May 2024 18:26:48 +0200 Subject: [PATCH] Revert "refactor: api v2 schedules (#14870)" (#15130) This reverts commit c25c835b76ce1dcb7b24c44b04b29d6f91d4c546. --- .../bookings.controller.e2e-spec.ts | 5 +- .../v2/src/ee/me/me.controller.e2e-spec.ts | 14 +- apps/api/v2/src/ee/me/me.controller.ts | 2 +- .../schedules.controller.e2e-spec.ts | 187 ++++---- .../controllers/schedules.controller.ts | 38 +- .../schedules/inputs/create-schedule.input.ts | 20 + .../outputs/create-schedule.output.ts | 3 +- .../outputs/delete-schedule.output.ts | 0 .../outputs/get-default-schedule.output.ts | 3 +- .../schedules/outputs/get-schedule.output.ts | 7 +- .../schedules/outputs/get-schedules.output.ts | 9 +- .../outputs/schedule-updated.output.ts | 95 ++++ .../ee/schedules/outputs/schedule.output.ts | 105 +++++ .../outputs/update-schedule.output.ts | 11 +- .../v2/src/ee/schedules/schedules.module.ts | 7 +- .../src/ee/schedules/schedules.repository.ts | 145 +----- .../services/input-schedules.service.ts | 40 -- .../services/output-schedules.service.ts | 78 ---- .../schedules/services/schedules.service.ts | 119 +++-- .../availabilities/availabilities.module.ts | 10 + .../availabilities/availabilities.service.ts | 16 + .../inputs/create-availability.input.ts | 48 ++ .../oauth-client-users.controller.ts | 1 + .../v2/src/modules/users/users.repository.ts | 8 - apps/api/v2/swagger/documentation.json | 436 +++++++++++------- packages/lib/availability.ts | 6 +- .../for-atom.ts => client/transformers.ts} | 35 +- packages/lib/schedules/index.ts | 2 +- .../lib/schedules/transformers/from-api.ts | 54 --- packages/lib/schedules/transformers/index.ts | 2 - .../availability/AvailabilitySettings.tsx | 8 +- .../transformApiScheduleForAtom.spec.ts | 127 ----- .../transformApiScheduleForAtom.ts | 59 --- .../transformAtomScheduleForApi.spec.ts | 72 --- .../transformAtomScheduleForApi.ts | 62 --- .../PlatformAvailabilitySettingsWrapper.tsx | 61 +-- .../atoms/hooks/schedules/useSchedule.ts | 26 -- .../atoms/hooks/schedules/useSchedules.ts | 26 -- .../platform/atoms/hooks/useClientSchedule.ts | 31 ++ .../{schedules => }/useDeleteSchedule.ts | 4 +- .../{schedules => }/useUpdateSchedule.ts | 18 +- packages/platform/atoms/jest.config.json | 9 - packages/platform/atoms/package.json | 8 +- packages/platform/libraries/index.ts | 14 +- packages/platform/types/schedules.ts | 115 +++++ .../platform/types/schedules/constants.ts | 13 - packages/platform/types/schedules/index.ts | 3 - .../schedules/inputs/create-schedule.input.ts | 97 ---- .../platform/types/schedules/inputs/index.ts | 2 - .../schedules/inputs/update-schedule.input.ts | 56 --- .../platform/types/schedules/outputs/index.ts | 7 - .../schedules/outputs/schedule.output.ts | 66 --- .../availability/schedule/get.handler.ts | 15 +- .../availability/schedule/update.handler.ts | 4 +- yarn.lock | 203 ++++++-- 55 files changed, 1215 insertions(+), 1397 deletions(-) create mode 100644 apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/create-schedule.output.ts (89%) rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/delete-schedule.output.ts (100%) rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/get-default-schedule.output.ts (89%) rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/get-schedule.output.ts (84%) rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/get-schedules.output.ts (83%) create mode 100644 apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts create mode 100644 apps/api/v2/src/ee/schedules/outputs/schedule.output.ts rename {packages/platform/types => apps/api/v2/src/ee}/schedules/outputs/update-schedule.output.ts (73%) delete mode 100644 apps/api/v2/src/ee/schedules/services/input-schedules.service.ts delete mode 100644 apps/api/v2/src/ee/schedules/services/output-schedules.service.ts create mode 100644 apps/api/v2/src/modules/availabilities/availabilities.module.ts create mode 100644 apps/api/v2/src/modules/availabilities/availabilities.service.ts create mode 100644 apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts rename packages/lib/schedules/{transformers/for-atom.ts => client/transformers.ts} (68%) delete mode 100644 packages/lib/schedules/transformers/from-api.ts delete mode 100644 packages/lib/schedules/transformers/index.ts delete mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts delete mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts delete mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts delete mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts delete mode 100644 packages/platform/atoms/hooks/schedules/useSchedule.ts delete mode 100644 packages/platform/atoms/hooks/schedules/useSchedules.ts create mode 100644 packages/platform/atoms/hooks/useClientSchedule.ts rename packages/platform/atoms/hooks/{schedules => }/useDeleteSchedule.ts (94%) rename packages/platform/atoms/hooks/{schedules => }/useUpdateSchedule.ts (64%) delete mode 100644 packages/platform/atoms/jest.config.json create mode 100644 packages/platform/types/schedules.ts delete mode 100644 packages/platform/types/schedules/constants.ts delete mode 100644 packages/platform/types/schedules/index.ts delete mode 100644 packages/platform/types/schedules/inputs/create-schedule.input.ts delete mode 100644 packages/platform/types/schedules/inputs/index.ts delete mode 100644 packages/platform/types/schedules/inputs/update-schedule.input.ts delete mode 100644 packages/platform/types/schedules/outputs/index.ts delete mode 100644 packages/platform/types/schedules/outputs/schedule.output.ts diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts index 5cfdf9b814..891a8e7b62 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts @@ -2,9 +2,11 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output"; import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output"; +import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input"; import { SchedulesModule } from "@/ee/schedules/schedules.module"; import { SchedulesService } from "@/ee/schedules/services/schedules.service"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; @@ -19,7 +21,6 @@ import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { handleNewBooking } from "@calcom/platform-libraries"; -import { CreateScheduleInput } from "@calcom/platform-types"; import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types"; describe("Bookings Endpoints", () => { @@ -42,7 +43,7 @@ describe("Bookings Endpoints", () => { const moduleRef = await withAccessTokenAuth( userEmail, Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule], + imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule], }) ) .overrideGuard(PermissionsGuard) diff --git a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts b/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts index 5851f88753..d5179acd75 100644 --- a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts @@ -2,6 +2,7 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { SchedulesModule } from "@/ee/schedules/schedules.module"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input"; @@ -33,7 +34,14 @@ describe("Me Endpoints", () => { const moduleRef = await withAccessTokenAuth( userEmail, Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule], + imports: [ + AppModule, + PrismaModule, + AvailabilitiesModule, + UsersModule, + TokensModule, + SchedulesModule, + ], }) ) .overrideGuard(PermissionsGuard) @@ -110,13 +118,13 @@ describe("Me Endpoints", () => { }); it("should not update user associated with access token given invalid time format", async () => { - const bodyWithIncorrectTimeFormat = { timeFormat: 100 }; + const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 }; return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400); }); it("should not update user associated with access token given invalid week start", async () => { - const bodyWithIncorrectWeekStart = { weekStart: "waba luba dub dub" }; + const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" }; return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400); }); diff --git a/apps/api/v2/src/ee/me/me.controller.ts b/apps/api/v2/src/ee/me/me.controller.ts index 5b213c9397..2c64df0397 100644 --- a/apps/api/v2/src/ee/me/me.controller.ts +++ b/apps/api/v2/src/ee/me/me.controller.ts @@ -44,7 +44,7 @@ export class MeController { ): Promise { const updatedUser = await this.usersRepository.update(user.id, bodySchedule); if (bodySchedule.timeZone && user.defaultScheduleId) { - await this.schedulesRepository.updateUserSchedule(user.id, user.defaultScheduleId, { + await this.schedulesRepository.updateUserSchedule(user, user.defaultScheduleId, { timeZone: bodySchedule.timeZone, }); } diff --git a/apps/api/v2/src/ee/schedules/controllers/schedules.controller.e2e-spec.ts b/apps/api/v2/src/ee/schedules/controllers/schedules.controller.e2e-spec.ts index bf998f83c4..7e6dec77f5 100644 --- a/apps/api/v2/src/ee/schedules/controllers/schedules.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/schedules/controllers/schedules.controller.e2e-spec.ts @@ -1,7 +1,12 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; +import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input"; +import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output"; +import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output"; +import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output"; import { SchedulesModule } from "@/ee/schedules/schedules.module"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; @@ -15,14 +20,6 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { - CreateScheduleInput, - CreateScheduleOutput, - GetScheduleOutput, - GetSchedulesOutput, - ScheduleOutput, - UpdateScheduleOutput, -} from "@calcom/platform-types"; import { UpdateScheduleInput } from "@calcom/platform-types"; describe("Schedules Endpoints", () => { @@ -35,27 +32,23 @@ describe("Schedules Endpoints", () => { const userEmail = "schedules-controller-e2e@api.com"; let user: User; - const createScheduleInput: CreateScheduleInput = { - name: "work", - timeZone: "Europe/Rome", - isDefault: true, - }; - - const defaultAvailability: CreateScheduleInput["availability"] = [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ]; - let createdSchedule: CreateScheduleOutput["data"]; + const defaultAvailabilityDays = [1, 2, 3, 4, 5]; + const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z"; + const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z"; beforeAll(async () => { const moduleRef = await withAccessTokenAuth( userEmail, Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule], + imports: [ + AppModule, + PrismaModule, + AvailabilitiesModule, + UsersModule, + TokensModule, + SchedulesModule, + ], }) ) .overrideGuard(PermissionsGuard) @@ -82,66 +75,60 @@ describe("Schedules Endpoints", () => { }); it("should create a default schedule", async () => { + const scheduleName = "schedule-name"; + const scheduleTimeZone = "Europe/Rome"; + const isDefault = true; + + const body: CreateScheduleInput = { + name: scheduleName, + timeZone: scheduleTimeZone, + isDefault, + }; + return request(app.getHttpServer()) .post("/api/v2/schedules") - .send(createScheduleInput) + .send(body) .expect(201) .then(async (response) => { - const responseBody: CreateScheduleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - createdSchedule = response.body.data; + const responseData: CreateScheduleOutput = response.body; + expect(responseData.status).toEqual(SUCCESS_STATUS); + expect(responseData.data).toBeDefined(); + expect(responseData.data.isDefault).toEqual(isDefault); + expect(responseData.data.timeZone).toEqual(scheduleTimeZone); + expect(responseData.data.name).toEqual(scheduleName); - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1); + const schedule = responseData.data.schedule; + expect(schedule).toBeDefined(); + expect(schedule.length).toEqual(1); + expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays); + expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime); + expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime); - const scheduleOwner = createdSchedule.ownerId - ? await userRepositoryFixture.get(createdSchedule.ownerId) + const scheduleUser = schedule?.[0].userId + ? await userRepositoryFixture.get(schedule?.[0].userId) : null; - expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id); + expect(scheduleUser?.defaultScheduleId).toEqual(responseData.data.id); + createdSchedule = responseData.data; }); }); - function outputScheduleMatchesExpected( - outputSchedule: ScheduleOutput | null, - expected: CreateScheduleInput & { availability: CreateScheduleInput["availability"] } & { - overrides: CreateScheduleInput["overrides"]; - }, - expectedAvailabilityLength: number - ) { - expect(outputSchedule).toBeTruthy(); - expect(outputSchedule?.name).toEqual(expected.name); - expect(outputSchedule?.timeZone).toEqual(expected.timeZone); - expect(outputSchedule?.isDefault).toEqual(expected.isDefault); - expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength); - - const outputScheduleAvailability = outputSchedule?.availability[0]; - expect(outputScheduleAvailability).toBeDefined(); - expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days); - expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime); - expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime); - - expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides)); - } - it("should get default schedule", async () => { return request(app.getHttpServer()) .get("/api/v2/schedules/default") .expect(200) .then(async (response) => { - const responseBody: GetScheduleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const outputSchedule = responseBody.data; + const responseData: CreateScheduleOutput = response.body; + expect(responseData.status).toEqual(SUCCESS_STATUS); + expect(responseData.data).toBeDefined(); + expect(responseData.data.id).toEqual(createdSchedule.id); + expect(responseData.data.schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId); - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1); + const schedule = responseData.data.schedule; + expect(schedule).toBeDefined(); + expect(schedule.length).toEqual(1); + expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays); + expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime); + expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime); }); }); @@ -150,21 +137,23 @@ describe("Schedules Endpoints", () => { .get(`/api/v2/schedules`) .expect(200) .then((response) => { - const responseBody: GetSchedulesOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const outputSchedule = responseBody.data[0]; + const responseData: GetSchedulesOutput = response.body; + expect(responseData.status).toEqual(SUCCESS_STATUS); + expect(responseData.data).toBeDefined(); + expect(responseData.data?.[0].id).toEqual(createdSchedule.id); + expect(responseData.data?.[0].schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId); - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1); + const schedule = responseData.data?.[0].schedule; + expect(schedule).toBeDefined(); + expect(schedule.length).toEqual(1); + expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays); + expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime); + expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime); }); }); it("should update schedule name", async () => { - const newScheduleName = "updated-schedule-name"; + const newScheduleName = "new-schedule-name"; const body: UpdateScheduleInput = { name: newScheduleName, @@ -177,41 +166,19 @@ describe("Schedules Endpoints", () => { .then((response: any) => { const responseData: UpdateScheduleOutput = response.body; expect(responseData.status).toEqual(SUCCESS_STATUS); - const responseSchedule = responseData.data; + expect(responseData.data).toBeDefined(); + expect(responseData.data.schedule.name).toEqual(newScheduleName); + expect(responseData.data.schedule.id).toEqual(createdSchedule.id); + expect(responseData.data.schedule.userId).toEqual(createdSchedule.schedule[0].userId); - const expectedSchedule = { ...createdSchedule, name: newScheduleName }; - outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1); + const availability = responseData.data.schedule.availability; + expect(availability).toBeDefined(); + expect(availability?.length).toEqual(1); + expect(availability?.[0]?.days).toEqual(defaultAvailabilityDays); + expect(availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime); + expect(availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime); - createdSchedule = responseSchedule; - }); - }); - - it("should add overrides", async () => { - const overrides = [ - { - date: "2026-05-05", - startTime: "10:00", - endTime: "12:00", - }, - ]; - - const body: UpdateScheduleInput = { - overrides, - }; - - return request(app.getHttpServer()) - .patch(`/api/v2/schedules/${createdSchedule.id}`) - .send(body) - .expect(200) - .then((response: any) => { - const responseData: UpdateScheduleOutput = response.body; - expect(responseData.status).toEqual(SUCCESS_STATUS); - const responseSchedule = responseData.data; - - const expectedSchedule = { ...createdSchedule, overrides }; - outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1); - - createdSchedule = responseSchedule; + createdSchedule.name = newScheduleName; }); }); diff --git a/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts b/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts index 396d7fb88d..b2860cfe5e 100644 --- a/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts +++ b/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts @@ -1,3 +1,9 @@ +import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output"; +import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output"; +import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output"; +import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output"; +import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output"; +import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output"; import { SchedulesService } from "@/ee/schedules/services/schedules.service"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; @@ -20,16 +26,9 @@ import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger"; import { Throttle } from "@nestjs/throttler"; import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { - CreateScheduleOutput, - CreateScheduleInput, - UpdateScheduleInput, - GetScheduleOutput, - UpdateScheduleOutput, - GetDefaultScheduleOutput, - DeleteScheduleOutput, - GetSchedulesOutput, -} from "@calcom/platform-types"; +import { UpdateScheduleInput } from "@calcom/platform-types"; + +import { CreateScheduleInput } from "../inputs/create-schedule.input"; @Controller({ path: "schedules", @@ -47,22 +46,26 @@ export class SchedulesController { @Body() bodySchedule: CreateScheduleInput ): Promise { const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule); + const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule); return { status: SUCCESS_STATUS, - data: schedule, + data: scheduleFormatted, }; } @Get("/default") @Permissions([SCHEDULE_READ]) @ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput }) - async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise { + async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise { const schedule = await this.schedulesService.getUserScheduleDefault(user.id); + const scheduleFormatted = schedule + ? await this.schedulesService.formatScheduleForAtom(user, schedule) + : null; return { status: SUCCESS_STATUS, - data: schedule, + data: scheduleFormatted, }; } @@ -74,10 +77,11 @@ export class SchedulesController { @Param("scheduleId") scheduleId: number ): Promise { const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId); + const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule); return { status: SUCCESS_STATUS, - data: schedule, + data: scheduleFormatted, }; } @@ -85,13 +89,15 @@ export class SchedulesController { @Permissions([SCHEDULE_READ]) async getSchedules(@GetUser() user: UserWithProfile): Promise { const schedules = await this.schedulesService.getUserSchedules(user.id); + const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules); return { status: SUCCESS_STATUS, - data: schedules, + data: schedulesFormatted, }; } + // note(Lauris): currently this endpoint is atoms only @Patch("/:scheduleId") @Permissions([SCHEDULE_WRITE]) async updateSchedule( @@ -100,7 +106,7 @@ export class SchedulesController { @Param("scheduleId") scheduleId: string ): Promise { const updatedSchedule = await this.schedulesService.updateUserSchedule( - user.id, + user, Number(scheduleId), bodySchedule ); diff --git a/apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts b/apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts new file mode 100644 index 0000000000..f90c291adf --- /dev/null +++ b/apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts @@ -0,0 +1,20 @@ +import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input"; +import { Type } from "class-transformer"; +import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator"; + +export class CreateScheduleInput { + @IsString() + name!: string; + + @IsTimeZone() + timeZone!: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateAvailabilityInput) + @IsOptional() + availabilities?: CreateAvailabilityInput[]; + + @IsBoolean() + isDefault!: boolean; +} diff --git a/packages/platform/types/schedules/outputs/create-schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts similarity index 89% rename from packages/platform/types/schedules/outputs/create-schedule.output.ts rename to apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts index 9ff78f7094..bbcdd17887 100644 --- a/packages/platform/types/schedules/outputs/create-schedule.output.ts +++ b/apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts @@ -1,11 +1,10 @@ +import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { ScheduleOutput } from "./schedule.output"; - export class CreateScheduleOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/packages/platform/types/schedules/outputs/delete-schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/delete-schedule.output.ts similarity index 100% rename from packages/platform/types/schedules/outputs/delete-schedule.output.ts rename to apps/api/v2/src/ee/schedules/outputs/delete-schedule.output.ts diff --git a/packages/platform/types/schedules/outputs/get-default-schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts similarity index 89% rename from packages/platform/types/schedules/outputs/get-default-schedule.output.ts rename to apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts index 0ad1969f3b..cf8369fa21 100644 --- a/packages/platform/types/schedules/outputs/get-default-schedule.output.ts +++ b/apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts @@ -1,11 +1,10 @@ +import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { ScheduleOutput } from "./schedule.output"; - export class GetDefaultScheduleOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/packages/platform/types/schedules/outputs/get-schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts similarity index 84% rename from packages/platform/types/schedules/outputs/get-schedule.output.ts rename to apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts index c34268347c..f49e291e23 100644 --- a/packages/platform/types/schedules/outputs/get-schedule.output.ts +++ b/apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts @@ -1,11 +1,10 @@ +import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { ScheduleOutput } from "./schedule.output"; - export class GetScheduleOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) @@ -17,7 +16,5 @@ export class GetScheduleOutput { @IsNotEmptyObject() @ValidateNested() @Type(() => ScheduleOutput) - data!: ScheduleOutput | null; - - error?: Error; + data!: ScheduleOutput; } diff --git a/packages/platform/types/schedules/outputs/get-schedules.output.ts b/apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts similarity index 83% rename from packages/platform/types/schedules/outputs/get-schedules.output.ts rename to apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts index a501ee6330..9f7f6ce32f 100644 --- a/packages/platform/types/schedules/outputs/get-schedules.output.ts +++ b/apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts @@ -1,22 +1,21 @@ +import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { ScheduleOutput } from "./schedule.output"; - export class GetSchedulesOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - @ApiProperty({ type: [ScheduleOutput] }) + @ApiProperty({ + type: ScheduleOutput, + }) @IsNotEmptyObject() @ValidateNested({ each: true }) @Type(() => ScheduleOutput) @IsArray() data!: ScheduleOutput[]; - - error?: Error; } diff --git a/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts b/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts new file mode 100644 index 0000000000..a16600db98 --- /dev/null +++ b/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts @@ -0,0 +1,95 @@ +import { Type } from "class-transformer"; +import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator"; + +class EventTypeModel { + @IsInt() + id!: number; + + @IsOptional() + @IsString() + eventName?: string | null; +} + +class AvailabilityModel { + @IsInt() + id!: number; + + @IsOptional() + @IsInt() + userId?: number | null; + + @IsOptional() + @IsInt() + scheduleId?: number | null; + + @IsOptional() + @IsInt() + eventTypeId?: number | null; + + @IsArray() + @IsInt({ each: true }) + days!: number[]; + + @IsOptional() + @Type(() => Date) + @IsString() // Assuming date is serialized/deserialized appropriately + startTime?: Date; + + @IsOptional() + @Type(() => Date) + @IsString() // Assuming date is serialized/deserialized appropriately + endTime?: Date; + + @IsOptional() + @Type(() => Date) + @IsString() // Assuming date is serialized/deserialized appropriately + date?: Date | null; +} + +class ScheduleModel { + @IsInt() + id!: number; + + @IsInt() + userId!: number; + + @IsString() + name!: string; + + @IsOptional() + @IsString() + timeZone?: string | null; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => EventTypeModel) + @IsArray() + eventType?: EventTypeModel[]; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AvailabilityModel) + @IsArray() + availability?: AvailabilityModel[]; +} + +export class UpdatedScheduleOutput { + @ValidateNested() + @Type(() => ScheduleModel) + schedule!: ScheduleModel; + + @IsBoolean() + isDefault!: boolean; + + @IsOptional() + @IsString() + timeZone?: string; + + @IsOptional() + @IsInt() + prevDefaultId?: number | null; + + @IsOptional() + @IsInt() + currentDefaultId?: number | null; +} diff --git a/apps/api/v2/src/ee/schedules/outputs/schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/schedule.output.ts new file mode 100644 index 0000000000..015bfb77d8 --- /dev/null +++ b/apps/api/v2/src/ee/schedules/outputs/schedule.output.ts @@ -0,0 +1,105 @@ +import { Type } from "class-transformer"; +import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator"; + +class AvailabilityModel { + @IsInt() + id!: number; + + @IsOptional() + @IsInt() + userId?: number | null; + + @IsOptional() + @IsInt() + eventTypeId?: number | null; + + @IsArray() + @IsInt({ each: true }) + days!: number[]; + + @IsDate() + @Type(() => Date) + startTime!: Date; + + @IsDate() + @Type(() => Date) + endTime!: Date; + + @IsOptional() + @IsDate() + @Type(() => Date) + date?: Date | null; + + @IsOptional() + @IsInt() + scheduleId?: number | null; +} + +class WorkingHours { + @IsArray() + @IsInt({ each: true }) + days!: number[]; + + @IsInt() + startTime!: number; + + @IsInt() + endTime!: number; + + @IsOptional() + @IsInt() + userId?: number | null; +} + +class TimeRange { + @IsOptional() + @IsInt() + userId?: number | null; + + @IsDate() + start!: Date; + + @IsDate() + end!: Date; +} + +export class ScheduleOutput { + @IsInt() + id!: number; + + @IsString() + name!: string; + + @IsBoolean() + isManaged!: boolean; + + @ValidateNested({ each: true }) + @Type(() => WorkingHours) + workingHours!: WorkingHours[]; + + @ValidateNested({ each: true }) + @Type(() => AvailabilityModel) + @IsArray() + schedule!: AvailabilityModel[]; + + availability!: TimeRange[][]; + + @IsString() + timeZone!: string; + + @ValidateNested({ each: true }) + @IsArray() + // note(Lauris) it should be + // dateOverrides!: { ranges: TimeRange[] }[]; + // but docs aren't generating correctly it results in array of strings + dateOverrides!: unknown[]; + + @IsBoolean() + isDefault!: boolean; + + @IsBoolean() + isLastSchedule!: boolean; + + @IsBoolean() + readOnly!: boolean; +} diff --git a/packages/platform/types/schedules/outputs/update-schedule.output.ts b/apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts similarity index 73% rename from packages/platform/types/schedules/outputs/update-schedule.output.ts rename to apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts index d80eb065bd..70ebe9ba9b 100644 --- a/packages/platform/types/schedules/outputs/update-schedule.output.ts +++ b/apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts @@ -1,23 +1,20 @@ +import { UpdatedScheduleOutput } from "@/ee/schedules/outputs/schedule-updated.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { ScheduleOutput } from "./schedule.output"; - export class UpdateScheduleOutput { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: ScheduleOutput, + type: UpdatedScheduleOutput, }) @IsNotEmptyObject() @ValidateNested() - @Type(() => ScheduleOutput) - data!: ScheduleOutput; - - error?: Error; + @Type(() => UpdatedScheduleOutput) + data!: UpdatedScheduleOutput; } diff --git a/apps/api/v2/src/ee/schedules/schedules.module.ts b/apps/api/v2/src/ee/schedules/schedules.module.ts index 0f9bd4a9d4..3fe53634ee 100644 --- a/apps/api/v2/src/ee/schedules/schedules.module.ts +++ b/apps/api/v2/src/ee/schedules/schedules.module.ts @@ -1,16 +1,15 @@ import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller"; import { SchedulesRepository } from "@/ee/schedules/schedules.repository"; -import { InputSchedulesService } from "@/ee/schedules/services/input-schedules.service"; -import { OutputSchedulesService } from "@/ee/schedules/services/output-schedules.service"; import { SchedulesService } from "@/ee/schedules/services/schedules.service"; +import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, UsersModule, TokensModule], - providers: [SchedulesRepository, SchedulesService, InputSchedulesService, OutputSchedulesService], + imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule], + providers: [SchedulesRepository, SchedulesService], controllers: [SchedulesController], exports: [SchedulesService, SchedulesRepository], }) diff --git a/apps/api/v2/src/ee/schedules/schedules.repository.ts b/apps/api/v2/src/ee/schedules/schedules.repository.ts index a6c4615de4..b447b6b469 100644 --- a/apps/api/v2/src/ee/schedules/schedules.repository.ts +++ b/apps/api/v2/src/ee/schedules/schedules.repository.ts @@ -1,34 +1,19 @@ +import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input"; +import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; -import type { CreateScheduleInput } from "@calcom/platform-types"; - -type InputScheduleAvailabilityTransformed = { - days: number[]; - startTime: Date; - endTime: Date; -}; - -type InputScheduleOverrideTransformed = { - date: Date; - startTime: Date; - endTime: Date; -}; - -type InputScheduleTransformed = Omit & { - availability: InputScheduleAvailabilityTransformed[]; - overrides: InputScheduleOverrideTransformed[]; -}; - @Injectable() export class SchedulesRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - async createSchedule(userId: number, schedule: Omit) { - const { availability, overrides } = schedule; - + async createScheduleWithAvailabilities( + userId: number, + schedule: CreateScheduleInput, + availabilities: CreateAvailabilityInput[] + ) { const createScheduleData: Prisma.ScheduleCreateInput = { user: { connect: { @@ -39,34 +24,17 @@ export class SchedulesRepository { timeZone: schedule.timeZone, }; - const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = []; - - if (availability && availability.length > 0) { - availability.forEach((availability) => { - availabilitiesAndOverrides.push({ - days: availability.days, - startTime: availability.startTime, - endTime: availability.endTime, - userId, - }); - }); - } - - if (overrides && overrides.length > 0) { - overrides.forEach((override) => { - availabilitiesAndOverrides.push({ - date: override.date, - startTime: override.startTime, - endTime: override.endTime, - userId, - }); - }); - } - - if (availabilitiesAndOverrides.length > 0) { + if (availabilities.length > 0) { createScheduleData.availability = { createMany: { - data: availabilitiesAndOverrides, + data: availabilities.map((availability) => { + return { + days: availability.days, + startTime: availability.startTime, + endTime: availability.endTime, + userId, + }; + }), }, }; } @@ -96,87 +64,6 @@ export class SchedulesRepository { return schedule; } - async updateSchedule( - userId: number, - scheduleId: number, - schedule: Partial> - ) { - const { availability, overrides } = schedule; - - const updateScheduleData: Prisma.ScheduleUpdateInput = { - name: schedule.name, - timeZone: schedule.timeZone, - }; - - const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = []; - - const deleteConditions = []; - if (availability) { - // note(Lauris): availabilities and overrides are stored in the same "Availability" table, - // but availabilities have "date" field as null, while overrides have it as not null, so delete - // condition below results in deleting only rows from Availability table that are availabilities. - deleteConditions.push({ - scheduleId: { equals: scheduleId }, - date: null, - }); - } - - if (overrides) { - // note(Lauris): availabilities and overrides are stored in the same "Availability" table, - // but overrides have "date" field as not-null, while availabilities have it as null, so delete - // condition below results in deleting only rows from Availability table that are overrides. - deleteConditions.push({ - scheduleId: { equals: scheduleId }, - NOT: { date: null }, - }); - } - - if (availability && availability.length > 0) { - availability.forEach((availability) => { - availabilitiesAndOverrides.push({ - days: availability.days, - startTime: availability.startTime, - endTime: availability.endTime, - userId, - }); - }); - } - - if (overrides && overrides.length > 0) { - overrides.forEach((override) => { - availabilitiesAndOverrides.push({ - date: override.date, - startTime: override.startTime, - endTime: override.endTime, - userId, - }); - }); - } - - if (availabilitiesAndOverrides.length > 0) { - updateScheduleData.availability = { - deleteMany: deleteConditions, - createMany: { - data: availabilitiesAndOverrides, - }, - }; - } - - const updatedSchedule = await this.dbWrite.prisma.schedule.update({ - where: { - id: scheduleId, - }, - data: { - ...updateScheduleData, - }, - include: { - availability: true, - }, - }); - - return updatedSchedule; - } - async getSchedulesByUserId(userId: number) { const schedules = await this.dbRead.prisma.schedule.findMany({ where: { diff --git a/apps/api/v2/src/ee/schedules/services/input-schedules.service.ts b/apps/api/v2/src/ee/schedules/services/input-schedules.service.ts deleted file mode 100644 index c642e272d7..0000000000 --- a/apps/api/v2/src/ee/schedules/services/input-schedules.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from "@nestjs/common"; - -import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries"; -import { CreateScheduleInput, ScheduleAvailabilityInput } from "@calcom/platform-types"; -import { ScheduleOverrideInput } from "@calcom/platform-types"; - -@Injectable() -export class InputSchedulesService { - transformInputCreateSchedule(inputSchedule: CreateScheduleInput) { - const defaultAvailability: ScheduleAvailabilityInput[] = [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ]; - const defaultOverrides: ScheduleOverrideInput[] = []; - - const availability = this.transformInputScheduleAvailability( - inputSchedule.availability || defaultAvailability - ); - const overrides = this.transformInputOverrides(inputSchedule.overrides || defaultOverrides); - - const internalCreateSchedule = { - ...inputSchedule, - availability, - overrides, - }; - - return internalCreateSchedule; - } - - transformInputScheduleAvailability(inputAvailability: ScheduleAvailabilityInput[]) { - return transformApiScheduleAvailability(inputAvailability); - } - - transformInputOverrides(inputOverrides: ScheduleOverrideInput[]) { - return transformApiScheduleOverrides(inputOverrides); - } -} diff --git a/apps/api/v2/src/ee/schedules/services/output-schedules.service.ts b/apps/api/v2/src/ee/schedules/services/output-schedules.service.ts deleted file mode 100644 index 901644cf7a..0000000000 --- a/apps/api/v2/src/ee/schedules/services/output-schedules.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; -import type { Availability, Schedule } from "@prisma/client"; - -import { WeekDay } from "@calcom/platform-types"; - -@Injectable() -export class OutputSchedulesService { - constructor(private readonly usersRepository: UsersRepository) {} - - async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) { - if (!databaseSchedule.timeZone) { - throw new Error("Failed to create schedule because its timezone is not set."); - } - - const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId( - databaseSchedule.userId - ); - - const createdScheduleAvailabilities = databaseSchedule.availability.filter( - (availability) => !!availability.days.length - ); - const createdScheduleOverrides = databaseSchedule.availability.filter((override) => !!override.date); - - return { - id: databaseSchedule.id, - ownerId: databaseSchedule.userId, - name: databaseSchedule.name, - timeZone: databaseSchedule.timeZone, - availability: createdScheduleAvailabilities.map((availability) => ({ - days: availability.days.map(transformNumberToDay), - startTime: this.padHoursMinutesWithZeros( - availability.startTime.getUTCHours() + ":" + availability.startTime.getUTCMinutes() - ), - endTime: this.padHoursMinutesWithZeros( - availability.endTime.getUTCHours() + ":" + availability.endTime.getUTCMinutes() - ), - })), - isDefault: databaseSchedule.id === ownerDefaultScheduleId, - overrides: createdScheduleOverrides.map((override) => ({ - date: - override.date?.getUTCFullYear() + - "-" + - (override.date ? override.date.getUTCMonth() + 1 : "").toString().padStart(2, "0") + - "-" + - override.date?.getUTCDate().toString().padStart(2, "0"), - startTime: this.padHoursMinutesWithZeros( - override.startTime.getUTCHours() + ":" + override.startTime.getUTCMinutes() - ), - endTime: this.padHoursMinutesWithZeros( - override.endTime.getUTCHours() + ":" + override.endTime.getUTCMinutes() - ), - })), - }; - } - - padHoursMinutesWithZeros(hhMM: string) { - const [hours, minutes] = hhMM.split(":"); - - const formattedHours = hours.padStart(2, "0"); - const formattedMinutes = minutes.padStart(2, "0"); - - return `${formattedHours}:${formattedMinutes}`; - } -} - -function transformNumberToDay(day: number): WeekDay { - const weekMap: { [key: number]: WeekDay } = { - 0: "Sunday", - 1: "Monday", - 2: "Tuesday", - 3: "Wednesday", - 4: "Thursday", - 5: "Friday", - 6: "Saturday", - }; - return weekMap[day]; -} diff --git a/apps/api/v2/src/ee/schedules/services/schedules.service.ts b/apps/api/v2/src/ee/schedules/services/schedules.service.ts index a416d14f69..c91176e422 100644 --- a/apps/api/v2/src/ee/schedules/services/schedules.service.ts +++ b/apps/api/v2/src/ee/schedules/services/schedules.service.ts @@ -1,42 +1,55 @@ +import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input"; +import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output"; import { SchedulesRepository } from "@/ee/schedules/schedules.repository"; -import { InputSchedulesService } from "@/ee/schedules/services/input-schedules.service"; -import { OutputSchedulesService } from "@/ee/schedules/services/output-schedules.service"; -import { UsersRepository } from "@/modules/users/users.repository"; +import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service"; +import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { Schedule } from "@prisma/client"; +import { User } from "@prisma/client"; -import { CreateScheduleInput, ScheduleOutput } from "@calcom/platform-types"; +import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries"; +import { updateScheduleHandler } from "@calcom/platform-libraries"; +import { + transformWorkingHoursForClient, + transformAvailabilityForClient, + transformDateOverridesForClient, +} from "@calcom/platform-libraries"; import { UpdateScheduleInput } from "@calcom/platform-types"; @Injectable() export class SchedulesService { constructor( private readonly schedulesRepository: SchedulesRepository, - private readonly inputSchedulesService: InputSchedulesService, - private readonly outputSchedulesService: OutputSchedulesService, + private readonly availabilitiesService: AvailabilitiesService, private readonly usersRepository: UsersRepository ) {} async createUserDefaultSchedule(userId: number, timeZone: string) { - const defaultSchedule = { + const schedule = { isDefault: true, name: "Default schedule", timeZone, }; - return this.createUserSchedule(userId, defaultSchedule); + return this.createUserSchedule(userId, schedule); } - async createUserSchedule(userId: number, scheduleInput: CreateScheduleInput): Promise { - const schedule = this.inputSchedulesService.transformInputCreateSchedule(scheduleInput); + async createUserSchedule(userId: number, schedule: CreateScheduleInput) { + const availabilities = schedule.availabilities?.length + ? schedule.availabilities + : [this.availabilitiesService.getDefaultAvailabilityInput()]; - const createdSchedule = await this.schedulesRepository.createSchedule(userId, schedule); + const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities( + userId, + schedule, + availabilities + ); if (schedule.isDefault) { await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id); } - return this.outputSchedulesService.getResponseSchedule(createdSchedule); + return createdSchedule; } async getUserScheduleDefault(userId: number) { @@ -44,10 +57,7 @@ export class SchedulesService { if (!user?.defaultScheduleId) return null; - const defaultSchedule = await this.schedulesRepository.getScheduleById(user.defaultScheduleId); - - if (!defaultSchedule) return null; - return this.outputSchedulesService.getResponseSchedule(defaultSchedule); + return this.schedulesRepository.getScheduleById(user.defaultScheduleId); } async getUserSchedule(userId: number, scheduleId: number) { @@ -59,45 +69,38 @@ export class SchedulesService { this.checkUserOwnsSchedule(userId, existingSchedule); - return this.outputSchedulesService.getResponseSchedule(existingSchedule); + return existingSchedule; } async getUserSchedules(userId: number) { - const schedules = await this.schedulesRepository.getSchedulesByUserId(userId); - return Promise.all( - schedules.map(async (schedule) => { - return this.outputSchedulesService.getResponseSchedule(schedule); - }) - ); + return this.schedulesRepository.getSchedulesByUserId(userId); } - async updateUserSchedule(userId: number, scheduleId: number, bodySchedule: UpdateScheduleInput) { + async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) { const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId); if (!existingSchedule) { throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`); } - this.checkUserOwnsSchedule(userId, existingSchedule); + this.checkUserOwnsSchedule(user.id, existingSchedule); - const availability = bodySchedule.availability - ? this.inputSchedulesService.transformInputScheduleAvailability(bodySchedule.availability) - : undefined; - const overrides = bodySchedule.overrides - ? this.inputSchedulesService.transformInputOverrides(bodySchedule.overrides) - : undefined; + const schedule = await this.getUserSchedule(user.id, Number(scheduleId)); + const scheduleFormatted = await this.formatScheduleForAtom(user, schedule); - if (bodySchedule.isDefault) { - await this.usersRepository.setDefaultSchedule(userId, scheduleId); + if (!bodySchedule.schedule) { + // note(Lauris): When updating an availability in cal web app, lets say only its name, also + // the schedule is sent and then passed to the update handler. Notably, availability is passed too + // and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect + // schedule that would be passed by the web app. If we don't, then updating schedule name will erase + // schedule. + bodySchedule.schedule = scheduleFormatted.availability; } - const updatedSchedule = await this.schedulesRepository.updateSchedule(userId, scheduleId, { - ...bodySchedule, - availability, - overrides, + return updateScheduleHandler({ + input: { scheduleId: Number(scheduleId), ...bodySchedule }, + ctx: { user }, }); - - return this.outputSchedulesService.getResponseSchedule(updatedSchedule); } async deleteUserSchedule(userId: number, scheduleId: number) { @@ -112,6 +115,44 @@ export class SchedulesService { return this.schedulesRepository.deleteScheduleById(scheduleId); } + async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise { + const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id); + return this.transformScheduleForAtom(schedule, usersSchedulesCount, user); + } + + async formatSchedulesForAtom( + user: User, + schedules: ScheduleWithAvailabilities[] + ): Promise { + const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id); + return Promise.all( + schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user)) + ); + } + + async transformScheduleForAtom( + schedule: ScheduleWithAvailabilities, + userSchedulesCount: number, + user: Pick + ): Promise { + const timeZone = schedule.timeZone || user.timeZone; + const defaultSchedule = await this.getUserScheduleDefault(user.id); + + return { + id: schedule.id, + name: schedule.name, + isManaged: schedule.userId !== user.id, + workingHours: transformWorkingHoursForClient(schedule), + schedule: schedule.availability, + availability: transformAvailabilityForClient(schedule), + timeZone, + dateOverrides: transformDateOverridesForClient(schedule, timeZone), + isDefault: defaultSchedule?.id === schedule.id, + isLastSchedule: userSchedulesCount <= 1, + readOnly: schedule.userId !== user.id, + }; + } + checkUserOwnsSchedule(userId: number, schedule: Pick) { if (userId !== schedule.userId) { throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`); diff --git a/apps/api/v2/src/modules/availabilities/availabilities.module.ts b/apps/api/v2/src/modules/availabilities/availabilities.module.ts new file mode 100644 index 0000000000..f3fe35bf6a --- /dev/null +++ b/apps/api/v2/src/modules/availabilities/availabilities.module.ts @@ -0,0 +1,10 @@ +import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule], + providers: [AvailabilitiesService], + exports: [AvailabilitiesService], +}) +export class AvailabilitiesModule {} diff --git a/apps/api/v2/src/modules/availabilities/availabilities.service.ts b/apps/api/v2/src/modules/availabilities/availabilities.service.ts new file mode 100644 index 0000000000..fc7dc14ec4 --- /dev/null +++ b/apps/api/v2/src/modules/availabilities/availabilities.service.ts @@ -0,0 +1,16 @@ +import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class AvailabilitiesService { + getDefaultAvailabilityInput(): CreateAvailabilityInput { + const startTime = new Date(new Date().setUTCHours(9, 0, 0, 0)); + const endTime = new Date(new Date().setUTCHours(17, 0, 0, 0)); + + return { + days: [1, 2, 3, 4, 5], + startTime, + endTime, + }; + } +} diff --git a/apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts b/apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts new file mode 100644 index 0000000000..f03698705b --- /dev/null +++ b/apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts @@ -0,0 +1,48 @@ +import { BadRequestException } from "@nestjs/common"; +import { ApiProperty } from "@nestjs/swagger"; +import { Transform, TransformFnParams } from "class-transformer"; +import { IsArray, IsDate, IsNumber } from "class-validator"; + +export class CreateAvailabilityInput { + @IsArray() + @IsNumber({}, { each: true }) + @ApiProperty({ example: [1, 2] }) + days!: number[]; + + @IsDate() + @Transform(({ value, key }: TransformFnParams) => transformStringToDate(value, key)) + startTime!: Date; + + @IsDate() + @Transform(({ value, key }: TransformFnParams) => transformStringToDate(value, key)) + endTime!: Date; +} + +function transformStringToDate(value: string, key: string): Date { + // note(Lauris): incoming value is ISO8061 e.g. 2025-0412T13:17:56.324Z + const dateTimeParts = value.split("T"); + + const timePart = dateTimeParts[1].split(".")[0]; // Removes milliseconds + const parts = timePart.split(":"); + + if (parts.length !== 3) { + throw new BadRequestException( + `Invalid time format. Expected format(ISO8061): 2025-0412T13:17:56.324Z. Received: ${value}` + ); + } + const [hours, minutes, seconds] = parts.map(Number); + + if (hours < 0 || hours > 23) { + throw new BadRequestException(`Invalid ${key} hours. Expected value between 0 and 23`); + } + + if (minutes < 0 || minutes > 59) { + throw new BadRequestException(`Invalid ${key} minutes. Expected value between 0 and 59`); + } + + if (seconds < 0 || seconds > 59) { + throw new BadRequestException(`Invalid ${key} seconds. Expected value between 0 and 59`); + } + + return new Date(new Date().setUTCHours(hours, minutes, seconds, 0)); +} diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index 1423903b45..12345efa5f 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -21,6 +21,7 @@ import { HttpStatus, Param, Patch, + BadRequestException, Delete, Query, NotFoundException, diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index cd77469035..e03429b192 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -173,14 +173,6 @@ export class UsersRepository { }, }); } - - async getUserScheduleDefaultId(userId: number) { - const user = await this.findById(userId); - - if (!user?.defaultScheduleId) return null; - - return user?.defaultScheduleId; - } } function capitalizeTimezone(timezone: string) { diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index ba7fa77e21..03fb4cc1e9 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2883,26 +2883,26 @@ "data" ] }, - "ScheduleAvailabilityInput": { + "CreateAvailabilityInput": { "type": "object", "properties": { "days": { "example": [ - "Monday", - "Tuesday" + 1, + 2 ], "type": "array", "items": { - "type": "string" + "type": "number" } }, "startTime": { - "type": "string", - "example": "09:00" + "format": "date-time", + "type": "string" }, "endTime": { - "type": "string", - "example": "10:00" + "format": "date-time", + "type": "string" } }, "required": [ @@ -2911,71 +2911,23 @@ "endTime" ] }, - "ScheduleOverrideInput": { - "type": "object", - "properties": { - "date": { - "type": "string", - "example": "2024-05-20" - }, - "startTime": { - "type": "string", - "example": "12:00" - }, - "endTime": { - "type": "string", - "example": "13:00" - } - }, - "required": [ - "date", - "startTime", - "endTime" - ] - }, "CreateScheduleInput": { "type": "object", "properties": { "name": { - "type": "string", - "example": "One-on-one coaching" + "type": "string" }, "timeZone": { - "type": "string", - "example": "Europe/Rome" + "type": "string" }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], + "availabilities": { "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput" + "$ref": "#/components/schemas/CreateAvailabilityInput" } }, "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput" - } + "type": "boolean" } }, "required": [ @@ -2984,67 +2936,163 @@ "isDefault" ] }, - "ScheduleOutput": { + "WorkingHours": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "type": "number" + }, + "endTime": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "AvailabilityModel": { "type": "object", "properties": { "id": { + "type": "number" + }, + "userId": { "type": "number", - "example": 254 + "nullable": true }, - "ownerId": { + "eventTypeId": { "type": "number", - "example": 478 + "nullable": true }, - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], + "days": { "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput" + "type": "number" } }, - "isDefault": { - "type": "boolean", - "example": true + "startTime": { + "format": "date-time", + "type": "string" }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "13:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput" - } + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "days", + "startTime", + "endTime" + ] + }, + "TimeRange": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "nullable": true + }, + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "ScheduleOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "isManaged": { + "type": "boolean" + }, + "workingHours": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkingHours" + } + }, + "schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel" + } + }, + "availability": { + "type": "array", + "items": { + "required": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeRange" + } + } + }, + "timeZone": { + "type": "string" + }, + "dateOverrides": { + "type": "array", + "items": { + "type": "object" + } + }, + "isDefault": { + "type": "boolean" + }, + "isLastSchedule": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" } }, "required": [ "id", - "ownerId", "name", - "timeZone", + "isManaged", + "workingHours", + "schedule", "availability", + "timeZone", + "dateOverrides", "isDefault", - "overrides" + "isLastSchedule", + "readOnly" ] }, "CreateScheduleOutput": { @@ -3079,7 +3127,12 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput" + } + ] } }, "required": [ @@ -3099,15 +3152,7 @@ ] }, "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/ScheduleOutput" - } - ] - }, - "error": { - "type": "object" + "$ref": "#/components/schemas/ScheduleOutput" } }, "required": [ @@ -3127,13 +3172,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOutput" - } - }, - "error": { - "type": "object" + "$ref": "#/components/schemas/ScheduleOutput" } }, "required": [ @@ -3144,48 +3183,138 @@ "UpdateScheduleInput": { "type": "object", "properties": { + "timeZone": { + "type": "string" + }, "name": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "schedule": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + }, + "dateOverrides": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + } + }, + "required": [ + "timeZone", + "name", + "isDefault", + "schedule" + ] + }, + "EventTypeModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "eventName": { "type": "string", - "example": "One-on-one coaching" + "nullable": true + } + }, + "required": [ + "id" + ] + }, + "ScheduleModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "name": { + "type": "string" }, "timeZone": { "type": "string", - "example": "Europe/Rome" + "nullable": true + }, + "eventType": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeModel" + } }, "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput" + "$ref": "#/components/schemas/AvailabilityModel" } } - } + }, + "required": [ + "id", + "userId", + "name" + ] + }, + "UpdatedScheduleOutput": { + "type": "object", + "properties": { + "schedule": { + "$ref": "#/components/schemas/ScheduleModel" + }, + "isDefault": { + "type": "boolean" + }, + "timeZone": { + "type": "string" + }, + "prevDefaultId": { + "type": "number", + "nullable": true + }, + "currentDefaultId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "schedule", + "isDefault" + ] }, "UpdateScheduleOutput": { "type": "object", @@ -3199,10 +3328,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" - }, - "error": { - "type": "object" + "$ref": "#/components/schemas/UpdatedScheduleOutput" } }, "required": [ diff --git a/packages/lib/availability.ts b/packages/lib/availability.ts index b604e07742..c2763786a6 100644 --- a/packages/lib/availability.ts +++ b/packages/lib/availability.ts @@ -128,10 +128,10 @@ export function getWorkingHours( } export function availabilityAsString( - availability: Pick, + availability: Availability, { locale, hour12 }: { locale?: string; hour12?: boolean } ) { - const weekSpan = (availability: Pick) => { + const weekSpan = (availability: Availability) => { const days = availability.days.slice(1).reduce( (days, day) => { if (days[days.length - 1].length === 1 && days[days.length - 1][0] === day - 1) { @@ -153,7 +153,7 @@ export function availabilityAsString( .join(", "); }; - const timeSpan = (availability: Pick) => { + const timeSpan = (availability: Availability) => { return `${new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric", hour12 }).format( new Date(availability.startTime.toISOString().slice(0, -1)) )} - ${new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric", hour12 }).format( diff --git a/packages/lib/schedules/transformers/for-atom.ts b/packages/lib/schedules/client/transformers.ts similarity index 68% rename from packages/lib/schedules/transformers/for-atom.ts rename to packages/lib/schedules/client/transformers.ts index 7695f37ef1..c3afe15999 100644 --- a/packages/lib/schedules/transformers/for-atom.ts +++ b/packages/lib/schedules/client/transformers.ts @@ -1,25 +1,33 @@ -import type { Availability } from "@prisma/client"; +import type { Availability as AvailabilityModel, Schedule as ScheduleModel } from "@prisma/client"; import dayjs from "@calcom/dayjs"; import { getWorkingHours } from "@calcom/lib/availability"; import { yyyymmdd } from "@calcom/lib/date-fns"; import type { Schedule, TimeRange } from "@calcom/types/schedule"; -type ScheduleAvailability = Pick[]; -type ScheduleOverride = Pick[]; +export type ScheduleWithAvailabilities = ScheduleModel & { availability: AvailabilityModel[] }; -export function transformWorkingHoursForAtom(schedule: { - timeZone: string | null; - availability: ScheduleAvailability; -}) { +export type ScheduleWithAvailabilitiesForWeb = Pick & { + isManaged: boolean; + workingHours: ReturnType; + schedule: AvailabilityModel[]; + availability: ReturnType; + timeZone: string; + dateOverrides: ReturnType; + isDefault: boolean; + isLastSchedule: boolean; + readOnly: boolean; +}; + +export function transformWorkingHoursForClient(schedule: ScheduleWithAvailabilities) { return getWorkingHours( { timeZone: schedule.timeZone || undefined, utcOffset: 0 }, schedule.availability || [] ); } -export function transformAvailabilityForAtom(schedule: { availability: ScheduleAvailability }) { - return transformScheduleToAvailabilityForAtom(schedule).map((a) => +export function transformAvailabilityForClient(schedule: ScheduleWithAvailabilities) { + return transformScheduleToAvailabilityForClient(schedule).map((a) => a.map((startAndEnd) => ({ ...startAndEnd, end: new Date(startAndEnd.end.toISOString().replace("23:59:00.000Z", "23:59:59.999Z")), @@ -27,10 +35,7 @@ export function transformAvailabilityForAtom(schedule: { availability: ScheduleA ); } -export function transformDateOverridesForAtom( - schedule: { availability: ScheduleOverride }, - timeZone: string -) { +export function transformDateOverridesForClient(schedule: ScheduleWithAvailabilities, timeZone: string) { return schedule.availability.reduce((acc, override) => { // only iff future date override if (!override.date || dayjs.tz(override.date, timeZone).isBefore(dayjs(), "day")) { @@ -62,7 +67,9 @@ export function transformDateOverridesForAtom( }, [] as { ranges: TimeRange[] }[]); } -export const transformScheduleToAvailabilityForAtom = (schedule: { availability: ScheduleAvailability }) => { +export const transformScheduleToAvailabilityForClient = ( + schedule: Partial & { availability: AvailabilityModel[] } +) => { return schedule.availability.reduce( (schedule: Schedule, availability) => { availability.days.forEach((day) => { diff --git a/packages/lib/schedules/index.ts b/packages/lib/schedules/index.ts index a8401e8101..abca07a1d6 100644 --- a/packages/lib/schedules/index.ts +++ b/packages/lib/schedules/index.ts @@ -1 +1 @@ -export * from "./transformers"; +export * from "./client/transformers"; diff --git a/packages/lib/schedules/transformers/from-api.ts b/packages/lib/schedules/transformers/from-api.ts deleted file mode 100644 index 6e428b0f44..0000000000 --- a/packages/lib/schedules/transformers/from-api.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ScheduleAvailabilityInput } from "@calcom/platform-types"; -import type { ScheduleOverrideInput } from "@calcom/platform-types"; -import type { WeekDay } from "@calcom/platform-types"; - -export function transformApiScheduleAvailability(inputAvailability: ScheduleAvailabilityInput[]) { - return inputAvailability.map((availability) => ({ - days: availability.days.map(transformDayToNumber), - startTime: createDateFromHoursMinutes(availability.startTime), - endTime: createDateFromHoursMinutes(availability.endTime), - })); -} - -export function transformApiScheduleOverrides(inputOverrides: ScheduleOverrideInput[]) { - return inputOverrides.map((override) => ({ - date: new Date(override.date), - startTime: createDateFromHoursMinutes(override.startTime), - endTime: createDateFromHoursMinutes(override.endTime), - })); -} - -function createDateFromHoursMinutes(hoursMinutes: string): Date { - const parts = hoursMinutes.split(":"); - - if (parts.length < 2) { - throw new Error("Invalid time format. Please use 'hh:mm'."); - } - - const hours = parseInt(parts[0], 10); - const minutes = parseInt(parts[1], 10); - - if (isNaN(hours) || hours < 0 || hours > 23) { - throw new Error("Hours must be a number between 0 and 23."); - } - if (isNaN(minutes) || minutes < 0 || minutes > 59) { - throw new Error("Minutes must be a number between 0 and 59."); - } - - const utcDate = new Date(Date.UTC(1970, 0, 1, hours, minutes)); - - return utcDate; -} - -function transformDayToNumber(day: WeekDay): number { - const weekMap: { [key in WeekDay]: number } = { - Sunday: 0, - Monday: 1, - Tuesday: 2, - Wednesday: 3, - Thursday: 4, - Friday: 5, - Saturday: 6, - }; - return weekMap[day]; -} diff --git a/packages/lib/schedules/transformers/index.ts b/packages/lib/schedules/transformers/index.ts deleted file mode 100644 index 832691070b..0000000000 --- a/packages/lib/schedules/transformers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./for-atom"; -export * from "./from-api"; diff --git a/packages/platform/atoms/availability/AvailabilitySettings.tsx b/packages/platform/atoms/availability/AvailabilitySettings.tsx index a33606905c..3954c15ce0 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -62,8 +62,6 @@ export type CustomClassNames = { }; }; -export type Availability = Pick; - type AvailabilitySettingsProps = { skeletonLabel?: string; schedule: { @@ -75,7 +73,7 @@ type AvailabilitySettingsProps = { workingHours: WorkingHours[]; dateOverrides: { ranges: TimeRange[] }[]; timeZone: string; - schedule: Availability[]; + schedule: Schedule[]; }; travelSchedules?: RouterOutputs["viewer"]["getTravelSchedules"]; handleDelete: () => void; @@ -276,9 +274,7 @@ export function AvailabilitySettings({ schedule.schedule .filter((availability) => !!availability.days.length) .map((availability) => ( - + {availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
diff --git a/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts b/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts deleted file mode 100644 index 5074238db6..0000000000 --- a/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { ScheduleOutput } from "@calcom/platform-types"; -import type { User } from "@calcom/prisma/client"; - -import { transformApiScheduleForAtom } from "./transformApiScheduleForAtom"; - -const SCHEDULE_OWNER_ID = 256; - -describe("transformScheduleForAtom", () => { - const user: Pick = { - id: SCHEDULE_OWNER_ID, - defaultScheduleId: 139, - timeZone: "America/New_York", - }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it("should return null if user is not provided", () => { - const schedule: ScheduleOutput | null = { - id: 139, - ownerId: SCHEDULE_OWNER_ID, - name: "Default", - timeZone: "America/Cancun", - availability: [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ], - isDefault: true, - overrides: [], - }; - - expect(transformApiScheduleForAtom(undefined, schedule, 1)).toBeNull(); - }); - - it("should return null if schedule is not provided", () => { - expect(transformApiScheduleForAtom(user, null, 1)).toBeNull(); - }); - - it("should transform schedule correctly", () => { - const schedule: ScheduleOutput = { - id: 139, - ownerId: SCHEDULE_OWNER_ID, - name: "Default", - timeZone: "America/Cancun", - availability: [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ], - isDefault: true, - overrides: [], - }; - - const expectedResult = { - id: 139, - name: "Default", - isManaged: false, - workingHours: [ - { - days: [1, 2, 3, 4, 5], - startTime: 540, - endTime: 1020, - userId: 256, - }, - ], - schedule: [ - { - userId: 256, - scheduleId: 139, - days: [1, 2, 3, 4, 5], - startTime: new Date(Date.UTC(1970, 0, 1, 9, 0)), - endTime: new Date(Date.UTC(1970, 0, 1, 17, 0)), - date: null, - }, - ], - availability: [ - [], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [], - ], - timeZone: "America/Cancun", - dateOverrides: [], - isDefault: true, - isLastSchedule: true, - readOnly: false, - }; - - const result = transformApiScheduleForAtom(user, schedule, 1); - - expect(result).toEqual(expectedResult); - }); -}); diff --git a/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts b/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts deleted file mode 100644 index c3aa3a62a3..0000000000 --- a/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - transformAvailabilityForAtom, - transformDateOverridesForAtom, - transformApiScheduleAvailability, - transformApiScheduleOverrides, - transformWorkingHoursForAtom, -} from "@calcom/lib/schedules/transformers"; -import type { ScheduleOutput } from "@calcom/platform-types"; -import type { User } from "@calcom/prisma/client"; - -export function transformApiScheduleForAtom( - user: Pick | undefined, - schedule: ScheduleOutput | null | undefined, - userSchedulesCount: number -) { - if (!user || !schedule) { - return null; - } - - const transformedSchedule = { - ...schedule, - availability: transformApiScheduleAvailability(schedule.availability), - overrides: transformApiScheduleOverrides(schedule.overrides), - }; - - const combined = [...transformedSchedule.availability, ...transformedSchedule.overrides]; - const availability = combined.map((entry) => { - return { - ...entry, - userId: schedule.ownerId, - scheduleId: schedule.id, - days: "days" in entry ? entry.days : [], - date: "date" in entry ? entry.date : null, - }; - }); - - const atomSchedule = { - ...schedule, - availability, - userId: schedule.ownerId, - }; - - const timeZone = schedule.timeZone || user.timeZone; - const defaultScheduleId = user.defaultScheduleId; - - return { - id: schedule.id, - name: schedule.name, - isManaged: schedule.ownerId !== user.id, - workingHours: transformWorkingHoursForAtom(atomSchedule), - schedule: availability, - availability: transformAvailabilityForAtom(atomSchedule), - timeZone, - dateOverrides: transformDateOverridesForAtom(atomSchedule, timeZone), - isDefault: defaultScheduleId === schedule.id, - isLastSchedule: userSchedulesCount <= 1, - readOnly: schedule.ownerId !== user.id, - }; -} diff --git a/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts b/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts deleted file mode 100644 index d9876ca4ba..0000000000 --- a/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { UpdateScheduleInput } from "@calcom/platform-types"; - -import type { AvailabilityFormValues } from "../types"; -import { transformAtomScheduleForApi } from "./transformAtomScheduleForApi"; - -describe("transformAtomScheduleForApi", () => { - it("should transform atom schedule correctly to API format", () => { - const input: AvailabilityFormValues = { - name: "Default", - schedule: [ - [], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T09:00:00.000Z"), - end: new Date("2024-05-14T17:00:00.000Z"), - }, - ], - [ - { - start: new Date("2024-05-14T11:00:00.000Z"), - end: new Date("2024-05-14T12:00:00.000Z"), - }, - ], - [], - ], - dateOverrides: [], - timeZone: "America/Cancun", - isDefault: true, - }; - - const expectedOutput: UpdateScheduleInput = { - name: "Default", - timeZone: "America/Cancun", - isDefault: true, - availability: [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday"], - startTime: "09:00", - endTime: "17:00", - }, - { - days: ["Friday"], - startTime: "11:00", - endTime: "12:00", - }, - ], - overrides: [], - }; - - const result = transformAtomScheduleForApi(input); - - expect(result).toEqual(expectedOutput); - }); -}); diff --git a/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts b/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts deleted file mode 100644 index f912f687da..0000000000 --- a/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ScheduleAvailabilityInput, UpdateScheduleInput, WeekDay } from "@calcom/platform-types"; - -import type { AvailabilityFormValues } from "../types"; - -export function transformAtomScheduleForApi(body: AvailabilityFormValues): UpdateScheduleInput { - const { name, schedule, dateOverrides, timeZone, isDefault } = body; - - const overrides = - dateOverrides.flatMap( - (dateOverridesRanges) => - dateOverridesRanges?.ranges?.map((range) => ({ - date: `${range.start.getUTCFullYear}-${range.start.getUTCMonth}-${range.start.getUTCDate}`, - startTime: `${range.start.getUTCHours}-${range.start.getUTCMinutes}`, - endTime: `${range.end.getUTCHours}-${range.end.getUTCMinutes}`, - })) ?? [] - ) ?? []; - - const availability = formatScheduleTime(schedule); - - return { name, timeZone, isDefault, availability, overrides }; -} - -function formatScheduleTime( - weekSchedule: AvailabilityFormValues["schedule"] -): UpdateScheduleInput["availability"] { - const daysOfWeek: WeekDay[] = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ]; - - const formattedSchedule = weekSchedule.map((daySchedule, index) => - daySchedule.map((event) => ({ - startTime: convertToHHMM(event.start.toISOString()), - endTime: convertToHHMM(event.end.toISOString()), - days: [daysOfWeek[index]], - })) - ); - - const timeMap: { [key: string]: ScheduleAvailabilityInput } = {}; - - formattedSchedule.flat().forEach((event) => { - const timeKey = `${event.startTime}-${event.endTime}`; - if (!timeMap[timeKey]) { - timeMap[timeKey] = { startTime: event.startTime, endTime: event.endTime, days: [] }; - } - timeMap[timeKey].days.push(...event.days); - }); - - return Object.values(timeMap); -} - -function convertToHHMM(isoDate: string): string { - const date = new Date(isoDate); - const hours = date.getUTCHours().toString().padStart(2, "0"); - const minutes = date.getUTCMinutes().toString().padStart(2, "0"); - return `${hours}:${minutes}`; -} diff --git a/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx b/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx index 8aeaff53c6..7595836584 100644 --- a/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx +++ b/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx @@ -1,18 +1,16 @@ import type { ScheduleLabelsType } from "@calcom/features/schedules/components/Schedule"; -import type { ApiErrorResponse, ApiResponse, ScheduleOutput } from "@calcom/platform-types"; +import type { ScheduleWithAvailabilitiesForWeb, UpdateScheduleOutputType } from "@calcom/platform-libraries"; +import type { ApiErrorResponse, ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; -import useDeleteSchedule from "../../hooks/schedules/useDeleteSchedule"; -import { useSchedule } from "../../hooks/schedules/useSchedule"; -import { useSchedules } from "../../hooks/schedules/useSchedules"; -import useUpdateSchedule from "../../hooks/schedules/useUpdateSchedule"; +import useClientSchedule from "../../hooks/useClientSchedule"; +import useDeleteSchedule from "../../hooks/useDeleteSchedule"; import { useMe } from "../../hooks/useMe"; +import useUpdateSchedule from "../../hooks/useUpdateSchedule"; import { AtomsWrapper } from "../../src/components/atoms-wrapper"; import { useToast } from "../../src/components/ui/use-toast"; -import type { Availability } from "../AvailabilitySettings"; +import type { Schedule } from "../AvailabilitySettings"; import type { CustomClassNames } from "../AvailabilitySettings"; import { AvailabilitySettings } from "../AvailabilitySettings"; -import { transformApiScheduleForAtom } from "../atom-api-transformers/transformApiScheduleForAtom"; -import { transformAtomScheduleForApi } from "../atom-api-transformers/transformAtomScheduleForApi"; import type { AvailabilityFormValues } from "../types"; type PlatformAvailabilitySettingsWrapperProps = { @@ -21,7 +19,7 @@ type PlatformAvailabilitySettingsWrapperProps = { tooltips: Partial; }; customClassNames?: Partial; - onUpdateSuccess?: (res: ApiResponse) => void; + onUpdateSuccess?: (res: ApiResponse) => void; onUpdateError?: (err: ApiErrorResponse) => void; onDeleteSuccess?: (res: ApiResponse) => void; onDeleteError?: (err: ApiErrorResponse) => void; @@ -37,10 +35,10 @@ export const PlatformAvailabilitySettingsWrapper = ({ onUpdateSuccess, disableEditableHeading = false, }: PlatformAvailabilitySettingsWrapperProps) => { - const { isLoading, data: schedule } = useSchedule(id); - const { data: schedules } = useSchedules(); + const { isLoading, data: schedule } = useClientSchedule(id); + const mySchedule = schedule as ApiSuccessResponse; const { data: me } = useMe(); - const atomSchedule = transformApiScheduleForAtom(me?.data, schedule, schedules?.length || 0); + const userSchedule = mySchedule?.data; const { timeFormat } = me?.data || { timeFormat: null }; const { toast } = useToast(); @@ -79,43 +77,48 @@ export const PlatformAvailabilitySettingsWrapper = ({ }; const handleUpdate = async (id: number, body: AvailabilityFormValues) => { - const updateBody = transformAtomScheduleForApi(body); - updateSchedule({ id, ...updateBody }); + const transformedDateOverrides = + body.dateOverrides.flatMap( + (dateOverridesRanges) => + dateOverridesRanges?.ranges?.map((range) => ({ start: range.start, end: range.end })) ?? [] + ) ?? []; + + await updateSchedule({ ...body, scheduleId: id, dateOverrides: transformedDateOverrides }); }; if (isLoading) return
Loading...
; - if (!atomSchedule) return
No user schedule present
; + if (!userSchedule) return
No user schedule present
; return ( { - atomSchedule.id && handleDelete(atomSchedule.id); + userSchedule.id && handleDelete(userSchedule.id); }} handleSubmit={async (data) => { - atomSchedule.id && handleUpdate(atomSchedule.id, data); + userSchedule.id && handleUpdate(userSchedule.id, data); }} weekStart="Sunday" timeFormat={timeFormat} isLoading={isLoading} schedule={{ - name: atomSchedule.name, - id: atomSchedule.id, - isLastSchedule: atomSchedule.isLastSchedule, - isDefault: atomSchedule.isDefault, - workingHours: atomSchedule.workingHours, - dateOverrides: atomSchedule.dateOverrides, - timeZone: atomSchedule.timeZone, - availability: atomSchedule.availability, + name: userSchedule.name, + id: userSchedule.id, + isLastSchedule: userSchedule.isLastSchedule, + isDefault: userSchedule.isDefault, + workingHours: userSchedule.workingHours, + dateOverrides: userSchedule.dateOverrides, + timeZone: userSchedule.timeZone, + availability: userSchedule.availability, schedule: - atomSchedule.schedule.reduce( - (acc: Availability[], avail: Availability) => [ + userSchedule.schedule.reduce( + (acc: Schedule[], avail: Schedule) => [ ...acc, - { days: avail.days, startTime: new Date(avail.startTime), endTime: new Date(avail.endTime) }, + { ...avail, startTime: new Date(avail.startTime), endTime: new Date(avail.endTime) }, ], - [] + [] as Schedule[] ) || [], }} isDeleting={isDeletionInProgress} diff --git a/packages/platform/atoms/hooks/schedules/useSchedule.ts b/packages/platform/atoms/hooks/schedules/useSchedule.ts deleted file mode 100644 index df93bf6f31..0000000000 --- a/packages/platform/atoms/hooks/schedules/useSchedule.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { GetScheduleOutput } from "@calcom/platform-types"; - -import http from "../../lib/http"; - -export const QUERY_KEY = "user-schedule"; - -export const useSchedule = (id?: string) => { - const pathname = id ? `/${V2_ENDPOINTS.availability}/${id}` : `/${V2_ENDPOINTS.availability}/default`; - - const { isLoading, error, data } = useQuery({ - queryKey: [QUERY_KEY, id], - queryFn: () => { - return http.get(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return res.data.data; - } - throw new Error(res.data.error?.message); - }); - }, - }); - - return { isLoading, error, data }; -}; diff --git a/packages/platform/atoms/hooks/schedules/useSchedules.ts b/packages/platform/atoms/hooks/schedules/useSchedules.ts deleted file mode 100644 index f56c6f3aea..0000000000 --- a/packages/platform/atoms/hooks/schedules/useSchedules.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { GetSchedulesOutput } from "@calcom/platform-types"; - -import http from "../../lib/http"; - -export const QUERY_KEY = "user-schedules"; - -export const useSchedules = () => { - const pathname = `/${V2_ENDPOINTS.availability}`; - - const { isLoading, error, data } = useQuery({ - queryKey: [QUERY_KEY], - queryFn: () => { - return http.get(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return res.data.data; - } - throw new Error(res.data.error?.message); - }); - }, - }); - - return { isLoading, error, data }; -}; diff --git a/packages/platform/atoms/hooks/useClientSchedule.ts b/packages/platform/atoms/hooks/useClientSchedule.ts new file mode 100644 index 0000000000..edc950942b --- /dev/null +++ b/packages/platform/atoms/hooks/useClientSchedule.ts @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; + +import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries"; +import type { ApiResponse } from "@calcom/platform-types"; + +import http from "../lib/http"; + +export const QUERY_KEY = "user-schedule"; + +const useClientSchedule = (id?: string) => { + const pathname = id + ? `/${V2_ENDPOINTS.availability}/${id}?for=atom` + : `/${V2_ENDPOINTS.availability}/default?for=atom`; + + const { isLoading, error, data } = useQuery({ + queryKey: [QUERY_KEY, id], + queryFn: () => { + return http.get>(pathname).then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return res.data; + } + throw new Error(res.data.error.message); + }); + }, + }); + + return { isLoading, error, data }; +}; + +export default useClientSchedule; diff --git a/packages/platform/atoms/hooks/schedules/useDeleteSchedule.ts b/packages/platform/atoms/hooks/useDeleteSchedule.ts similarity index 94% rename from packages/platform/atoms/hooks/schedules/useDeleteSchedule.ts rename to packages/platform/atoms/hooks/useDeleteSchedule.ts index f476d68541..0322dcbb31 100644 --- a/packages/platform/atoms/hooks/schedules/useDeleteSchedule.ts +++ b/packages/platform/atoms/hooks/useDeleteSchedule.ts @@ -4,8 +4,8 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants"; import { V2_ENDPOINTS } from "@calcom/platform-constants"; import type { ApiResponse, ApiErrorResponse } from "@calcom/platform-types"; -import http from "../../lib/http"; -import { QUERY_KEY } from "./useSchedule"; +import http from "../lib/http"; +import { QUERY_KEY } from "./useClientSchedule"; interface IUseDeleteScheduleProps { onSuccess?: (res: ApiResponse) => void; diff --git a/packages/platform/atoms/hooks/schedules/useUpdateSchedule.ts b/packages/platform/atoms/hooks/useUpdateSchedule.ts similarity index 64% rename from packages/platform/atoms/hooks/schedules/useUpdateSchedule.ts rename to packages/platform/atoms/hooks/useUpdateSchedule.ts index f550cce165..dcc953135d 100644 --- a/packages/platform/atoms/hooks/schedules/useUpdateSchedule.ts +++ b/packages/platform/atoms/hooks/useUpdateSchedule.ts @@ -2,14 +2,14 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; import { V2_ENDPOINTS } from "@calcom/platform-constants"; +import type { UpdateScheduleOutputType } from "@calcom/platform-libraries"; import type { ApiResponse, UpdateScheduleInput, ApiErrorResponse } from "@calcom/platform-types"; -import type { ScheduleOutput } from "@calcom/platform-types"; -import http from "../../lib/http"; -import { QUERY_KEY as ScheduleQueryKey } from "./useSchedule"; +import http from "../lib/http"; +import { QUERY_KEY as ScheduleQueryKey } from "./useClientSchedule"; interface IPUpdateOAuthClient { - onSuccess?: (res: ApiResponse) => void; + onSuccess?: (res: ApiResponse) => void; onError?: (err: ApiErrorResponse) => void; } @@ -25,11 +25,15 @@ const useUpdateSchedule = ( ) => { const queryClient = useQueryClient(); - const mutation = useMutation, unknown, UpdateScheduleInput & { id: number }>({ + const mutation = useMutation< + ApiResponse, + unknown, + UpdateScheduleInput & { scheduleId: number } + >({ mutationFn: (data) => { - const pathname = `/${V2_ENDPOINTS.availability}/${data.id}?for=atom`; + const pathname = `/${V2_ENDPOINTS.availability}/${data.scheduleId}?for=atom`; - return http.patch>(pathname, data).then((res) => { + return http.patch>(pathname, data).then((res) => { return res.data; }); }, diff --git a/packages/platform/atoms/jest.config.json b/packages/platform/atoms/jest.config.json deleted file mode 100644 index 53b778bff5..0000000000 --- a/packages/platform/atoms/jest.config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.ts$": "ts-jest" - } -} diff --git a/packages/platform/atoms/package.json b/packages/platform/atoms/package.json index 10bf456877..2d8f76c60a 100644 --- a/packages/platform/atoms/package.json +++ b/packages/platform/atoms/package.json @@ -8,8 +8,7 @@ "scripts": { "dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch", "build": "yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client", - "publish": "rm -rf dist && yarn build && npm publish --access public", - "test": "jest" + "publish": "rm -rf dist && yarn build && npm publish --access public" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", @@ -53,15 +52,12 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "@tanstack/react-query": "^5.17.15", - "@types/jest": "^29.5.12", "class-variance-authority": "^0.4.0", "clsx": "^2.0.0", - "jest": "^29.7.0", "lucide-react": "^0.364.0", "react-use": "^17.4.2", "tailwind-merge": "^1.13.2", "tailwindcss": "^3.3.3", - "tailwindcss-animate": "^1.0.6", - "ts-jest": "^29.1.2" + "tailwindcss-animate": "^1.0.6" } } diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 6a57be023c..a2e91c469e 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -48,12 +48,14 @@ export type { ConnectedDestinationCalendars } from "@calcom/lib/getConnectedDest export { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; export { - transformWorkingHoursForAtom, - transformAvailabilityForAtom, - transformDateOverridesForAtom, - transformApiScheduleAvailability, - transformApiScheduleOverrides, -} from "@calcom/lib/schedules/transformers"; + transformWorkingHoursForClient, + transformAvailabilityForClient, + transformDateOverridesForClient, +} from "@calcom/lib/schedules/client/transformers"; +export type { + ScheduleWithAvailabilities, + ScheduleWithAvailabilitiesForWeb, +} from "@calcom/lib/schedules/client/transformers"; export type { BookingCreateBody, BookingResponse, diff --git a/packages/platform/types/schedules.ts b/packages/platform/types/schedules.ts new file mode 100644 index 0000000000..f9fe1578ed --- /dev/null +++ b/packages/platform/types/schedules.ts @@ -0,0 +1,115 @@ +import { ApiProperty as DocsProperty, ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsString, IsBoolean, IsOptional, ValidateNested, IsArray, IsDate } from "class-validator"; +import { DateTime } from "luxon"; +import { z } from "zod"; + +const scheduleSchema = z.object({ + id: z.number().int(), + userId: z.number().int(), + name: z.string(), + timeZone: z.string().nullish(), +}); + +const availabilitySchema = z.object({ + id: z.number().int(), + days: z.number().int().array(), + startTime: z.date(), + endTime: z.date(), +}); + +export const schemaScheduleResponse = z + .object({}) + .merge(scheduleSchema) + .merge( + z.object({ + availability: z + .array(availabilitySchema) + .transform((availabilities) => + availabilities.map((availability) => ({ + ...availability, + startTime: DateTime.fromJSDate(availability.startTime).toUTC().toFormat("HH:mm:ss"), + endTime: DateTime.fromJSDate(availability.endTime).toUTC().toFormat("HH:mm:ss"), + })) + ) + .optional(), + }) + ); + +export type ScheduleResponse = z.infer; + +class ScheduleItem { + @IsString() + start!: Date; + + @IsString() + end!: Date; +} + +class DateOverride { + @IsDate() + @Type(() => Date) + start!: Date; + + @IsDate() + @Type(() => Date) + end!: Date; +} + +export class UpdateScheduleInput { + @IsString() + @IsOptional() + @DocsProperty() + timeZone?: string; + + @IsString() + @IsOptional() + @DocsProperty() + name?: string; + + @IsBoolean() + @IsOptional() + @DocsProperty() + isDefault?: boolean; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => ScheduleItem) + @DocsProperty() + @IsArray() + @ApiProperty({ + type: [[ScheduleItem]], + example: [ + [], + [{ start: "2022-01-01T00:00:00.000Z", end: "2022-01-02T00:00:00.000Z" }], + [], + [], + [], + [], + [], + ], + isArray: true, + }) + schedule?: ScheduleItem[][]; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => DateOverride) + @IsArray() + @DocsProperty() + @ApiProperty({ + type: [DateOverride], + example: [ + [], + [{ start: "2022-01-01T00:00:00.000Z", end: "2022-01-02T00:00:00.000Z" }], + [], + [], + [], + [], + [], + ], + isArray: true, + required: false, + }) + dateOverrides?: DateOverride[]; +} diff --git a/packages/platform/types/schedules/constants.ts b/packages/platform/types/schedules/constants.ts deleted file mode 100644 index 23ce20f2f4..0000000000 --- a/packages/platform/types/schedules/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const WEEK_DAYS = [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", -] as const; - -export type WeekDay = (typeof WEEK_DAYS)[number]; - -export const TIME_FORMAT_HH_MM = /^([01]\d|2[0-3]):([0-5]\d)$/; diff --git a/packages/platform/types/schedules/index.ts b/packages/platform/types/schedules/index.ts deleted file mode 100644 index 2d351e9bd3..0000000000 --- a/packages/platform/types/schedules/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./inputs"; -export * from "./outputs"; -export * from "./constants"; diff --git a/packages/platform/types/schedules/inputs/create-schedule.input.ts b/packages/platform/types/schedules/inputs/create-schedule.input.ts deleted file mode 100644 index 6923dd3b06..0000000000 --- a/packages/platform/types/schedules/inputs/create-schedule.input.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { - IsBoolean, - IsOptional, - IsString, - ValidateNested, - IsArray, - Matches, - IsISO8601, - IsTimeZone, - IsIn, -} from "class-validator"; - -import type { WeekDay } from "../constants"; -import { TIME_FORMAT_HH_MM, WEEK_DAYS } from "../constants"; - -export class ScheduleAvailabilityInput { - @IsArray() - @IsIn(WEEK_DAYS, { each: true }) - @ApiProperty({ example: ["Monday", "Tuesday"] }) - days!: WeekDay[]; - - @IsString() - @Matches(TIME_FORMAT_HH_MM, { message: "startTime must be a valid time format HH:MM" }) - @ApiProperty({ example: "09:00" }) - startTime!: string; - - @IsString() - @Matches(TIME_FORMAT_HH_MM, { message: "endTime must be a valid time format HH:MM" }) - @ApiProperty({ example: "10:00" }) - endTime!: string; -} - -export class ScheduleOverrideInput { - @IsISO8601({ strict: true }) - @ApiProperty({ example: "2024-05-20" }) - date!: string; - - @IsString() - @Matches(TIME_FORMAT_HH_MM, { message: "startTime must be a valid time format HH:MM" }) - @ApiProperty({ example: "12:00" }) - startTime!: string; - - @IsString() - @Matches(TIME_FORMAT_HH_MM, { message: "endTime must be a valid time format HH:MM" }) - @ApiProperty({ example: "13:00" }) - endTime!: string; -} - -export class CreateScheduleInput { - @IsString() - @ApiProperty({ example: "One-on-one coaching" }) - name!: string; - - @IsTimeZone() - @ApiProperty({ example: "Europe/Rome" }) - timeZone!: string; - - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ScheduleAvailabilityInput) - @ApiProperty({ - type: [ScheduleAvailabilityInput], - example: [ - { - days: ["Monday", "Tuesday"], - startTime: "09:00", - endTime: "10:00", - }, - ], - required: false, - }) - availability?: ScheduleAvailabilityInput[]; - - @IsBoolean() - @ApiProperty({ example: true }) - isDefault!: boolean; - - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ScheduleOverrideInput) - @ApiProperty({ - type: [ScheduleOverrideInput], - example: [ - { - date: "2024-05-20", - startTime: "12:00", - endTime: "14:00", - }, - ], - required: false, - }) - overrides?: ScheduleOverrideInput[]; -} diff --git a/packages/platform/types/schedules/inputs/index.ts b/packages/platform/types/schedules/inputs/index.ts deleted file mode 100644 index 061eb32a22..0000000000 --- a/packages/platform/types/schedules/inputs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./create-schedule.input"; -export * from "./update-schedule.input"; diff --git a/packages/platform/types/schedules/inputs/update-schedule.input.ts b/packages/platform/types/schedules/inputs/update-schedule.input.ts deleted file mode 100644 index 5920b11d36..0000000000 --- a/packages/platform/types/schedules/inputs/update-schedule.input.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsString, IsBoolean, IsOptional, ValidateNested, IsArray, IsTimeZone } from "class-validator"; - -import { ScheduleAvailabilityInput, ScheduleOverrideInput } from "./create-schedule.input"; - -export class UpdateScheduleInput { - @IsString() - @IsOptional() - @ApiProperty({ example: "One-on-one coaching", required: false }) - name?: string; - - @IsTimeZone() - @IsOptional() - @ApiProperty({ example: "Europe/Rome", required: false }) - timeZone?: string; - - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ScheduleAvailabilityInput) - @ApiProperty({ - type: [ScheduleAvailabilityInput], - example: [ - { - days: ["Monday", "Tuesday"], - startTime: "09:00", - endTime: "10:00", - }, - ], - required: false, - }) - availability?: ScheduleAvailabilityInput[]; - - @IsBoolean() - @IsOptional() - @ApiProperty({ example: true, required: false }) - isDefault?: boolean; - - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ScheduleOverrideInput) - @ApiProperty({ - type: [ScheduleOverrideInput], - example: [ - { - date: "2024-05-20", - startTime: "12:00", - endTime: "14:00", - }, - ], - required: false, - }) - overrides?: ScheduleOverrideInput[]; -} diff --git a/packages/platform/types/schedules/outputs/index.ts b/packages/platform/types/schedules/outputs/index.ts deleted file mode 100644 index c53dcae82b..0000000000 --- a/packages/platform/types/schedules/outputs/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./schedule.output"; -export * from "./get-schedule.output"; -export * from "./get-schedules.output"; -export * from "./get-default-schedule.output"; -export * from "./create-schedule.output"; -export * from "./update-schedule.output"; -export * from "./delete-schedule.output"; diff --git a/packages/platform/types/schedules/outputs/schedule.output.ts b/packages/platform/types/schedules/outputs/schedule.output.ts deleted file mode 100644 index f05a73ec73..0000000000 --- a/packages/platform/types/schedules/outputs/schedule.output.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { - IsBoolean, - IsOptional, - IsString, - ValidateNested, - IsArray, - IsTimeZone, - IsNumber, -} from "class-validator"; - -import { ScheduleAvailabilityInput, ScheduleOverrideInput } from "../inputs/create-schedule.input"; - -export class ScheduleOutput { - @IsNumber() - @ApiProperty({ example: 254 }) - id!: number; - - @IsNumber() - @ApiProperty({ example: 478 }) - ownerId!: number; - - @IsString() - @ApiProperty({ example: "One-on-one coaching" }) - name!: string; - - @IsTimeZone() - @ApiProperty({ example: "Europe/Rome" }) - timeZone!: string; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => ScheduleAvailabilityInput) - @ApiProperty({ - type: [ScheduleAvailabilityInput], - example: [ - { - days: ["Monday", "Tuesday"], - startTime: "09:00", - endTime: "10:00", - }, - ], - }) - availability!: ScheduleAvailabilityInput[]; - - @IsBoolean() - @ApiProperty({ example: true }) - isDefault!: boolean; - - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ScheduleOverrideInput) - @ApiProperty({ - type: [ScheduleOverrideInput], - example: [ - { - date: "2024-05-20", - startTime: "12:00", - endTime: "13:00", - }, - ], - }) - overrides!: ScheduleOverrideInput[]; -} diff --git a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts index 0ede8713f0..42fbf5a78b 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts @@ -1,7 +1,8 @@ +import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/lib"; import { - transformAvailabilityForAtom, - transformDateOverridesForAtom, - transformWorkingHoursForAtom, + transformAvailabilityForClient, + transformDateOverridesForClient, + transformWorkingHoursForClient, } from "@calcom/lib"; import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser"; import { prisma } from "@calcom/prisma"; @@ -19,7 +20,7 @@ type GetOptions = { input: TGetInputSchema; }; -export const getHandler = async ({ ctx, input }: GetOptions) => { +export const getHandler = async ({ ctx, input }: GetOptions): Promise => { const { user } = ctx; const schedule = await prisma.schedule.findUnique({ @@ -65,11 +66,11 @@ export const getHandler = async ({ ctx, input }: GetOptions) => { id: schedule.id, name: schedule.name, isManaged: schedule.userId !== user.id, - workingHours: transformWorkingHoursForAtom(schedule), + workingHours: transformWorkingHoursForClient(schedule), schedule: schedule.availability, - availability: transformAvailabilityForAtom(schedule), + availability: transformAvailabilityForClient(schedule), timeZone, - dateOverrides: transformDateOverridesForAtom(schedule, timeZone), + dateOverrides: transformDateOverridesForClient(schedule, timeZone), isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id, isLastSchedule: schedulesCount <= 1, readOnly: schedule.userId !== user.id && !input.isManagedEventType, diff --git a/packages/trpc/server/routers/viewer/availability/schedule/update.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/update.handler.ts index 25626716ea..f1fe4bb498 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/update.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/update.handler.ts @@ -1,4 +1,4 @@ -import { transformScheduleToAvailabilityForAtom } from "@calcom/lib"; +import { transformScheduleToAvailabilityForClient } from "@calcom/lib"; import { getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { hasEditPermissionForUserID } from "@calcom/lib/hasEditPermissionForUser"; import { prisma } from "@calcom/prisma"; @@ -118,7 +118,7 @@ export const updateHandler = async ({ input, ctx }: UpdateOptions) => { }, }); - const userAvailability = transformScheduleToAvailabilityForAtom(schedule); + const userAvailability = transformScheduleToAvailabilityForClient(schedule); return { schedule, diff --git a/yarn.lock b/yarn.lock index 69dbae83ff..65a336a801 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4006,7 +4006,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/atoms@*, @calcom/atoms@workspace:packages/platform/atoms": +"@calcom/atoms@*, @calcom/atoms@workspace:^, @calcom/atoms@workspace:packages/platform/atoms": version: 0.0.0-use.local resolution: "@calcom/atoms@workspace:packages/platform/atoms" dependencies: @@ -4015,7 +4015,6 @@ __metadata: "@radix-ui/react-toast": ^1.1.5 "@rollup/plugin-node-resolve": ^15.0.1 "@tanstack/react-query": ^5.17.15 - "@types/jest": ^29.5.12 "@types/node": ^20.3.1 "@types/react": 18.0.26 "@types/react-dom": ^18.0.9 @@ -4023,7 +4022,6 @@ __metadata: autoprefixer: ^10.4.19 class-variance-authority: ^0.4.0 clsx: ^2.0.0 - jest: ^29.7.0 lucide-react: ^0.364.0 postcss: ^8.4.38 postcss-import: ^16.1.0 @@ -4034,7 +4032,6 @@ __metadata: tailwind-merge: ^1.13.2 tailwindcss: ^3.3.3 tailwindcss-animate: ^1.0.6 - ts-jest: ^29.1.2 typescript: ^4.9.4 vite: ^5.0.10 vite-plugin-dts: ^3.7.3 @@ -5391,6 +5388,7 @@ __metadata: dependencies: "@algora/sdk": ^0.1.2 "@calcom/app-store": "*" + "@calcom/atoms": "workspace:^" "@calcom/config": "*" "@calcom/dayjs": "*" "@calcom/embed-react": "workspace:^" @@ -5436,6 +5434,7 @@ __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 @@ -5448,6 +5447,7 @@ __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 @@ -5457,7 +5457,7 @@ __metadata: i18n-unused: ^0.13.0 iframe-resizer-react: ^1.1.0 keen-slider: ^6.8.0 - lucide-react: ^0.171.0 + lucide-react: ^0.364.0 micro: ^10.0.1 next: ^14.1.3 next-auth: ^4.22.1 @@ -5469,7 +5469,7 @@ __metadata: prism-react-renderer: ^1.3.5 react: ^18.2.0 react-confetti: ^6.0.1 - react-datocms: ^3.1.0 + react-datocms: ^5.0.3 react-device-detect: ^2.2.2 react-dom: ^18.2.0 react-fast-marquee: ^1.6.4 @@ -5481,6 +5481,7 @@ __metadata: react-merge-refs: 1.1.0 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 @@ -9180,6 +9181,59 @@ __metadata: languageName: node linkType: hard +"@mux/mux-player-react@npm:*": + version: 2.5.0 + resolution: "@mux/mux-player-react@npm:2.5.0" + dependencies: + "@mux/mux-player": 2.5.0 + "@mux/playback-core": 0.23.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: 9b2854793a47928d964fbbd46d4c2e659d1dea29993bf74febcdbd3dfd2729abdb9d9d926e67113376ea7a9d99da9c275705d702996503deade6d35128e7f6ac + languageName: node + linkType: hard + +"@mux/mux-player@npm:2.5.0": + version: 2.5.0 + resolution: "@mux/mux-player@npm:2.5.0" + dependencies: + "@mux/mux-video": 0.18.0 + "@mux/playback-core": 0.23.0 + media-chrome: ~3.2.1 + checksum: 566bcb3372fb0f6ac4e37ce82707d565693681d010a7ae6d9890ae6b74320d8464af7bb1f8582709a7263c2e09e9e58137c2caf5f2c2eb3db91a137fb625d064 + languageName: node + linkType: hard + +"@mux/mux-video@npm:0.18.0": + version: 0.18.0 + resolution: "@mux/mux-video@npm:0.18.0" + dependencies: + "@mux/playback-core": 0.23.0 + castable-video: ~1.0.6 + custom-media-element: ~1.2.3 + media-tracks: ~0.3.0 + checksum: a6a8137cfbfd04304f11434cc8e2eff3c1f3e72fd758cd4b2bf4eea4ce29a29525116a491e4be3ce4c995eba483704a5020ab4db5960017b22c5a913bfb04279 + languageName: node + linkType: hard + +"@mux/playback-core@npm:0.23.0": + version: 0.23.0 + resolution: "@mux/playback-core@npm:0.23.0" + dependencies: + hls.js: ~1.5.8 + mux-embed: ~5.2.0 + checksum: c5bee62359ee79094ceca839972a7ecbcaf7339cb68ab5f404bbb6b3b7e25d1066c69bc4d92aa9912af5fa6d6a0e4eabe745e3589fffcc92f8b78171a6af58eb + languageName: node + linkType: hard + "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.9 resolution: "@ndelangen/get-tarball@npm:3.0.9" @@ -16143,7 +16197,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.10, @types/jest@npm:^29.5.12": +"@types/jest@npm:^29.5.10": version: 29.5.12 resolution: "@types/jest@npm:29.5.12" dependencies: @@ -20429,6 +20483,15 @@ __metadata: languageName: node linkType: hard +"castable-video@npm:~1.0.6": + version: 1.0.6 + resolution: "castable-video@npm:1.0.6" + dependencies: + custom-media-element: ~1.2.2 + checksum: 873ea75b35c594ed3755bacedd33628c070c751e34e0a64e42f98a4e9e0fda7f988ecac0ec723a4f7a25f332ba4650d4686e826614d81663554b9b31298cc648 + languageName: node + linkType: hard + "ccount@npm:^2.0.0": version: 2.0.1 resolution: "ccount@npm:2.0.1" @@ -20893,6 +20956,15 @@ __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" @@ -21165,6 +21237,13 @@ __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" @@ -22477,6 +22556,13 @@ __metadata: languageName: node linkType: hard +"custom-media-element@npm:~1.2.2, 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 + "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" @@ -26532,6 +26618,26 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^11.0.25": + version: 11.1.7 + resolution: "framer-motion@npm:11.1.7" + 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: ba4f2f0823eec7b78fa87180545cf8c832929ce52de8bab34312a67bfd6dee4ab6ad17143b9df2efcd4ede7cb87fc326843a1101a6cad3031c89a6ad2160e037 + languageName: node + linkType: hard + "fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -28213,6 +28319,13 @@ __metadata: languageName: node linkType: hard +"hls.js@npm:~1.5.8": + version: 1.5.8 + resolution: "hls.js@npm:1.5.8" + checksum: 93781b38859ce852952244e9671d536a73acb93a20cfbc3a68e372056d024789e48c7d67354b6b9ac35acab4a161d0d15adcf64279a0832f46465233cdbc8be0 + languageName: node + linkType: hard + "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -33007,15 +33120,6 @@ __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" @@ -33584,6 +33688,20 @@ __metadata: languageName: node linkType: hard +"media-chrome@npm:~3.2.1": + version: 3.2.1 + resolution: "media-chrome@npm:3.2.1" + checksum: 0e7d8a6850d0c4844be7b7975bcf80886aad5f2ae3cff1081aa5a7b7920ddf9f15b2da4df875f83ffb5f083d3fc1d337a8ac204317e40391f8311eb7f2f58c5e + languageName: node + linkType: hard + +"media-tracks@npm:~0.3.0": + version: 0.3.0 + resolution: "media-tracks@npm:0.3.0" + checksum: 217880bdf566cd070f22fce2cba132cd602ae2838a20f1c75aeb5f928edda283133d474b1f427deb2758cd9b64911f88eb6380921662566bbeb7051adbb81629 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -34938,6 +35056,13 @@ __metadata: languageName: node linkType: hard +"mux-embed@npm:~5.2.0": + version: 5.2.0 + resolution: "mux-embed@npm:5.2.0" + checksum: c90f6e216a8239ef15534b26feb2299cbac51dff82766e23c89be6e30bf4348fe711fb092968b607fd45d1b509e75882efc2e12e0b34039483c1bf1b7c6690bb + languageName: node + linkType: hard + "mysql2@npm:3.9.1": version: 3.9.1 resolution: "mysql2@npm:3.9.1" @@ -39219,20 +39344,21 @@ __metadata: languageName: node linkType: hard -"react-datocms@npm:^3.1.0": - version: 3.1.4 - resolution: "react-datocms@npm:3.1.4" +"react-datocms@npm:^5.0.3": + version: 5.0.3 + resolution: "react-datocms@npm:5.0.3" 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: ^8.33.1 + react-intersection-observer: ^9.4.3 react-string-replace: ^1.1.0 universal-base64: ^2.1.0 use-deep-compare-effect: ^1.6.1 peerDependencies: react: ">= 16.12.0" - checksum: 54aba12aef4937175c2011548a8a576c96c8d8a596e84d191826910624c1d596e76a49782689dc236388a10803b02e700ac820cb7500cca7fd147a81f6c544c3 + checksum: 22c20152afb54424acfe967a2c8c525cd9f132a33374f2aba0231f16ea64dade389b096e2dac8de9ffded612bc32e9891d725609ee947639fe1cef907cb143f5 languageName: node linkType: hard @@ -39484,12 +39610,16 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^8.33.1": - version: 8.34.0 - resolution: "react-intersection-observer@npm:8.34.0" +"react-intersection-observer@npm:^9.4.3": + version: 9.8.2 + resolution: "react-intersection-observer@npm:9.8.2" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0 - checksum: 7713fecfd1512c7f5a60f9f0bf15403b8f8bbd4110bcafaeaea6de36a0e0eb60368c3638f99e9c97b75ad8fc787ea48c241dcb5c694f821d7f2976f709082cc5 + 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 + peerDependenciesMeta: + react-dom: + optional: true + checksum: 0e77226cf458499959773da9db9e00ae33a6eb95c2fa1ec3f506bbcba00955c1c27d2320fba8cdd740cadb1c7d6cc396386cbd9c359298cff196cac660dbee49 languageName: node linkType: hard @@ -40013,6 +40143,18 @@ __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" @@ -41840,6 +41982,13 @@ __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" @@ -44726,7 +44875,7 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.1, ts-jest@npm:^29.1.2": +"ts-jest@npm:^29.1.1": version: 29.1.2 resolution: "ts-jest@npm:29.1.2" dependencies: