Revert "refactor: api v2 schedules (#14870)" (#15130)

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