feat: add advanced tab attributes to api/v2/event-types (#16314)

* added bookerLayouts

* added requiresConfirmation

* added tests

* undo extra changes

* undo extra change

* fixed imports

* added requiresBookerEmailVerification

* added hideCalendarNotes

* added lockTimeZoneToggleOnBookingPage

* added eventTypeColor

* undo extra changes

* added seats

* added requiresConfirmationWillBlockSlot to requiresConfirmation

* refactor: Implement BaseEventType class to avoid duplication across EventType input/output classes

## D-R-Y ##

* temp-fix: will revert later

* Correct placement of users array from EventTypeOutput to TeamEventTypeOutput

* Refactor: Shifted transformation logic to controllers using interceptors and pipes for a cleaner service layer.

* small fix

* Refactor: Moved transformation logic from service to controller using interceptors and pipes

** organisations/event-types

* feat: adde tests for `validateEventTypeInputs`

* fix: failing test

* fix: layout validator

* renamed blockCalendarForUnconfirmedBookings to blockUnconfirmedBookingsInBooker

* testing changes

* added disabled state

* fix merege conflict

* renamed requiresConfirmation to confirmationPolicy

* renamed OutputEventTypesResponseInterceptor to OutputTeamEventTypesResponseInterceptor

* renamed `darkThemeColor` -> `darkThemeHex` and `lightThemeColor` -> `lightThemeHex`

* renamed `eventTypeColor` -> `color`

* changed `requiresConfirmation` -> `confirmationPolicy`

* added tests for disabled state

* added class-validators to `disabled?: false`

* Revert: Split create and update input/output classes

* feat: Refactor responses to use DTOs with plainToClass for explicit property control

	•	Updated all endpoints to return response DTOs (e.g., CreateTeamEventTypeOutput, GetTeamEventTypeOutput) to ensure consistent Swagger documentation generation.
	•	Introduced plainToClass for transforming responses, enabling explicit control over which properties are returned in the response, following Morgan’s recommendation.
	•	Replaced generic HandlerResponse type with class-validator based DTOs for each endpoint, ensuring TypeScript enforces correct response structure.
	•	Applied strategy: "excludeAll" to limit response properties to only those explicitly defined in the DTO, preventing unintended fields from being included in the API responses.

* added pipes to transform output data

* add clean script to platform-types package.json

* added customName

* added destination calendar

* fixed type errors

* added useDestinationCalendarEmail

* refactor: api-reqest and api-response

* Update event-type.output.ts

* fixed errors

* Update yarn.lock

* added some missed properties to output

* Update constants.ts

* removed return type

* fixed some type errors

* reuse types

* Improve readability of validation functions

* Update CHANGELOG.md

* chore: post publish platform libraries

* Update documentation.json

* fix: reset platform libraries to 0.0.0

* fixup! fix: reset platform libraries to 0.0.0

* Update event-type.tranformed.ts

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
authored and GitHub committed 2024-09-24 13:36:05 +03:00
1 parent 6de454b071
commit 2e2a6c73b1
68 files changed
+2859 -6305

No files matched your search

@@ -1,5 +1,6 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -15,7 +16,10 @@ import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/e
import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -34,22 +38,33 @@ import {
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
SkipTakePagination,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
export type EventTypeHandlerResponse = {
data: DatabaseTeamEventType[] | DatabaseTeamEventType;
status: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
};
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@DocsTags("Organizations Event Types")
export class OrganizationsEventTypesController {
constructor(private readonly organizationsEventTypesService: OrganizationsEventTypesService) {}
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@@ -61,16 +76,22 @@ export class OrganizationsEventTypesController {
@Param("orgId", ParseIntPipe) orgId: number,
@Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14
): Promise<CreateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput(
user.id,
teamId,
bodyEventType
);
const eventType = await this.organizationsEventTypesService.createTeamEventType(
user,
teamId,
orgId,
bodyEventType
transformedBody
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -90,7 +111,9 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventType,
data: (await this.outputTeamEventTypesResponsePipe.transform(
eventType
)) as TeamEventTypeOutput_2024_06_14,
};
}
@@ -125,12 +148,13 @@ export class OrganizationsEventTypesController {
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
return {
status: SUCCESS_STATUS,
data: eventType ? [eventType] : [],
data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []),
};
}
@@ -138,7 +162,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -155,7 +179,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -169,16 +193,23 @@ export class OrganizationsEventTypesController {
@GetUser() user: UserWithProfile,
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
): Promise<UpdateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput(
user.id,
eventTypeId,
teamId,
bodyEventType
);
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
eventTypeId,
teamId,
bodyEventType,
transformedBody,
user
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -189,7 +220,7 @@ export class OrganizationsEventTypesController {
@HttpCode(HttpStatus.OK)
async deleteTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId") eventTypeId: number
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<DeleteTeamEventTypeOutput> {
const eventType = await this.organizationsEventTypesService.deleteTeamEventType(teamId, eventTypeId);
@@ -17,7 +17,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateTeamEventTypeInput_2024_06_14,
@@ -276,6 +281,30 @@ describe("Organizations Event Types Endpoints", () => {
value: 30,
rolling: true,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
},
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
};
return request(app.getHttpServer())
@@ -297,6 +326,12 @@ describe("Organizations Event Types Endpoints", () => {
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(data.offsetStart).toEqual(body.offsetStart);
expect(data.bookingWindow).toEqual(body.bookingWindow);
expect(data.bookerLayouts).toEqual(body.bookerLayouts);
expect(data.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
collectiveEventType = responseBody.data;
});
@@ -444,7 +479,7 @@ describe("Organizations Event Types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`)
.send(body)
.expect(404);
.expect(400);
});
it("should update collective event-type", async () => {
@@ -0,0 +1,38 @@
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class OutputTeamEventTypesResponsePipe implements PipeTransform {
constructor(private readonly outputOrganizationsEventTypesService: OutputOrganizationsEventTypesService) {}
private async transformEventType(item: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14> {
return plainToClass(
TeamEventTypeOutput_2024_06_14,
await this.outputOrganizationsEventTypesService.getResponseTeamEventType(item),
{ strategy: "exposeAll" }
);
}
// Implementing function overloading to ensure correct return types based on input type:
async transform(value: DatabaseTeamEventType[]): Promise<TeamEventTypeOutput_2024_06_14[]>;
async transform(value: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]> {
if (Array.isArray(value)) {
return await Promise.all(value.map((item) => this.transformEventType(item)));
} else {
return await this.transformEventType(value);
}
}
}