refactor: v2 default booking fields (#18287)

* feat: allow title,notes,reschedule reason inputs

* refactor: v2 event type default booking fields as in web app

* wip: input validation

* input field validation

* fix: tests

* chore: bump libraries

* remove change in docs

* fix input type

* chore: ensure additional notes at the end

* fixup! Merge branch 'main' into lauris/cal-4932-platform-refactor-default-booking-fields

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Lauris Skraucis
2025-01-06 16:49:57 +02:00
committed by GitHub
co-authored by Morgan Vernay Morgan
parent 657f9dfd41
commit c8172ab790
19 changed files with 7050 additions and 611 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.79",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -952,7 +952,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
slug: recurringSeatedTventTypeSlug,
});
expect(firstBooking.attendees.length).toEqual(2);
expect(firstBooking.attendees[0]).toEqual({
const firstAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeOne
);
expect(firstAttendee).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
@@ -965,7 +968,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
expect(firstBooking.attendees[1]).toEqual({
const secondAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeTwo
);
expect(secondAttendee).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
@@ -33,11 +33,18 @@ import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
NameFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/enums";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
if (a.slug > b.slug) return 1;
return 0;
};
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
let app: INestApplication;
@@ -93,6 +100,68 @@ describe("Event types Endpoints", () => {
let orgUserEventType1: EventType;
let orgUserEventType2: EventType;
let orgUserEventType3: EventType;
const defaultResponseBookingFieldName = {
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldEmail = {
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldLocation = {
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldTitle = {
isDefault: true,
type: "text",
slug: "title",
required: true,
hidden: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldNotes = {
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldGuests = {
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldRescheduleReason = {
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
hidden: false,
disableOnPrefill: false,
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -253,7 +322,7 @@ describe("Event types Endpoints", () => {
});
it("should create an event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
@@ -379,30 +448,25 @@ describe("Event types Endpoints", () => {
);
expect(createdEventType.color).toEqual(body.color);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
// note(Lauris): location booking field is added if multiple locations are passed
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
{
isDefault: true,
required: false,
slug: "location",
type: "radioInput",
disableOnPrefill: false,
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: true,
hidden: false,
isDefault: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldNotes },
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
@@ -432,7 +496,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -455,7 +521,6 @@ describe("Event types Endpoints", () => {
});
it(`/GET/event-types by username and orgSlug`, async () => {
console.log(organization);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
@@ -636,7 +701,6 @@ describe("Event types Endpoints", () => {
.expect(201);
const createdEventType = createResponse.body.data;
console.log("createdEventType: ", createdEventType);
expect(createdEventType).toMatchObject({
id: expect.any(Number),
@@ -779,13 +843,20 @@ describe("Event types Endpoints", () => {
});
it("should update event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
disableOnPrefill: true,
};
const notesBookingField: NotesDefaultFieldInput_2024_06_14 = {
slug: "notes",
label: "lemme take some notes",
placeholder: "write your notes here",
required: true,
};
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
@@ -804,6 +875,7 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
notesBookingField,
],
bookingLimitsCount: {
day: 4,
@@ -866,21 +938,25 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: false,
hidden: false,
isDefault: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
];
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
@@ -997,7 +1073,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -1091,14 +1169,6 @@ describe("Event types Endpoints", () => {
placeholder: "",
},
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
@@ -1116,6 +1186,14 @@ describe("Event types Endpoints", () => {
hidden: false,
},
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
];
beforeAll(async () => {
@@ -1239,15 +1317,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1283,6 +1352,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1369,15 +1447,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1413,6 +1482,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1426,19 +1504,21 @@ describe("Event types Endpoints", () => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
]);
expect(fetchedEventType.bookingFields.sort(orderBySlug)).toEqual(
[
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
...expectedReturnSystemFields,
].sort(orderBySlug)
);
});
});
@@ -1541,14 +1621,6 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
@@ -1565,6 +1637,14 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
@@ -5,16 +5,19 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import {
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldTitle,
systemAfterFieldNotes,
systemAfterFieldGuests,
systemAfterFieldRescheduleReason,
EventTypeMetaDataSchema,
systemBeforeFieldLocation,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
transformEventColorsApiToInternal,
@@ -217,27 +220,46 @@ export class InputEventTypesService_2024_06_14 {
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const customFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
const systemCustomFields = internalFields.filter((field) => !this.isUserCustomField(field));
const userCustomFields = internalFields.filter((field) => this.isUserCustomField(field));
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
const systemCustomRescheduleReasonField = systemCustomFields?.find(
(field) => field.name === "rescheduleReason"
);
const customNameField = customFields?.find((field) => field.type === "name");
const customEmailField = customFields?.find((field) => field.type === "email");
const defaultFieldsBefore: (SystemField | CustomField)[] = [
customNameField || systemBeforeFieldName,
customEmailField || systemBeforeFieldEmail,
systemCustomNameField || systemBeforeFieldName,
systemCustomEmailField || systemBeforeFieldEmail,
systemBeforeFieldLocation,
];
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
if (hasMultipleLocations) {
defaultFieldsBefore.push(systemBeforeFieldLocation);
}
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
const defaultFieldsAfter = [
systemCustomTitleField || systemAfterFieldTitle,
systemCustomNotesField || systemAfterFieldNotes,
systemCustomGuestsField || systemAfterFieldGuests,
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
return (
field.type !== "name" &&
field.type !== "email" &&
field.name !== "title" &&
field.name !== "notes" &&
field.name !== "guests" &&
field.name !== "rescheduleReason"
);
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
@@ -242,7 +242,19 @@ export class OutputEventTypesService_2024_06_14 {
}
}
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
const fields = [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
// ensure additional notes are always at the end
return fields.sort((a, b) => {
if (!a?.slug || !b?.slug) return 0;
if (a.slug === "notes" && b.slug !== "notes") {
return 1;
}
if (b.slug === "notes" && a.slug !== "notes") {
return -1;
}
return 0;
});
}
getDefaultBookingFields(isOrgTeamEvent: boolean) {
@@ -677,27 +677,9 @@ describe("Organizations Event Types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: false,
hidden: true,
},
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
{
isDefault: true,
type: "radioInput",
@@ -708,33 +690,33 @@ describe("Organizations Event Types Endpoints", () => {
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
slug: "title",
type: "text",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
type: "multiemail",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
+274 -82
View File
@@ -7741,7 +7741,7 @@
"type"
]
},
"NameFieldInput_2024_06_14": {
"NameDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7766,7 +7766,7 @@
"placeholder"
]
},
"EmailFieldInput_2024_06_14": {
"EmailDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7791,6 +7791,126 @@
"placeholder"
]
},
"TitleDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "title",
"description": "only allowed value for type is `title`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=journey`, the title field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"NotesDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "notes",
"description": "only allowed value for type is `notes`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=journey`, the notes field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"GuestsDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "guests",
"description": "only allowed value for type is `guests`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=bob@cal.com`, the guests field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"RescheduleReasonDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=travel`, the rescheduleReason field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"BookerLayouts_2024_06_14": {
"type": "object",
"properties": {
@@ -7921,10 +8041,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -8475,20 +8607,19 @@
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`",
"default": "rescheduleReason"
},
"type": {
"type": "string",
"default": "textarea"
},
"required": {
"type": "boolean"
},
@@ -8505,33 +8636,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "title"
},
"type": {
"type": "string",
"default": "text"
"default": "textarea"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "title",
"description": "only allowed value for type is `title`",
"default": "title"
},
"required": {
"type": "boolean"
@@ -8549,33 +8686,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "notes"
},
"type": {
"type": "string",
"default": "textarea"
"default": "text"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "notes",
"description": "only allowed value for type is `notes`",
"default": "notes"
},
"required": {
"type": "boolean"
@@ -8593,33 +8736,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=hello`, the notes field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "guests"
},
"type": {
"type": "string",
"default": "multiemail"
"default": "textarea"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "guests",
"description": "only allowed value for type is `guests`",
"default": "guests"
},
"required": {
"type": "boolean"
@@ -8637,15 +8786,22 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=lauris@cal.com`, the guests field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"type": {
"type": "string",
"default": "multiemail"
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
"isDefault",
"type"
]
},
"AddressFieldOutput_2024_06_14": {
@@ -9776,10 +9932,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -11353,10 +11521,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -12178,10 +12358,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
+229 -57
View File
@@ -7173,7 +7173,7 @@
},
"required": ["type"]
},
"NameFieldInput_2024_06_14": {
"NameDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7194,7 +7194,7 @@
},
"required": ["type", "label", "placeholder"]
},
"EmailFieldInput_2024_06_14": {
"EmailDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7215,6 +7215,118 @@
},
"required": ["type", "label", "placeholder"]
},
"TitleDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "title",
"description": "only allowed value for type is `title`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=journey`, the title field will be prefilled with this value and disabled."
}
},
"required": ["slug"]
},
"NotesDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "notes",
"description": "only allowed value for type is `notes`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=journey`, the notes field will be prefilled with this value and disabled."
}
},
"required": ["slug"]
},
"GuestsDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "guests",
"description": "only allowed value for type is `guests`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=bob@cal.com`, the guests field will be prefilled with this value and disabled."
}
},
"required": ["slug"]
},
"RescheduleReasonDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=travel`, the rescheduleReason field will be prefilled with this value and disabled."
}
},
"required": ["slug"]
},
"BookerLayouts_2024_06_14": {
"type": "object",
"properties": {
@@ -7324,10 +7436,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -7837,20 +7961,13 @@
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"enum": ["name", "email", "title", "notes", "guests"],
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`",
"default": "rescheduleReason"
},
"type": {
"type": "string",
"default": "textarea"
},
"required": {
"type": "boolean"
},
@@ -7867,26 +7984,29 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
}
},
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "title"
},
"type": {
"type": "string",
"default": "text"
"default": "textarea"
}
},
"required": ["slug", "isDefault", "type"]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": ["name", "email", "title", "notes", "guests"],
"example": "title",
"description": "only allowed value for type is `title`",
"default": "title"
},
"required": {
"type": "boolean"
@@ -7904,26 +8024,29 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
}
},
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "notes"
},
"type": {
"type": "string",
"default": "textarea"
"default": "text"
}
},
"required": ["slug", "isDefault", "type"]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": ["name", "email", "title", "notes", "guests"],
"example": "notes",
"description": "only allowed value for type is `notes`",
"default": "notes"
},
"required": {
"type": "boolean"
@@ -7941,26 +8064,29 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=hello`, the notes field will be prefilled with this value and disabled."
}
},
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "guests"
},
"type": {
"type": "string",
"default": "multiemail"
"default": "textarea"
}
},
"required": ["slug", "isDefault", "type"]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": ["name", "email", "title", "notes", "guests"],
"example": "guests",
"description": "only allowed value for type is `guests`",
"default": "guests"
},
"required": {
"type": "boolean"
@@ -7978,9 +8104,19 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=lauris@cal.com`, the guests field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"type": {
"type": "string",
"default": "multiemail"
}
},
"required": ["isDefault", "slug", "type", "required", "hidden", "disableOnPrefill"]
"required": ["slug", "isDefault", "type"]
},
"AddressFieldOutput_2024_06_14": {
"type": "object",
@@ -8991,10 +9127,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -10334,10 +10482,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -11093,10 +11253,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -32,7 +32,7 @@ import {
} from "../internal-to-api/booking-fields";
import {
transformLocationsApiToInternal,
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
transformSelectOptionsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
@@ -151,7 +151,7 @@ describe("transformLocationsApiToInternal", () => {
});
});
describe("transformBookingFieldsApiRequestToInternal", () => {
describe("transformBookingFieldsApiToInternal", () => {
it("should transform name field", () => {
const bookingField: InputBookingField_2024_06_14 = {
type: "name",
@@ -176,7 +176,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
const expectedOutput: SystemField[] = [expectedField];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -200,7 +200,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -237,7 +237,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -274,7 +274,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -311,7 +311,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -348,7 +348,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -385,7 +385,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -424,7 +424,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -455,14 +455,13 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
],
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptionsApiToInternal(bookingField.options),
disableOnPrefill: false,
hidden: false,
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -499,7 +498,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -530,14 +529,13 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
],
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptionsApiToInternal(bookingField.options),
disableOnPrefill: false,
hidden: false,
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -568,14 +566,13 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
],
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptionsApiToInternal(bookingField.options),
disableOnPrefill: false,
hidden: false,
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -611,7 +608,7 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -645,14 +642,13 @@ describe("transformBookingFieldsApiRequestToInternal", () => {
],
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptionsApiToInternal(bookingField.options),
disableOnPrefill,
hidden: false,
},
];
const result = transformBookingFieldsApiRequestToInternal(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -1,4 +1,17 @@
import type { InputBookingField_2024_06_14, OutputBookingField_2024_06_14 } from "@calcom/platform-types";
import type {
InputBookingField_2024_06_14,
LocationDefaultFieldOutput_2024_06_14,
PhoneDefaultFieldOutput_2024_06_14,
MultiSelectFieldInput_2024_06_14,
CheckboxGroupFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
} from "@calcom/platform-types";
import {
systemBeforeFieldEmail,
@@ -13,11 +26,14 @@ import {
type SystemField,
} from "../internal-to-api";
export function transformBookingFieldsApiRequestToInternal(
inputBookingFields: InputBookingField_2024_06_14[]
): (SystemField | CustomField)[] {
const customBookingFields = inputBookingFields.map((field) => {
const baseProperties = getBasePropertiesRequest(field);
type InputBookingField =
| InputBookingField_2024_06_14
| LocationDefaultFieldOutput_2024_06_14
| PhoneDefaultFieldOutput_2024_06_14;
export function transformBookingFieldsApiToInternal(bookingFields: InputBookingField[]) {
const customBookingFields = bookingFields.map((field) => {
const baseProperties = getBaseProperties(field);
const options =
"options" in field && field.options ? transformSelectOptionsApiToInternal(field.options) : undefined;
@@ -35,42 +51,12 @@ export function transformBookingFieldsApiRequestToInternal(
return customBookingFields;
}
function getBasePropertiesRequest(field: InputBookingField_2024_06_14): SystemField | CustomField {
if (field.type === "name") {
const systemName = { ...systemBeforeFieldName };
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].label = field.label;
}
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].placeholder = field.placeholder;
}
// note(Lauris): we attach top level label and placeholder for easier access when converting database event type
// to v2 response event type even though form builder uses label and placeholder from variantsConfig.
systemName.label = field.label;
systemName.placeholder = field.placeholder;
return {
...systemName,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.type === "email") {
return {
...systemBeforeFieldEmail,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.type === "boolean") {
function getBaseProperties(field: InputBookingField): CustomField | SystemField {
if (fieldIsSelect(field)) {
return {
name: field.slug,
type: field.type,
label: field.label,
labelAsSafeHtml: `<p>${field.label}</p>\n`,
sources: [
{
id: "user",
@@ -82,88 +68,11 @@ function getBasePropertiesRequest(field: InputBookingField_2024_06_14): SystemFi
editable: "user",
required: field.required,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
hidden: "hidden" in field ? field.hidden : false,
};
}
return {
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 : "",
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
};
}
export function transformSelectOptionsApiToInternal(options: string[]) {
return options.map((option) => ({
label: option,
value: option,
}));
}
export function transformBookingFieldsApiResponseToInternal(bookingFields: OutputBookingField_2024_06_14[]) {
const customBookingFields = bookingFields.map((field) => {
const baseProperties = getBasePropertiesResponse(field);
const options =
"options" in field && field.options ? transformSelectOptionsApiToInternal(field.options) : undefined;
if (!options) {
return baseProperties;
}
return {
...baseProperties,
options,
};
});
return customBookingFields;
}
function getBasePropertiesResponse(field: OutputBookingField_2024_06_14) {
if (field.type === "name") {
const systemName = { ...systemBeforeFieldName };
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].label = field.label;
}
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].placeholder = field.placeholder;
}
// note(Lauris): we attach top level label and placeholder for easier access when converting database event type
// to v2 response event type even though form builder uses label and placeholder from variantsConfig.
systemName.label = field.label;
systemName.placeholder = field.placeholder;
return {
...systemName,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.type === "email") {
return {
...systemBeforeFieldEmail,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.slug === "location") {
if (fieldIsDefaultSystemLocation(field)) {
return {
...systemBeforeFieldLocation,
required: field.required,
@@ -171,51 +80,80 @@ function getBasePropertiesResponse(field: OutputBookingField_2024_06_14) {
};
}
if (field.slug === "attendeePhoneNumber") {
if (fieldIsDefaultAttendeePhone(field)) {
return {
...systemBeforeFieldPhone,
required: field.required,
};
}
if (field.slug === "rescheduleReason") {
if (fieldIsCustomSystemName(field)) {
const systemName = { ...systemBeforeFieldName };
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].label = field.label;
}
if (systemName.variantsConfig?.variants?.fullName?.fields?.[0]) {
systemName.variantsConfig.variants.fullName.fields[0].placeholder = field.placeholder;
}
// note(Lauris): we attach top level label and placeholder for easier access when converting database event type
// to v2 response event type even though form builder uses label and placeholder from variantsConfig.
systemName.label = field.label;
systemName.placeholder = field.placeholder;
return {
...systemName,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (fieldIsCustomSystemEmail(field)) {
return {
...systemBeforeFieldEmail,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (fieldIsCustomSystemRescheduleReason(field)) {
return {
...systemAfterFieldRescheduleReason,
required: field.required,
hidden: field.hidden,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: "placeholder" in field ? field.placeholder : "",
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.slug === "title") {
if (fieldIsCustomSystemTitle(field)) {
return {
...systemAfterFieldTitle,
required: field.required,
hidden: field.hidden,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: "placeholder" in field ? field.placeholder : "",
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.slug === "notes") {
if (fieldIsCustomSystemNotes(field)) {
return {
...systemAfterFieldNotes,
required: field.required,
hidden: field.hidden,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: "placeholder" in field ? field.placeholder : "",
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (field.slug === "guests") {
if (fieldIsCustomSystemGuests(field)) {
return {
...systemAfterFieldGuests,
required: field.required,
hidden: field.hidden,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: "placeholder" in field ? field.placeholder : "",
disableOnPrefill: !!field.disableOnPrefill,
@@ -237,7 +175,7 @@ function getBasePropertiesResponse(field: OutputBookingField_2024_06_14) {
},
],
editable: "user",
required: field.required,
required: !!field.required,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
};
@@ -256,9 +194,63 @@ function getBasePropertiesResponse(field: OutputBookingField_2024_06_14) {
},
],
editable: "user",
required: "required" in field ? field.required : false,
placeholder: "placeholder" in field && field.placeholder ? field.placeholder : "",
disableOnPrefill: "disableOnPrefill" in field ? !!field.disableOnPrefill : false,
hidden: "hidden" in field ? field.hidden : undefined,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
};
}
function fieldIsCustomSystemName(field: InputBookingField): field is NameDefaultFieldInput_2024_06_14 {
return "type" in field && field.type === "name";
}
function fieldIsCustomSystemEmail(field: InputBookingField): field is EmailDefaultFieldInput_2024_06_14 {
return "type" in field && field.type === "email";
}
function fieldIsCustomSystemTitle(field: InputBookingField): field is TitleDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "title";
}
function fieldIsCustomSystemNotes(field: InputBookingField): field is NotesDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "notes";
}
function fieldIsCustomSystemGuests(field: InputBookingField): field is GuestsDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "guests";
}
function fieldIsCustomSystemRescheduleReason(
field: InputBookingField
): field is RescheduleReasonDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "rescheduleReason";
}
function fieldIsDefaultAttendeePhone(field: InputBookingField): field is PhoneDefaultFieldOutput_2024_06_14 {
return "slug" in field && field.slug === "attendeePhoneNumber";
}
function fieldIsDefaultSystemLocation(
field: InputBookingField
): field is LocationDefaultFieldOutput_2024_06_14 {
return "slug" in field && field.slug === "location";
}
function fieldIsSelect(
field: InputBookingField
): field is
| CheckboxGroupFieldInput_2024_06_14
| RadioGroupFieldInput_2024_06_14
| MultiSelectFieldInput_2024_06_14 {
return (
"type" in field && (field.type === "checkbox" || field.type === "radio" || field.type === "multiselect")
);
}
export function transformSelectOptionsApiToInternal(options: string[]) {
return options.map((option) => ({
label: option,
value: option,
}));
}
@@ -26,7 +26,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
@@ -36,7 +36,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
@@ -46,7 +46,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: field.placeholder,
@@ -57,7 +57,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: field.placeholder,
@@ -68,7 +68,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: field.placeholder,
@@ -79,7 +79,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: field.placeholder,
@@ -90,7 +90,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
label: field.label,
placeholder: field.placeholder,
@@ -101,7 +101,7 @@ export function transformBookingFieldsInternalToApi(
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
required: !!field.required,
hidden: !!field.hidden,
};
default:
@@ -122,7 +122,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -133,7 +133,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -144,7 +144,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -155,7 +155,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -166,7 +166,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -177,7 +177,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -188,7 +188,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
};
@@ -198,7 +198,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
placeholder: field.placeholder,
options: field.options ? field.options.map((option) => option.value) : [],
disableOnPrefill: !!field.disableOnPrefill,
@@ -210,7 +210,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -221,7 +221,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -232,7 +232,7 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
required: !!field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
@@ -542,7 +542,7 @@ export const systemAfterFieldTitle: TitleSystemField = {
name: "title",
editable: "system-but-optional",
required: true,
hidden: false,
hidden: true,
defaultPlaceholder: "",
sources: [
{
@@ -2,7 +2,7 @@ import { defaultEvents } from "@calcom/lib/defaultEvents";
import type { SystemField } from "@calcom/lib/event-types/transformers";
import {
transformLocationsApiToInternal,
transformBookingFieldsApiResponseToInternal,
transformBookingFieldsApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
@@ -16,10 +16,10 @@ import {
} from "@calcom/lib/event-types/transformers";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import type {
CustomFieldOutput_2024_06_14,
EmailDefaultFieldOutput_2024_06_14,
EventTypeOutput_2024_06_14,
InputLocation_2024_06_14,
KnownBookingField_2024_06_14,
NameDefaultFieldOutput_2024_06_14,
TeamEventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
@@ -329,7 +329,9 @@ function getBookingFields(
bookingFields: EventTypeOutput_2024_06_14["bookingFields"],
defaultFormValues: BookerPlatformWrapperAtomProps["defaultFormValues"] | undefined
) {
const transformedBookingFields = transformBookingFieldsApiResponseToInternal(bookingFields);
const transformedBookingFields = transformBookingFieldsApiToInternal(
bookingFields.filter((field) => isKnownField(field))
);
const hasNameField = transformedBookingFields.some((field) => field.name === "name");
const hasEmailField = transformedBookingFields.some((field) => field.name === "email");
@@ -369,10 +371,10 @@ function getBookingFields(
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(fieldsWithSystem);
}
function isCustomField(
function isKnownField(
field: EventTypeOutput_2024_06_14["bookingFields"][number]
): field is CustomFieldOutput_2024_06_14 {
return field.type !== "unknown" && !field.isDefault;
): field is KnownBookingField_2024_06_14 {
return field.type !== "unknown";
}
function getBookingWindow(inputBookingWindow: EventTypeOutput_2024_06_14["bookingWindow"]) {
+4 -1
View File
@@ -126,7 +126,7 @@ export { EventTypeMetaDataSchema, userMetadata, bookingMetadataSchema } from "@c
export {
// note(Lauris): Api to internal
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
@@ -155,6 +155,9 @@ export {
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldRescheduleReason,
systemAfterFieldTitle,
systemAfterFieldNotes,
systemAfterFieldGuests,
} from "@calcom/lib/event-types/transformers";
export type {
@@ -21,9 +21,14 @@ const inputBookingFieldTypes = [
"boolean",
] as const;
export class NameFieldInput_2024_06_14 {
const inputBookingFieldSlugs = ["name", "email", "title", "notes", "guests"] as const;
export class NameDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({ example: "name", description: "only allowed value for type is `name`" })
@DocsProperty({
example: "name",
description: "only allowed value for type is `name`",
})
type!: "name";
@IsString()
@@ -48,9 +53,12 @@ export class NameFieldInput_2024_06_14 {
disableOnPrefill?: boolean;
}
export class EmailFieldInput_2024_06_14 {
export class EmailDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({ example: "email", description: "only allowed value for type is `email`" })
@DocsProperty({
example: "email",
description: "only allowed value for type is `email`",
})
type!: "email";
@IsString()
@@ -74,6 +82,170 @@ export class EmailFieldInput_2024_06_14 {
})
disableOnPrefill?: boolean;
}
export class TitleDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldSlugs)
@DocsProperty({ example: "title", description: "only allowed value for type is `title`" })
slug!: "title";
@IsBoolean()
@IsOptional()
@DocsPropertyOptional()
required?: boolean;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
description:
"If true show under event type settings but don't show this booking field in the Booker. If false show in both.",
})
hidden?: boolean;
@IsString()
@IsOptional()
@DocsPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@DocsPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
type: Boolean,
description:
"Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value.\
For example, if URL contains query parameter `&title=journey`,\
the title field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class NotesDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldSlugs)
@DocsProperty({ example: "notes", description: "only allowed value for type is `notes`" })
slug!: "notes";
@IsBoolean()
@IsOptional()
@DocsPropertyOptional()
required?: boolean;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
description:
"If true show under event type settings but don't show this booking field in the Booker. If false show in both.",
})
hidden?: boolean;
@IsString()
@IsOptional()
@DocsPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@DocsPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
type: Boolean,
description:
"Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value.\
For example, if URL contains query parameter `&notes=journey`,\
the notes field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class GuestsDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldSlugs)
@DocsProperty({ example: "guests", description: "only allowed value for type is `guests`" })
slug!: "guests";
@IsBoolean()
@IsOptional()
@DocsPropertyOptional()
required?: boolean;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
description:
"If true show under event type settings but don't show this booking field in the Booker. If false show in both.",
})
hidden?: boolean;
@IsString()
@IsOptional()
@DocsPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@DocsPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
type: Boolean,
description:
"Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value.\
For example, if URL contains query parameter `&guests=bob@cal.com`,\
the guests field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class RescheduleReasonDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldSlugs)
@DocsProperty({
example: "rescheduleReason",
description: "only allowed value for type is `rescheduleReason`",
})
slug!: "rescheduleReason";
@IsBoolean()
@IsOptional()
@DocsPropertyOptional()
required?: boolean;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
description:
"If true show under event type settings but don't show this booking field in the Booker. If false show in both.",
})
hidden?: boolean;
@IsString()
@IsOptional()
@DocsPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@DocsPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
type: Boolean,
description:
"Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value.\
For example, if URL contains query parameter `&rescheduleReason=travel`,\
the rescheduleReason field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class PhoneFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({ example: "phone", description: "only allowed value for type is `phone`" })
@@ -576,9 +748,16 @@ export class BooleanFieldInput_2024_06_14 {
hidden?: boolean;
}
type InputDefaultField_2024_06_14 =
| NameDefaultFieldInput_2024_06_14
| EmailDefaultFieldInput_2024_06_14
| TitleDefaultFieldInput_2024_06_14
| NotesDefaultFieldInput_2024_06_14
| GuestsDefaultFieldInput_2024_06_14
| RescheduleReasonDefaultFieldInput_2024_06_14;
export type InputBookingField_2024_06_14 =
| NameFieldInput_2024_06_14
| EmailFieldInput_2024_06_14
| InputDefaultField_2024_06_14
| PhoneFieldInput_2024_06_14
| AddressFieldInput_2024_06_14
| TextFieldInput_2024_06_14
@@ -593,9 +772,13 @@ export type InputBookingField_2024_06_14 =
@ValidatorConstraint({ async: true })
class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private classTypeMap: { [key: string]: new () => InputBookingField_2024_06_14 } = {
name: NameFieldInput_2024_06_14,
email: EmailFieldInput_2024_06_14,
private classMap: { [key: string]: new () => InputBookingField_2024_06_14 } = {
name: NameDefaultFieldInput_2024_06_14,
email: EmailDefaultFieldInput_2024_06_14,
title: TitleDefaultFieldInput_2024_06_14,
notes: NotesDefaultFieldInput_2024_06_14,
guests: GuestsDefaultFieldInput_2024_06_14,
rescheduleReason: RescheduleReasonDefaultFieldInput_2024_06_14,
phone: PhoneFieldInput_2024_06_14,
address: AddressFieldInput_2024_06_14,
text: TextFieldInput_2024_06_14,
@@ -609,8 +792,6 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
boolean: BooleanFieldInput_2024_06_14,
};
private reservedSystemSlugs = ["location", "rescheduleReason", "attendeePhoneNumber"];
async validate(bookingFields: { type: string; slug: string }[]) {
if (!Array.isArray(bookingFields)) {
throw new BadRequestException(`'bookingFields' must be an array.`);
@@ -623,20 +804,19 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
const slugs: string[] = [];
for (const field of bookingFields) {
const { type, slug } = field;
if (!type) {
throw new BadRequestException(`Each booking field must have a 'type' property.`);
}
const fieldNeedsType =
slug !== "title" && slug !== "notes" && slug !== "guests" && slug !== "rescheduleReason";
const notSystemEditableField = type !== "name" && type !== "email";
if (notSystemEditableField && !slug) {
throw new BadRequestException(`Each booking field must have a 'slug' property.`);
}
if (this.reservedSystemSlugs.includes(slug)) {
if (fieldNeedsType && !type) {
throw new BadRequestException(
`The slug '${slug}' is reserved and cannot be used, because it is a slug of a default booking field. Reserved slugs are: ${this.reservedSystemSlugs.join(
", "
)}`
`All booking fields except ones with slug equal to title, notes, guests and rescheduleReason must have a 'type' property.`
);
}
const fieldNeedsSlug = type !== "name" && type !== "email";
if (fieldNeedsSlug && !slug) {
throw new BadRequestException(
`Each booking field except ones with type equal to name and email must have a 'slug' property.`
);
}
@@ -645,13 +825,15 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
);
}
if (notSystemEditableField) {
if (fieldNeedsSlug) {
slugs.push(slug);
}
const ClassType = this.classTypeMap[type];
const ClassType = type ? this.classMap[type] : this.classMap[slug];
if (!ClassType) {
throw new BadRequestException(`Unsupported booking field type '${type}'.`);
throw new BadRequestException(
type ? `Unsupported booking field type '${type}'.` : `Unsupported booking field slug '${slug}'.`
);
}
const instance = plainToInstance(ClassType, field);
@@ -682,3 +864,51 @@ export function ValidateInputBookingFields_2024_06_14(validationOptions?: Valida
});
};
}
function isDefaultField(field: InputBookingField_2024_06_14): field is InputDefaultField_2024_06_14 {
if (
isDefaultNameField(field) ||
isDefaultEmailField(field) ||
isDefaultTitleField(field) ||
isDefaultNotesField(field) ||
isDefaultGuestsField(field) ||
isDefaultRescheduleReasonField(field)
) {
return true;
}
return false;
}
function isDefaultNameField(field: InputBookingField_2024_06_14): field is NameDefaultFieldInput_2024_06_14 {
return ("type" in field && field.type === "name") || ("slug" in field && field.slug === "name");
}
function isDefaultEmailField(
field: InputBookingField_2024_06_14
): field is EmailDefaultFieldInput_2024_06_14 {
return ("type" in field && field.type === "email") || ("slug" in field && field.slug === "email");
}
function isDefaultTitleField(
field: InputBookingField_2024_06_14
): field is TitleDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "title";
}
function isDefaultNotesField(
field: InputBookingField_2024_06_14
): field is NotesDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "notes";
}
function isDefaultGuestsField(
field: InputBookingField_2024_06_14
): field is GuestsDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "guests";
}
function isDefaultRescheduleReasonField(
field: InputBookingField_2024_06_14
): field is RescheduleReasonDefaultFieldInput_2024_06_14 {
return "slug" in field && field.slug === "rescheduleReason";
}
@@ -26,16 +26,20 @@ import {
AddressFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
CheckboxGroupFieldInput_2024_06_14,
EmailFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
MultiEmailFieldInput_2024_06_14,
MultiSelectFieldInput_2024_06_14,
NameFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
NumberFieldInput_2024_06_14,
PhoneFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
SelectFieldInput_2024_06_14,
TextAreaFieldInput_2024_06_14,
TextFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
} from "./booking-fields.input";
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateInputBookingFields_2024_06_14 } from "./booking-fields.input";
@@ -106,8 +110,12 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
InputAttendeeAddressLocation_2024_06_14,
InputAttendeePhoneLocation_2024_06_14,
InputAttendeeDefinedLocation_2024_06_14,
NameFieldInput_2024_06_14,
EmailFieldInput_2024_06_14
NameDefaultFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14
)
export class CreateEventTypeInput_2024_06_14 {
@IsInt()
@@ -166,8 +174,12 @@ export class CreateEventTypeInput_2024_06_14 {
description:
"Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
oneOf: [
{ $ref: getSchemaPath(NameFieldInput_2024_06_14) },
{ $ref: getSchemaPath(EmailFieldInput_2024_06_14) },
{ $ref: getSchemaPath(NameDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(EmailDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(TitleDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(NotesDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(GuestsDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(RescheduleReasonDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(PhoneFieldInput_2024_06_14) },
{ $ref: getSchemaPath(AddressFieldInput_2024_06_14) },
{ $ref: getSchemaPath(TextFieldInput_2024_06_14) },
@@ -24,16 +24,20 @@ import {
AddressFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
CheckboxGroupFieldInput_2024_06_14,
EmailFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
MultiEmailFieldInput_2024_06_14,
MultiSelectFieldInput_2024_06_14,
NameFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
NumberFieldInput_2024_06_14,
PhoneFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
SelectFieldInput_2024_06_14,
TextAreaFieldInput_2024_06_14,
TextFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
ValidateInputBookingFields_2024_06_14,
} from "./booking-fields.input";
import type { BookingLimitsCount_2024_06_14 } from "./booking-limits-count.input";
@@ -104,8 +108,12 @@ import { Seats_2024_06_14 } from "./seats.input";
InputAttendeeAddressLocation_2024_06_14,
InputAttendeePhoneLocation_2024_06_14,
InputAttendeeDefinedLocation_2024_06_14,
NameFieldInput_2024_06_14,
EmailFieldInput_2024_06_14
NameDefaultFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14
)
export class UpdateEventTypeInput_2024_06_14 {
@IsOptional()
@@ -167,8 +175,12 @@ export class UpdateEventTypeInput_2024_06_14 {
description:
"Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
oneOf: [
{ $ref: getSchemaPath(NameFieldInput_2024_06_14) },
{ $ref: getSchemaPath(EmailFieldInput_2024_06_14) },
{ $ref: getSchemaPath(NameDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(EmailDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(TitleDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(NotesDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(GuestsDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(RescheduleReasonDefaultFieldInput_2024_06_14) },
{ $ref: getSchemaPath(PhoneFieldInput_2024_06_14) },
{ $ref: getSchemaPath(AddressFieldInput_2024_06_14) },
{ $ref: getSchemaPath(TextFieldInput_2024_06_14) },
@@ -1,4 +1,4 @@
import { BadRequestException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ApiProperty, ApiProperty as DocsProperty } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { IsBoolean, IsOptional, IsString } from "class-validator";
@@ -17,11 +17,15 @@ import {
CheckboxGroupFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
NameFieldInput_2024_06_14,
EmailFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
EmailDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
} from "../inputs";
export class NameDefaultFieldOutput_2024_06_14 extends NameFieldInput_2024_06_14 {
export class NameDefaultFieldOutput_2024_06_14 extends NameDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -47,7 +51,7 @@ export class NameDefaultFieldOutput_2024_06_14 extends NameFieldInput_2024_06_14
required!: boolean;
}
export class EmailDefaultFieldOutput_2024_06_14 extends EmailFieldInput_2024_06_14 {
export class EmailDefaultFieldOutput_2024_06_14 extends EmailDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -108,7 +112,7 @@ export class LocationDefaultFieldOutput_2024_06_14 {
hidden!: boolean;
}
export class RescheduleReasonDefaultFieldOutput_2024_06_14 {
export class RescheduleReasonDefaultFieldOutput_2024_06_14 extends RescheduleReasonDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -161,7 +165,7 @@ export class RescheduleReasonDefaultFieldOutput_2024_06_14 {
disableOnPrefill!: boolean;
}
export class TitleDefaultFieldOutput_2024_06_14 {
export class TitleDefaultFieldOutput_2024_06_14 extends TitleDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -214,7 +218,7 @@ export class TitleDefaultFieldOutput_2024_06_14 {
disableOnPrefill!: boolean;
}
export class NotesDefaultFieldOutput_2024_06_14 {
export class NotesDefaultFieldOutput_2024_06_14 extends NotesDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -267,7 +271,7 @@ export class NotesDefaultFieldOutput_2024_06_14 {
disableOnPrefill!: boolean;
}
export class GuestsDefaultFieldOutput_2024_06_14 {
export class GuestsDefaultFieldOutput_2024_06_14 extends GuestsDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -567,6 +571,8 @@ export type CustomFieldOutput_2024_06_14 =
| RadioGroupFieldOutput_2024_06_14
| BooleanFieldOutput_2024_06_14;
export type KnownBookingField_2024_06_14 = DefaultFieldOutput_2024_06_14 | CustomFieldOutput_2024_06_14;
export type OutputBookingField_2024_06_14 =
| DefaultFieldOutput_2024_06_14
| CustomFieldOutput_2024_06_14
@@ -574,6 +580,8 @@ export type OutputBookingField_2024_06_14 =
@ValidatorConstraint({ async: true })
class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private readonly logger = new Logger("OutputBookingFieldValidator_2024_06_14");
private defaultOutputNameMap: { [key: string]: new () => DefaultFieldOutput_2024_06_14 } = {
name: NameDefaultFieldOutput_2024_06_14,
email: EmailDefaultFieldOutput_2024_06_14,
@@ -600,34 +608,7 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
};
async validate(bookingFields: OutputBookingField_2024_06_14[]) {
if (!Array.isArray(bookingFields)) {
throw new BadRequestException(`'bookingFields' must be an array.`);
}
if (!bookingFields.length) {
throw new BadRequestException(`'bookingFields' must contain at least 1 booking field.`);
}
const slugs: string[] = [];
for (const field of bookingFields) {
const { type, slug } = field;
if (!type) {
throw new BadRequestException(`Each booking field must have a 'type' property.`);
}
if (!slug) {
throw new BadRequestException(`Each booking field must have a 'slug' property.`);
}
if (slugs.includes(slug)) {
throw new BadRequestException(
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
);
}
if (slug !== "unknown") {
slugs.push(slug);
}
if (this.isUnknownField(field)) {
await this.validateUnknownField(field);
} else if (this.isDefaultField(field)) {
@@ -653,35 +634,45 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
const errors = await validate(instance);
if (errors.length > 0) {
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
throw new BadRequestException(`Validation failed for ${field.slug} booking field: ${message}`);
this.logger.error(
`OutputBookingFieldValidator_2024_06_14: Validation failed for unknown booking field: ${message}`
);
}
}
async validateDefaultField(field: DefaultFieldOutput_2024_06_14) {
const ClassType = this.defaultOutputNameMap[field.slug];
if (!ClassType) {
throw new BadRequestException(`Unsupported default booking field slug '${field.slug}'.`);
this.logger.error(
`OutputBookingFieldValidator_2024_06_14: Unsupported default booking field slug '${field.slug}'.`
);
}
const instance = plainToInstance(ClassType, field);
const errors = await validate(instance);
if (errors.length > 0) {
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
throw new BadRequestException(`Validation failed for ${field.slug} booking field: ${message}`);
this.logger.error(
`OutputBookingFieldValidator_2024_06_14: Validation failed for ${field.slug} booking field: ${message}`
);
}
}
async validateCustomField(field: CustomFieldOutput_2024_06_14) {
const ClassType = this.customOutputTypeMap[field.type];
if (!ClassType) {
throw new BadRequestException(`Unsupported booking field type '${field.type}'.`);
this.logger.error(
`OutputBookingFieldValidator_2024_06_14: Unsupported custom booking field type '${field.type}'.`
);
}
const instance = plainToInstance(ClassType, field);
const errors = await validate(instance);
if (errors.length > 0) {
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
throw new BadRequestException(`Validation failed for ${field.type} booking field: ${message}`);
this.logger.error(
`OutputBookingFieldValidator_2024_06_14: Validation failed for ${field.type} booking field: ${message}`
);
}
}
@@ -6,7 +6,7 @@ import type {
transformIntervalLimitsApiToInternal,
transformRecurrenceApiToInternal,
transformSeatsApiToInternal,
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
InternalLocationsSchema,
} from "@calcom/lib/event-types/transformers";
@@ -33,7 +33,7 @@ export type InputEventTransformed_2024_06_14 = Omit<
eventName?: string;
bookingLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
locations?: z.infer<typeof InternalLocationsSchema>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiRequestToInternal>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
durationLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
recurringEvent?: ReturnType<typeof transformRecurrenceApiToInternal>;
eventTypeColor?: ReturnType<typeof transformEventColorsApiToInternal>;
+5806 -71
View File
File diff suppressed because it is too large Load Diff