refactor: v2 event-types (#15457)

* refactor: UserAvatarGroupWithOrg.tsx remove unecessary profile prop

* chore: define event-type types in platform/types

* feat: event-type transformes in lib and re-exported through platform-libraries

* chore: define user types in platform/types

* chore: add users endpoint to platform-constants

* refactor: app-store use narrowed down booker event type

* chore: version old event-types module

* fix: old event-types e2e test

* fix: libraries add missing export

* fix: reset libraries version to 0

* feat: new event-types module and users endpoints

* feat: make booker atom work with v2 event-types

* updating event type

* refactor: remove guard for get event-types

* refactor: move private hook to public

* Revert "refactor: remove guard for get event-types"

This reverts commit d41204069f1d5bb1305b388ce53399f9175f4249.

* Revert "refactor: move private hook to public"

This reverts commit 09322e8fba102e57104959f79ed6bfa9a677dc9d.

* refactor: get by username and slug in /event-types

* remove console log

* feat: locations handle displayEventPublicly

* test e2e event-types

* revert: user output from types

* refactor: event-types hooks have private and public folders

* refactor: require event-type.slug in input

* refactor: demand that all labels of booking fields are unique

* refactor: remove unused import

* refactor: only have email and name by default in booker

* refactor: add booking field slug

* fix: display event-type location publicly / hide it in booker

* fix: display event-type location publicly / hide it in booker

* fix: packages/lib tests

* fix: typescript in e2e test

* fix: dynamic event types input

* refactor: use IsIn instead of IsEnum

* refactor: simplify getEventTypes

* fix: use ApiAuthGuard instead of AccessTokenGuard

* chore: export more stuff from libraries

* refactor: SchedulingTypeEnum and Type

---------

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

Some files were not shown because too many files have changed in this diff Show More