From c25c835b76ce1dcb7b24c44b04b29d6f91d4c546 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Thu, 16 May 2024 14:20:13 +0200 Subject: [PATCH] refactor: api v2 schedules (#14870) * api: createSchedule refactor - still need to handle overrides * createSchedule: handle overrides * createSchedule: default schedule e2e test fix and pad hours:minutes * get default schedule, by id and all schedules * update schedule * refactor: update handler delete only if overrides or availabilities given * remove comment * refactor: move schedules inputs, outputs to platform-types + e2e tests refactor * refactor e2e * fix: overrides and test * fix managed user create * my brain is fried but refactor good * WIP: transform api schedule for atom * WIP: transform api schedule for atom * WIP: transform api schedule for atom * fix: re-export atom transformer from lib * fix: format schedule for api from atom * fix ts ignore * refactor: move schedule hooks into hooks/schedules * feat: useSchedules hook to get count of user schedules * tests: setup atom tests & test transformScheduleForAtom * rename transformers * test transformAtomScheduleForApi * reset scheduel update handler for trpc * fix: import path * refactor: transformers * swagger docs fix * get rid of unused availabilities module * get rid of eslint-disable-next-line in schedules repository * refactor: remove crypto from atoms vite rollupOptions * refactor: comment schedules repository update logic --- .../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/schedule-updated.output.ts | 95 ---- .../ee/schedules/outputs/schedule.output.ts | 105 ----- .../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 | 424 ++++++------------ packages/lib/availability.ts | 6 +- packages/lib/schedules/index.ts | 2 +- .../for-atom.ts} | 35 +- .../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 ++- .../{ => schedules}/useDeleteSchedule.ts | 4 +- .../atoms/hooks/schedules/useSchedule.ts | 26 ++ .../atoms/hooks/schedules/useSchedules.ts | 26 ++ .../{ => schedules}/useUpdateSchedule.ts | 18 +- .../platform/atoms/hooks/useClientSchedule.ts | 31 -- 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 +++ .../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 +- .../platform/types/schedules/outputs/index.ts | 7 + .../schedules/outputs/schedule.output.ts | 66 +++ .../outputs/update-schedule.output.ts | 11 +- .../availability/schedule/get.handler.ts | 15 +- .../availability/schedule/update.handler.ts | 4 +- yarn.lock | 203 ++------- 55 files changed, 1391 insertions(+), 1209 deletions(-) delete mode 100644 apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts delete mode 100644 apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts delete mode 100644 apps/api/v2/src/ee/schedules/outputs/schedule.output.ts create mode 100644 apps/api/v2/src/ee/schedules/services/input-schedules.service.ts create mode 100644 apps/api/v2/src/ee/schedules/services/output-schedules.service.ts delete mode 100644 apps/api/v2/src/modules/availabilities/availabilities.module.ts delete mode 100644 apps/api/v2/src/modules/availabilities/availabilities.service.ts delete mode 100644 apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts rename packages/lib/schedules/{client/transformers.ts => transformers/for-atom.ts} (68%) create mode 100644 packages/lib/schedules/transformers/from-api.ts create mode 100644 packages/lib/schedules/transformers/index.ts create mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts create mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts create mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts create mode 100644 packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts rename packages/platform/atoms/hooks/{ => schedules}/useDeleteSchedule.ts (94%) create mode 100644 packages/platform/atoms/hooks/schedules/useSchedule.ts create mode 100644 packages/platform/atoms/hooks/schedules/useSchedules.ts rename packages/platform/atoms/hooks/{ => schedules}/useUpdateSchedule.ts (64%) delete mode 100644 packages/platform/atoms/hooks/useClientSchedule.ts create mode 100644 packages/platform/atoms/jest.config.json delete mode 100644 packages/platform/types/schedules.ts create mode 100644 packages/platform/types/schedules/constants.ts create mode 100644 packages/platform/types/schedules/index.ts create mode 100644 packages/platform/types/schedules/inputs/create-schedule.input.ts create mode 100644 packages/platform/types/schedules/inputs/index.ts create mode 100644 packages/platform/types/schedules/inputs/update-schedule.input.ts rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/create-schedule.output.ts (89%) rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/delete-schedule.output.ts (100%) rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/get-default-schedule.output.ts (89%) rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/get-schedule.output.ts (84%) rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/get-schedules.output.ts (83%) create mode 100644 packages/platform/types/schedules/outputs/index.ts create mode 100644 packages/platform/types/schedules/outputs/schedule.output.ts rename {apps/api/v2/src/ee => packages/platform/types}/schedules/outputs/update-schedule.output.ts (73%) 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 891a8e7b62..5cfdf9b814 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,11 +2,9 @@ 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"; @@ -21,6 +19,7 @@ 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", () => { @@ -43,7 +42,7 @@ describe("Bookings Endpoints", () => { const moduleRef = await withAccessTokenAuth( userEmail, Test.createTestingModule({ - imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule], + imports: [AppModule, PrismaModule, 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 d5179acd75..5851f88753 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,7 +2,6 @@ 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"; @@ -34,14 +33,7 @@ describe("Me Endpoints", () => { const moduleRef = await withAccessTokenAuth( userEmail, Test.createTestingModule({ - imports: [ - AppModule, - PrismaModule, - AvailabilitiesModule, - UsersModule, - TokensModule, - SchedulesModule, - ], + imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule], }) ) .overrideGuard(PermissionsGuard) @@ -118,13 +110,13 @@ describe("Me Endpoints", () => { }); it("should not update user associated with access token given invalid time format", async () => { - const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 }; + const bodyWithIncorrectTimeFormat = { 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: UpdateManagedUserInput = { weekStart: "waba luba dub dub" }; + const bodyWithIncorrectWeekStart = { 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 2c64df0397..5b213c9397 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, user.defaultScheduleId, { + await this.schedulesRepository.updateUserSchedule(user.id, 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 7e6dec77f5..bf998f83c4 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,12 +1,7 @@ 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"; @@ -20,6 +15,14 @@ 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", () => { @@ -32,23 +35,27 @@ 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, - AvailabilitiesModule, - UsersModule, - TokensModule, - SchedulesModule, - ], + imports: [AppModule, PrismaModule, UsersModule, TokensModule, SchedulesModule], }) ) .overrideGuard(PermissionsGuard) @@ -75,60 +82,66 @@ 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(body) + .send(createScheduleInput) .expect(201) .then(async (response) => { - 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 responseBody: CreateScheduleOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + createdSchedule = response.body.data; - 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 expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1); - const scheduleUser = schedule?.[0].userId - ? await userRepositoryFixture.get(schedule?.[0].userId) + const scheduleOwner = createdSchedule.ownerId + ? await userRepositoryFixture.get(createdSchedule.ownerId) : null; - expect(scheduleUser?.defaultScheduleId).toEqual(responseData.data.id); - createdSchedule = responseData.data; + expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id); }); }); + 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 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 responseBody: GetScheduleOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const outputSchedule = responseBody.data; - 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 expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1); }); }); @@ -137,23 +150,21 @@ describe("Schedules Endpoints", () => { .get(`/api/v2/schedules`) .expect(200) .then((response) => { - 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 responseBody: GetSchedulesOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const outputSchedule = responseBody.data[0]; - 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); + const expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + outputScheduleMatchesExpected(outputSchedule, expectedSchedule, 1); }); }); it("should update schedule name", async () => { - const newScheduleName = "new-schedule-name"; + const newScheduleName = "updated-schedule-name"; const body: UpdateScheduleInput = { name: newScheduleName, @@ -166,19 +177,41 @@ describe("Schedules Endpoints", () => { .then((response: any) => { const responseData: UpdateScheduleOutput = response.body; expect(responseData.status).toEqual(SUCCESS_STATUS); - 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 responseSchedule = responseData.data; - 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); + const expectedSchedule = { ...createdSchedule, name: newScheduleName }; + outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1); - createdSchedule.name = newScheduleName; + 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; }); }); 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 b2860cfe5e..396d7fb88d 100644 --- a/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts +++ b/apps/api/v2/src/ee/schedules/controllers/schedules.controller.ts @@ -1,9 +1,3 @@ -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"; @@ -26,9 +20,16 @@ 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 { UpdateScheduleInput } from "@calcom/platform-types"; - -import { CreateScheduleInput } from "../inputs/create-schedule.input"; +import { + CreateScheduleOutput, + CreateScheduleInput, + UpdateScheduleInput, + GetScheduleOutput, + UpdateScheduleOutput, + GetDefaultScheduleOutput, + DeleteScheduleOutput, + GetSchedulesOutput, +} from "@calcom/platform-types"; @Controller({ path: "schedules", @@ -46,26 +47,22 @@ 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: scheduleFormatted, + data: schedule, }; } @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: scheduleFormatted, + data: schedule, }; } @@ -77,11 +74,10 @@ 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: scheduleFormatted, + data: schedule, }; } @@ -89,15 +85,13 @@ 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: schedulesFormatted, + data: schedules, }; } - // note(Lauris): currently this endpoint is atoms only @Patch("/:scheduleId") @Permissions([SCHEDULE_WRITE]) async updateSchedule( @@ -106,7 +100,7 @@ export class SchedulesController { @Param("scheduleId") scheduleId: string ): Promise { const updatedSchedule = await this.schedulesService.updateUserSchedule( - user, + user.id, 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 deleted file mode 100644 index f90c291adf..0000000000 --- a/apps/api/v2/src/ee/schedules/inputs/create-schedule.input.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts b/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts deleted file mode 100644 index a16600db98..0000000000 --- a/apps/api/v2/src/ee/schedules/outputs/schedule-updated.output.ts +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 015bfb77d8..0000000000 --- a/apps/api/v2/src/ee/schedules/outputs/schedule.output.ts +++ /dev/null @@ -1,105 +0,0 @@ -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/apps/api/v2/src/ee/schedules/schedules.module.ts b/apps/api/v2/src/ee/schedules/schedules.module.ts index 3fe53634ee..0f9bd4a9d4 100644 --- a/apps/api/v2/src/ee/schedules/schedules.module.ts +++ b/apps/api/v2/src/ee/schedules/schedules.module.ts @@ -1,15 +1,16 @@ 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, AvailabilitiesModule, UsersModule, TokensModule], - providers: [SchedulesRepository, SchedulesService], + imports: [PrismaModule, UsersModule, TokensModule], + providers: [SchedulesRepository, SchedulesService, InputSchedulesService, OutputSchedulesService], 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 b447b6b469..a6c4615de4 100644 --- a/apps/api/v2/src/ee/schedules/schedules.repository.ts +++ b/apps/api/v2/src/ee/schedules/schedules.repository.ts @@ -1,19 +1,34 @@ -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 createScheduleWithAvailabilities( - userId: number, - schedule: CreateScheduleInput, - availabilities: CreateAvailabilityInput[] - ) { + async createSchedule(userId: number, schedule: Omit) { + const { availability, overrides } = schedule; + const createScheduleData: Prisma.ScheduleCreateInput = { user: { connect: { @@ -24,17 +39,34 @@ export class SchedulesRepository { timeZone: schedule.timeZone, }; - if (availabilities.length > 0) { + 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) { createScheduleData.availability = { createMany: { - data: availabilities.map((availability) => { - return { - days: availability.days, - startTime: availability.startTime, - endTime: availability.endTime, - userId, - }; - }), + data: availabilitiesAndOverrides, }, }; } @@ -64,6 +96,87 @@ 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 new file mode 100644 index 0000000000..c642e272d7 --- /dev/null +++ b/apps/api/v2/src/ee/schedules/services/input-schedules.service.ts @@ -0,0 +1,40 @@ +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 new file mode 100644 index 0000000000..901644cf7a --- /dev/null +++ b/apps/api/v2/src/ee/schedules/services/output-schedules.service.ts @@ -0,0 +1,78 @@ +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 c91176e422..a416d14f69 100644 --- a/apps/api/v2/src/ee/schedules/services/schedules.service.ts +++ b/apps/api/v2/src/ee/schedules/services/schedules.service.ts @@ -1,55 +1,42 @@ -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 { AvailabilitiesService } from "@/modules/availabilities/availabilities.service"; -import { UserWithProfile, UsersRepository } from "@/modules/users/users.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 { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { Schedule } from "@prisma/client"; -import { User } from "@prisma/client"; -import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries"; -import { updateScheduleHandler } from "@calcom/platform-libraries"; -import { - transformWorkingHoursForClient, - transformAvailabilityForClient, - transformDateOverridesForClient, -} from "@calcom/platform-libraries"; +import { CreateScheduleInput, ScheduleOutput } from "@calcom/platform-types"; import { UpdateScheduleInput } from "@calcom/platform-types"; @Injectable() export class SchedulesService { constructor( private readonly schedulesRepository: SchedulesRepository, - private readonly availabilitiesService: AvailabilitiesService, + private readonly inputSchedulesService: InputSchedulesService, + private readonly outputSchedulesService: OutputSchedulesService, private readonly usersRepository: UsersRepository ) {} async createUserDefaultSchedule(userId: number, timeZone: string) { - const schedule = { + const defaultSchedule = { isDefault: true, name: "Default schedule", timeZone, }; - return this.createUserSchedule(userId, schedule); + return this.createUserSchedule(userId, defaultSchedule); } - async createUserSchedule(userId: number, schedule: CreateScheduleInput) { - const availabilities = schedule.availabilities?.length - ? schedule.availabilities - : [this.availabilitiesService.getDefaultAvailabilityInput()]; + async createUserSchedule(userId: number, scheduleInput: CreateScheduleInput): Promise { + const schedule = this.inputSchedulesService.transformInputCreateSchedule(scheduleInput); - const createdSchedule = await this.schedulesRepository.createScheduleWithAvailabilities( - userId, - schedule, - availabilities - ); + const createdSchedule = await this.schedulesRepository.createSchedule(userId, schedule); if (schedule.isDefault) { await this.usersRepository.setDefaultSchedule(userId, createdSchedule.id); } - return createdSchedule; + return this.outputSchedulesService.getResponseSchedule(createdSchedule); } async getUserScheduleDefault(userId: number) { @@ -57,7 +44,10 @@ export class SchedulesService { if (!user?.defaultScheduleId) return null; - return this.schedulesRepository.getScheduleById(user.defaultScheduleId); + const defaultSchedule = await this.schedulesRepository.getScheduleById(user.defaultScheduleId); + + if (!defaultSchedule) return null; + return this.outputSchedulesService.getResponseSchedule(defaultSchedule); } async getUserSchedule(userId: number, scheduleId: number) { @@ -69,38 +59,45 @@ export class SchedulesService { this.checkUserOwnsSchedule(userId, existingSchedule); - return existingSchedule; + return this.outputSchedulesService.getResponseSchedule(existingSchedule); } async getUserSchedules(userId: number) { - return this.schedulesRepository.getSchedulesByUserId(userId); + const schedules = await this.schedulesRepository.getSchedulesByUserId(userId); + return Promise.all( + schedules.map(async (schedule) => { + return this.outputSchedulesService.getResponseSchedule(schedule); + }) + ); } - async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) { + async updateUserSchedule(userId: number, 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(user.id, existingSchedule); + this.checkUserOwnsSchedule(userId, existingSchedule); - const schedule = await this.getUserSchedule(user.id, Number(scheduleId)); - const scheduleFormatted = await this.formatScheduleForAtom(user, schedule); + const availability = bodySchedule.availability + ? this.inputSchedulesService.transformInputScheduleAvailability(bodySchedule.availability) + : undefined; + const overrides = bodySchedule.overrides + ? this.inputSchedulesService.transformInputOverrides(bodySchedule.overrides) + : undefined; - 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; + if (bodySchedule.isDefault) { + await this.usersRepository.setDefaultSchedule(userId, scheduleId); } - return updateScheduleHandler({ - input: { scheduleId: Number(scheduleId), ...bodySchedule }, - ctx: { user }, + const updatedSchedule = await this.schedulesRepository.updateSchedule(userId, scheduleId, { + ...bodySchedule, + availability, + overrides, }); + + return this.outputSchedulesService.getResponseSchedule(updatedSchedule); } async deleteUserSchedule(userId: number, scheduleId: number) { @@ -115,44 +112,6 @@ 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 deleted file mode 100644 index f3fe35bf6a..0000000000 --- a/apps/api/v2/src/modules/availabilities/availabilities.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index fc7dc14ec4..0000000000 --- a/apps/api/v2/src/modules/availabilities/availabilities.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index f03698705b..0000000000 --- a/apps/api/v2/src/modules/availabilities/inputs/create-availability.input.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 12345efa5f..1423903b45 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,7 +21,6 @@ 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 e03429b192..cd77469035 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -173,6 +173,14 @@ 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 03fb4cc1e9..ba7fa77e21 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2883,26 +2883,26 @@ "data" ] }, - "CreateAvailabilityInput": { + "ScheduleAvailabilityInput": { "type": "object", "properties": { "days": { "example": [ - 1, - 2 + "Monday", + "Tuesday" ], "type": "array", "items": { - "type": "number" + "type": "string" } }, "startTime": { - "format": "date-time", - "type": "string" + "type": "string", + "example": "09:00" }, "endTime": { - "format": "date-time", - "type": "string" + "type": "string", + "example": "10:00" } }, "required": [ @@ -2911,23 +2911,71 @@ "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" + "type": "string", + "example": "One-on-one coaching" }, "timeZone": { - "type": "string" + "type": "string", + "example": "Europe/Rome" }, - "availabilities": { + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], "type": "array", "items": { - "$ref": "#/components/schemas/CreateAvailabilityInput" + "$ref": "#/components/schemas/ScheduleAvailabilityInput" } }, "isDefault": { - "type": "boolean" + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput" + } } }, "required": [ @@ -2936,163 +2984,67 @@ "isDefault" ] }, - "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", - "nullable": true - }, - "eventTypeId": { - "type": "number", - "nullable": true - }, - "days": { - "type": "array", - "items": { - "type": "number" - } - }, - "startTime": { - "format": "date-time", - "type": "string" - }, - "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" + "type": "number", + "example": 254 + }, + "ownerId": { + "type": "number", + "example": 478 }, "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" - } - } + "type": "string", + "example": "One-on-one coaching" }, "timeZone": { - "type": "string" + "type": "string", + "example": "Europe/Rome" }, - "dateOverrides": { + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], "type": "array", "items": { - "type": "object" + "$ref": "#/components/schemas/ScheduleAvailabilityInput" } }, "isDefault": { - "type": "boolean" + "type": "boolean", + "example": true }, - "isLastSchedule": { - "type": "boolean" - }, - "readOnly": { - "type": "boolean" + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "13:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput" + } } }, "required": [ "id", + "ownerId", "name", - "isManaged", - "workingHours", - "schedule", - "availability", "timeZone", - "dateOverrides", + "availability", "isDefault", - "isLastSchedule", - "readOnly" + "overrides" ] }, "CreateScheduleOutput": { @@ -3127,12 +3079,7 @@ ] }, "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/ScheduleOutput" - } - ] + "$ref": "#/components/schemas/ScheduleOutput" } }, "required": [ @@ -3152,7 +3099,15 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput" + } + ] + }, + "error": { + "type": "object" } }, "required": [ @@ -3172,7 +3127,13 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "error": { + "type": "object" } }, "required": [ @@ -3183,138 +3144,48 @@ "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", - "nullable": true - } - }, - "required": [ - "id" - ] - }, - "ScheduleModel": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "userId": { - "type": "number" - }, - "name": { - "type": "string" + "example": "One-on-one coaching" }, "timeZone": { "type": "string", - "nullable": true - }, - "eventType": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventTypeModel" - } + "example": "Europe/Rome" }, "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], "type": "array", "items": { - "$ref": "#/components/schemas/AvailabilityModel" + "$ref": "#/components/schemas/ScheduleAvailabilityInput" } - } - }, - "required": [ - "id", - "userId", - "name" - ] - }, - "UpdatedScheduleOutput": { - "type": "object", - "properties": { - "schedule": { - "$ref": "#/components/schemas/ScheduleModel" }, "isDefault": { - "type": "boolean" + "type": "boolean", + "example": true }, - "timeZone": { - "type": "string" - }, - "prevDefaultId": { - "type": "number", - "nullable": true - }, - "currentDefaultId": { - "type": "number", - "nullable": true + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput" + } } - }, - "required": [ - "schedule", - "isDefault" - ] + } }, "UpdateScheduleOutput": { "type": "object", @@ -3328,7 +3199,10 @@ ] }, "data": { - "$ref": "#/components/schemas/UpdatedScheduleOutput" + "$ref": "#/components/schemas/ScheduleOutput" + }, + "error": { + "type": "object" } }, "required": [ diff --git a/packages/lib/availability.ts b/packages/lib/availability.ts index c2763786a6..b604e07742 100644 --- a/packages/lib/availability.ts +++ b/packages/lib/availability.ts @@ -128,10 +128,10 @@ export function getWorkingHours( } export function availabilityAsString( - availability: Availability, + availability: Pick, { locale, hour12 }: { locale?: string; hour12?: boolean } ) { - const weekSpan = (availability: Availability) => { + const weekSpan = (availability: Pick) => { 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: Availability) => { + const timeSpan = (availability: Pick) => { 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/index.ts b/packages/lib/schedules/index.ts index abca07a1d6..a8401e8101 100644 --- a/packages/lib/schedules/index.ts +++ b/packages/lib/schedules/index.ts @@ -1 +1 @@ -export * from "./client/transformers"; +export * from "./transformers"; diff --git a/packages/lib/schedules/client/transformers.ts b/packages/lib/schedules/transformers/for-atom.ts similarity index 68% rename from packages/lib/schedules/client/transformers.ts rename to packages/lib/schedules/transformers/for-atom.ts index c3afe15999..7695f37ef1 100644 --- a/packages/lib/schedules/client/transformers.ts +++ b/packages/lib/schedules/transformers/for-atom.ts @@ -1,33 +1,25 @@ -import type { Availability as AvailabilityModel, Schedule as ScheduleModel } from "@prisma/client"; +import type { Availability } 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"; -export type ScheduleWithAvailabilities = ScheduleModel & { availability: AvailabilityModel[] }; +type ScheduleAvailability = Pick[]; +type ScheduleOverride = Pick[]; -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) { +export function transformWorkingHoursForAtom(schedule: { + timeZone: string | null; + availability: ScheduleAvailability; +}) { return getWorkingHours( { timeZone: schedule.timeZone || undefined, utcOffset: 0 }, schedule.availability || [] ); } -export function transformAvailabilityForClient(schedule: ScheduleWithAvailabilities) { - return transformScheduleToAvailabilityForClient(schedule).map((a) => +export function transformAvailabilityForAtom(schedule: { availability: ScheduleAvailability }) { + return transformScheduleToAvailabilityForAtom(schedule).map((a) => a.map((startAndEnd) => ({ ...startAndEnd, end: new Date(startAndEnd.end.toISOString().replace("23:59:00.000Z", "23:59:59.999Z")), @@ -35,7 +27,10 @@ export function transformAvailabilityForClient(schedule: ScheduleWithAvailabilit ); } -export function transformDateOverridesForClient(schedule: ScheduleWithAvailabilities, timeZone: string) { +export function transformDateOverridesForAtom( + schedule: { availability: ScheduleOverride }, + timeZone: string +) { return schedule.availability.reduce((acc, override) => { // only iff future date override if (!override.date || dayjs.tz(override.date, timeZone).isBefore(dayjs(), "day")) { @@ -67,9 +62,7 @@ export function transformDateOverridesForClient(schedule: ScheduleWithAvailabili }, [] as { ranges: TimeRange[] }[]); } -export const transformScheduleToAvailabilityForClient = ( - schedule: Partial & { availability: AvailabilityModel[] } -) => { +export const transformScheduleToAvailabilityForAtom = (schedule: { availability: ScheduleAvailability }) => { return schedule.availability.reduce( (schedule: Schedule, availability) => { availability.days.forEach((day) => { diff --git a/packages/lib/schedules/transformers/from-api.ts b/packages/lib/schedules/transformers/from-api.ts new file mode 100644 index 0000000000..6e428b0f44 --- /dev/null +++ b/packages/lib/schedules/transformers/from-api.ts @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000000..832691070b --- /dev/null +++ b/packages/lib/schedules/transformers/index.ts @@ -0,0 +1,2 @@ +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 3954c15ce0..a33606905c 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -62,6 +62,8 @@ export type CustomClassNames = { }; }; +export type Availability = Pick; + type AvailabilitySettingsProps = { skeletonLabel?: string; schedule: { @@ -73,7 +75,7 @@ type AvailabilitySettingsProps = { workingHours: WorkingHours[]; dateOverrides: { ranges: TimeRange[] }[]; timeZone: string; - schedule: Schedule[]; + schedule: Availability[]; }; travelSchedules?: RouterOutputs["viewer"]["getTravelSchedules"]; handleDelete: () => void; @@ -274,7 +276,9 @@ 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 new file mode 100644 index 0000000000..5074238db6 --- /dev/null +++ b/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.spec.ts @@ -0,0 +1,127 @@ +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 new file mode 100644 index 0000000000..c3aa3a62a3 --- /dev/null +++ b/packages/platform/atoms/availability/atom-api-transformers/transformApiScheduleForAtom.ts @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000000..d9876ca4ba --- /dev/null +++ b/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.spec.ts @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000000..f912f687da --- /dev/null +++ b/packages/platform/atoms/availability/atom-api-transformers/transformAtomScheduleForApi.ts @@ -0,0 +1,62 @@ +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 7595836584..8aeaff53c6 100644 --- a/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx +++ b/packages/platform/atoms/availability/wrappers/PlatformAvailabilitySettingsWrapper.tsx @@ -1,16 +1,18 @@ import type { ScheduleLabelsType } from "@calcom/features/schedules/components/Schedule"; -import type { ScheduleWithAvailabilitiesForWeb, UpdateScheduleOutputType } from "@calcom/platform-libraries"; -import type { ApiErrorResponse, ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; +import type { ApiErrorResponse, ApiResponse, ScheduleOutput } from "@calcom/platform-types"; -import useClientSchedule from "../../hooks/useClientSchedule"; -import useDeleteSchedule from "../../hooks/useDeleteSchedule"; +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 { 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 { Schedule } from "../AvailabilitySettings"; +import type { Availability } 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 = { @@ -19,7 +21,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; @@ -35,10 +37,10 @@ export const PlatformAvailabilitySettingsWrapper = ({ onUpdateSuccess, disableEditableHeading = false, }: PlatformAvailabilitySettingsWrapperProps) => { - const { isLoading, data: schedule } = useClientSchedule(id); - const mySchedule = schedule as ApiSuccessResponse; + const { isLoading, data: schedule } = useSchedule(id); + const { data: schedules } = useSchedules(); const { data: me } = useMe(); - const userSchedule = mySchedule?.data; + const atomSchedule = transformApiScheduleForAtom(me?.data, schedule, schedules?.length || 0); const { timeFormat } = me?.data || { timeFormat: null }; const { toast } = useToast(); @@ -77,48 +79,43 @@ export const PlatformAvailabilitySettingsWrapper = ({ }; const handleUpdate = async (id: number, body: AvailabilityFormValues) => { - const transformedDateOverrides = - body.dateOverrides.flatMap( - (dateOverridesRanges) => - dateOverridesRanges?.ranges?.map((range) => ({ start: range.start, end: range.end })) ?? [] - ) ?? []; - - await updateSchedule({ ...body, scheduleId: id, dateOverrides: transformedDateOverrides }); + const updateBody = transformAtomScheduleForApi(body); + updateSchedule({ id, ...updateBody }); }; if (isLoading) return
Loading...
; - if (!userSchedule) return
No user schedule present
; + if (!atomSchedule) return
No user schedule present
; return ( { - userSchedule.id && handleDelete(userSchedule.id); + atomSchedule.id && handleDelete(atomSchedule.id); }} handleSubmit={async (data) => { - userSchedule.id && handleUpdate(userSchedule.id, data); + atomSchedule.id && handleUpdate(atomSchedule.id, data); }} weekStart="Sunday" timeFormat={timeFormat} isLoading={isLoading} schedule={{ - name: userSchedule.name, - id: userSchedule.id, - isLastSchedule: userSchedule.isLastSchedule, - isDefault: userSchedule.isDefault, - workingHours: userSchedule.workingHours, - dateOverrides: userSchedule.dateOverrides, - timeZone: userSchedule.timeZone, - availability: userSchedule.availability, + name: atomSchedule.name, + id: atomSchedule.id, + isLastSchedule: atomSchedule.isLastSchedule, + isDefault: atomSchedule.isDefault, + workingHours: atomSchedule.workingHours, + dateOverrides: atomSchedule.dateOverrides, + timeZone: atomSchedule.timeZone, + availability: atomSchedule.availability, schedule: - userSchedule.schedule.reduce( - (acc: Schedule[], avail: Schedule) => [ + atomSchedule.schedule.reduce( + (acc: Availability[], avail: Availability) => [ ...acc, - { ...avail, startTime: new Date(avail.startTime), endTime: new Date(avail.endTime) }, + { days: avail.days, startTime: new Date(avail.startTime), endTime: new Date(avail.endTime) }, ], - [] as Schedule[] + [] ) || [], }} isDeleting={isDeletionInProgress} diff --git a/packages/platform/atoms/hooks/useDeleteSchedule.ts b/packages/platform/atoms/hooks/schedules/useDeleteSchedule.ts similarity index 94% rename from packages/platform/atoms/hooks/useDeleteSchedule.ts rename to packages/platform/atoms/hooks/schedules/useDeleteSchedule.ts index 0322dcbb31..f476d68541 100644 --- a/packages/platform/atoms/hooks/useDeleteSchedule.ts +++ b/packages/platform/atoms/hooks/schedules/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 "./useClientSchedule"; +import http from "../../lib/http"; +import { QUERY_KEY } from "./useSchedule"; interface IUseDeleteScheduleProps { onSuccess?: (res: ApiResponse) => void; diff --git a/packages/platform/atoms/hooks/schedules/useSchedule.ts b/packages/platform/atoms/hooks/schedules/useSchedule.ts new file mode 100644 index 0000000000..df93bf6f31 --- /dev/null +++ b/packages/platform/atoms/hooks/schedules/useSchedule.ts @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000..f56c6f3aea --- /dev/null +++ b/packages/platform/atoms/hooks/schedules/useSchedules.ts @@ -0,0 +1,26 @@ +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/useUpdateSchedule.ts b/packages/platform/atoms/hooks/schedules/useUpdateSchedule.ts similarity index 64% rename from packages/platform/atoms/hooks/useUpdateSchedule.ts rename to packages/platform/atoms/hooks/schedules/useUpdateSchedule.ts index dcc953135d..f550cce165 100644 --- a/packages/platform/atoms/hooks/useUpdateSchedule.ts +++ b/packages/platform/atoms/hooks/schedules/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 "./useClientSchedule"; +import http from "../../lib/http"; +import { QUERY_KEY as ScheduleQueryKey } from "./useSchedule"; interface IPUpdateOAuthClient { - onSuccess?: (res: ApiResponse) => void; + onSuccess?: (res: ApiResponse) => void; onError?: (err: ApiErrorResponse) => void; } @@ -25,15 +25,11 @@ const useUpdateSchedule = ( ) => { const queryClient = useQueryClient(); - const mutation = useMutation< - ApiResponse, - unknown, - UpdateScheduleInput & { scheduleId: number } - >({ + const mutation = useMutation, unknown, UpdateScheduleInput & { id: number }>({ mutationFn: (data) => { - const pathname = `/${V2_ENDPOINTS.availability}/${data.scheduleId}?for=atom`; + const pathname = `/${V2_ENDPOINTS.availability}/${data.id}?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/hooks/useClientSchedule.ts b/packages/platform/atoms/hooks/useClientSchedule.ts deleted file mode 100644 index edc950942b..0000000000 --- a/packages/platform/atoms/hooks/useClientSchedule.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/jest.config.json b/packages/platform/atoms/jest.config.json new file mode 100644 index 0000000000..53b778bff5 --- /dev/null +++ b/packages/platform/atoms/jest.config.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.ts$": "ts-jest" + } +} diff --git a/packages/platform/atoms/package.json b/packages/platform/atoms/package.json index 2d8f76c60a..10bf456877 100644 --- a/packages/platform/atoms/package.json +++ b/packages/platform/atoms/package.json @@ -8,7 +8,8 @@ "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" + "publish": "rm -rf dist && yarn build && npm publish --access public", + "test": "jest" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", @@ -52,12 +53,15 @@ "@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" + "tailwindcss-animate": "^1.0.6", + "ts-jest": "^29.1.2" } } diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index a2e91c469e..6a57be023c 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -48,14 +48,12 @@ export type { ConnectedDestinationCalendars } from "@calcom/lib/getConnectedDest export { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; export { - transformWorkingHoursForClient, - transformAvailabilityForClient, - transformDateOverridesForClient, -} from "@calcom/lib/schedules/client/transformers"; -export type { - ScheduleWithAvailabilities, - ScheduleWithAvailabilitiesForWeb, -} from "@calcom/lib/schedules/client/transformers"; + transformWorkingHoursForAtom, + transformAvailabilityForAtom, + transformDateOverridesForAtom, + transformApiScheduleAvailability, + transformApiScheduleOverrides, +} from "@calcom/lib/schedules/transformers"; export type { BookingCreateBody, BookingResponse, diff --git a/packages/platform/types/schedules.ts b/packages/platform/types/schedules.ts deleted file mode 100644 index f9fe1578ed..0000000000 --- a/packages/platform/types/schedules.ts +++ /dev/null @@ -1,115 +0,0 @@ -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 new file mode 100644 index 0000000000..23ce20f2f4 --- /dev/null +++ b/packages/platform/types/schedules/constants.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..2d351e9bd3 --- /dev/null +++ b/packages/platform/types/schedules/index.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..6923dd3b06 --- /dev/null +++ b/packages/platform/types/schedules/inputs/create-schedule.input.ts @@ -0,0 +1,97 @@ +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 new file mode 100644 index 0000000000..061eb32a22 --- /dev/null +++ b/packages/platform/types/schedules/inputs/index.ts @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..5920b11d36 --- /dev/null +++ b/packages/platform/types/schedules/inputs/update-schedule.input.ts @@ -0,0 +1,56 @@ +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/apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts b/packages/platform/types/schedules/outputs/create-schedule.output.ts similarity index 89% rename from apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts rename to packages/platform/types/schedules/outputs/create-schedule.output.ts index bbcdd17887..9ff78f7094 100644 --- a/apps/api/v2/src/ee/schedules/outputs/create-schedule.output.ts +++ b/packages/platform/types/schedules/outputs/create-schedule.output.ts @@ -1,10 +1,11 @@ -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/apps/api/v2/src/ee/schedules/outputs/delete-schedule.output.ts b/packages/platform/types/schedules/outputs/delete-schedule.output.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/outputs/delete-schedule.output.ts rename to packages/platform/types/schedules/outputs/delete-schedule.output.ts diff --git a/apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts b/packages/platform/types/schedules/outputs/get-default-schedule.output.ts similarity index 89% rename from apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts rename to packages/platform/types/schedules/outputs/get-default-schedule.output.ts index cf8369fa21..0ad1969f3b 100644 --- a/apps/api/v2/src/ee/schedules/outputs/get-default-schedule.output.ts +++ b/packages/platform/types/schedules/outputs/get-default-schedule.output.ts @@ -1,10 +1,11 @@ -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/apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts b/packages/platform/types/schedules/outputs/get-schedule.output.ts similarity index 84% rename from apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts rename to packages/platform/types/schedules/outputs/get-schedule.output.ts index f49e291e23..c34268347c 100644 --- a/apps/api/v2/src/ee/schedules/outputs/get-schedule.output.ts +++ b/packages/platform/types/schedules/outputs/get-schedule.output.ts @@ -1,10 +1,11 @@ -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]) @@ -16,5 +17,7 @@ export class GetScheduleOutput { @IsNotEmptyObject() @ValidateNested() @Type(() => ScheduleOutput) - data!: ScheduleOutput; + data!: ScheduleOutput | null; + + error?: Error; } diff --git a/apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts b/packages/platform/types/schedules/outputs/get-schedules.output.ts similarity index 83% rename from apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts rename to packages/platform/types/schedules/outputs/get-schedules.output.ts index 9f7f6ce32f..a501ee6330 100644 --- a/apps/api/v2/src/ee/schedules/outputs/get-schedules.output.ts +++ b/packages/platform/types/schedules/outputs/get-schedules.output.ts @@ -1,21 +1,22 @@ -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/packages/platform/types/schedules/outputs/index.ts b/packages/platform/types/schedules/outputs/index.ts new file mode 100644 index 0000000000..c53dcae82b --- /dev/null +++ b/packages/platform/types/schedules/outputs/index.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000000..f05a73ec73 --- /dev/null +++ b/packages/platform/types/schedules/outputs/schedule.output.ts @@ -0,0 +1,66 @@ +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/apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts b/packages/platform/types/schedules/outputs/update-schedule.output.ts similarity index 73% rename from apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts rename to packages/platform/types/schedules/outputs/update-schedule.output.ts index 70ebe9ba9b..d80eb065bd 100644 --- a/apps/api/v2/src/ee/schedules/outputs/update-schedule.output.ts +++ b/packages/platform/types/schedules/outputs/update-schedule.output.ts @@ -1,20 +1,23 @@ -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: UpdatedScheduleOutput, + type: ScheduleOutput, }) @IsNotEmptyObject() @ValidateNested() - @Type(() => UpdatedScheduleOutput) - data!: UpdatedScheduleOutput; + @Type(() => ScheduleOutput) + data!: ScheduleOutput; + + error?: Error; } 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 42fbf5a78b..0ede8713f0 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts @@ -1,8 +1,7 @@ -import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/lib"; import { - transformAvailabilityForClient, - transformDateOverridesForClient, - transformWorkingHoursForClient, + transformAvailabilityForAtom, + transformDateOverridesForAtom, + transformWorkingHoursForAtom, } from "@calcom/lib"; import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser"; import { prisma } from "@calcom/prisma"; @@ -20,7 +19,7 @@ type GetOptions = { input: TGetInputSchema; }; -export const getHandler = async ({ ctx, input }: GetOptions): Promise => { +export const getHandler = async ({ ctx, input }: GetOptions) => { const { user } = ctx; const schedule = await prisma.schedule.findUnique({ @@ -66,11 +65,11 @@ export const getHandler = async ({ ctx, input }: GetOptions): Promise { }, }); - const userAvailability = transformScheduleToAvailabilityForClient(schedule); + const userAvailability = transformScheduleToAvailabilityForAtom(schedule); return { schedule, diff --git a/yarn.lock b/yarn.lock index 65a336a801..69dbae83ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4006,7 +4006,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/atoms@*, @calcom/atoms@workspace:^, @calcom/atoms@workspace:packages/platform/atoms": +"@calcom/atoms@*, @calcom/atoms@workspace:packages/platform/atoms": version: 0.0.0-use.local resolution: "@calcom/atoms@workspace:packages/platform/atoms" dependencies: @@ -4015,6 +4015,7 @@ __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 @@ -4022,6 +4023,7 @@ __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 @@ -4032,6 +4034,7 @@ __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 @@ -5388,7 +5391,6 @@ __metadata: dependencies: "@algora/sdk": ^0.1.2 "@calcom/app-store": "*" - "@calcom/atoms": "workspace:^" "@calcom/config": "*" "@calcom/dayjs": "*" "@calcom/embed-react": "workspace:^" @@ -5434,7 +5436,6 @@ __metadata: "@vercel/og": ^0.5.0 autoprefixer: ^10.4.12 bcryptjs: ^2.4.3 - class-variance-authority: ^0.7.0 clsx: ^1.2.1 cobe: ^0.4.1 concurrently: ^7.6.0 @@ -5447,7 +5448,6 @@ __metadata: env-cmd: ^10.1.0 eslint: ^8.34.0 fathom-client: ^3.5.0 - framer-motion: ^11.0.25 globby: ^13.1.3 graphql: ^16.8.0 graphql-codegen: ^0.4.0 @@ -5457,7 +5457,7 @@ __metadata: i18n-unused: ^0.13.0 iframe-resizer-react: ^1.1.0 keen-slider: ^6.8.0 - lucide-react: ^0.364.0 + lucide-react: ^0.171.0 micro: ^10.0.1 next: ^14.1.3 next-auth: ^4.22.1 @@ -5469,7 +5469,7 @@ __metadata: prism-react-renderer: ^1.3.5 react: ^18.2.0 react-confetti: ^6.0.1 - react-datocms: ^5.0.3 + react-datocms: ^3.1.0 react-device-detect: ^2.2.2 react-dom: ^18.2.0 react-fast-marquee: ^1.6.4 @@ -5481,7 +5481,6 @@ __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 @@ -9181,59 +9180,6 @@ __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" @@ -16197,7 +16143,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.10": +"@types/jest@npm:^29.5.10, @types/jest@npm:^29.5.12": version: 29.5.12 resolution: "@types/jest@npm:29.5.12" dependencies: @@ -20483,15 +20429,6 @@ __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" @@ -20956,15 +20893,6 @@ __metadata: languageName: node linkType: hard -"class-variance-authority@npm:^0.7.0": - version: 0.7.0 - resolution: "class-variance-authority@npm:0.7.0" - dependencies: - clsx: 2.0.0 - checksum: e7fd1fab433ef06f52a1b7b241b70b4a185864deef199d3b0a2c3412f1cc179517288264c383f3b971a00d76811625fc8f7ffe709e6170219e88cd7368f08a20 - languageName: node - linkType: hard - "classnames@npm:^2.2.5, classnames@npm:^2.2.6": version: 2.3.2 resolution: "classnames@npm:2.3.2" @@ -21237,13 +21165,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e - languageName: node - linkType: hard - "clsx@npm:^1.1.1": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -22556,13 +22477,6 @@ __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" @@ -26618,26 +26532,6 @@ __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" @@ -28319,13 +28213,6 @@ __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" @@ -33120,6 +33007,15 @@ __metadata: languageName: node linkType: hard +"lucide-react@npm:^0.171.0": + version: 0.171.0 + resolution: "lucide-react@npm:0.171.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + checksum: 768ffe368c52a518ee339203d86ff4479989ab4d79c0716f721900c4bb7392ef6ff7a14807f6a685abd74d27f4c1778170bff77a0ab4c3e06c17944b557d8300 + languageName: node + linkType: hard + "lucide-react@npm:^0.364.0": version: 0.364.0 resolution: "lucide-react@npm:0.364.0" @@ -33688,20 +33584,6 @@ __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" @@ -35056,13 +34938,6 @@ __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" @@ -39344,21 +39219,20 @@ __metadata: languageName: node linkType: hard -"react-datocms@npm:^5.0.3": - version: 5.0.3 - resolution: "react-datocms@npm:5.0.3" +"react-datocms@npm:^3.1.0": + version: 3.1.4 + resolution: "react-datocms@npm:3.1.4" dependencies: - "@mux/mux-player-react": "*" datocms-listen: ^0.1.9 datocms-structured-text-generic-html-renderer: ^2.0.1 datocms-structured-text-utils: ^2.0.1 - react-intersection-observer: ^9.4.3 + react-intersection-observer: ^8.33.1 react-string-replace: ^1.1.0 universal-base64: ^2.1.0 use-deep-compare-effect: ^1.6.1 peerDependencies: react: ">= 16.12.0" - checksum: 22c20152afb54424acfe967a2c8c525cd9f132a33374f2aba0231f16ea64dade389b096e2dac8de9ffded612bc32e9891d725609ee947639fe1cef907cb143f5 + checksum: 54aba12aef4937175c2011548a8a576c96c8d8a596e84d191826910624c1d596e76a49782689dc236388a10803b02e700ac820cb7500cca7fd147a81f6c544c3 languageName: node linkType: hard @@ -39610,16 +39484,12 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.4.3": - version: 9.8.2 - resolution: "react-intersection-observer@npm:9.8.2" +"react-intersection-observer@npm:^8.33.1": + version: 8.34.0 + resolution: "react-intersection-observer@npm:8.34.0" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react-dom: - optional: true - checksum: 0e77226cf458499959773da9db9e00ae33a6eb95c2fa1ec3f506bbcba00955c1c27d2320fba8cdd740cadb1c7d6cc396386cbd9c359298cff196cac660dbee49 + react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0 + checksum: 7713fecfd1512c7f5a60f9f0bf15403b8f8bbd4110bcafaeaea6de36a0e0eb60368c3638f99e9c97b75ad8fc787ea48c241dcb5c694f821d7f2976f709082cc5 languageName: node linkType: hard @@ -40143,18 +40013,6 @@ __metadata: languageName: node linkType: hard -"react-twitter-embed@npm:^4.0.4": - version: 4.0.4 - resolution: "react-twitter-embed@npm:4.0.4" - dependencies: - scriptjs: ^2.5.9 - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: cdb3c5bd04c4da0efa767476be47c0a3865fb6335f2a1b9e242170167b51615c38164223278cef60c77143c4bac27ba582cbea054d0af3f138104fa5ec537c4c - languageName: node - linkType: hard - "react-universal-interface@npm:^0.6.2": version: 0.6.2 resolution: "react-universal-interface@npm:0.6.2" @@ -41982,13 +41840,6 @@ __metadata: languageName: node linkType: hard -"scriptjs@npm:^2.5.9": - version: 2.5.9 - resolution: "scriptjs@npm:2.5.9" - checksum: fc84cb6b60b6fb9aa6f1b3bc59fc94b233bd5241ed3a04233579014382b5eb60640269c87d8657902acc09f9b785ee33230c218627cea00e653564bda8f5acb6 - languageName: node - linkType: hard - "scuid@npm:^1.1.0": version: 1.1.0 resolution: "scuid@npm:1.1.0" @@ -44875,7 +44726,7 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.1": +"ts-jest@npm:^29.1.1, ts-jest@npm:^29.1.2": version: 29.1.2 resolution: "ts-jest@npm:29.1.2" dependencies: