refactor: v2 and booker atom booking fields (#17168)

* handle storing disableOnPrefill

* feat: allow overriding properties of default name and email fields

* feat: allow overriding properties of default name and email fields

* feat: output booking fields

* fix: event type test

* refactor: booker atom booking fields handling

* tests

* fix: boolean booking field

* update platform libraries

* fix: unit tests
This commit is contained in:
Lauris Skraucis
2024-10-21 13:45:30 +03:00
committed by GitHub
parent b7792d0d0a
commit 4e90b65884
16 changed files with 1155 additions and 1292 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.46",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.47",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -662,7 +662,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.data).toBeDefined();
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
expect(data.length).toEqual(2);
console.log("asap data", JSON.stringify(data, null, 2));
expect(data[0].start).toEqual(createdBooking.start);
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
});
@@ -31,6 +31,7 @@ import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
NameFieldInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/enums";
@@ -199,6 +200,13 @@ describe("Event types Endpoints", () => {
});
it("should create an event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
disableOnPrefill: false,
};
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class",
slug: "coding-class",
@@ -211,6 +219,7 @@ describe("Event types Endpoints", () => {
},
],
bookingFields: [
nameBookingField,
{
type: "select",
label: "select which language you want to learn",
@@ -218,6 +227,7 @@ describe("Event types Endpoints", () => {
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: true,
},
],
scheduleId: firstSchedule.id,
@@ -304,12 +314,14 @@ describe("Event types Endpoints", () => {
);
expect(createdEventType.color).toEqual(body.color);
const responseBookingFields = body.bookingFields || [];
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
@@ -630,11 +642,30 @@ describe("Event types Endpoints", () => {
});
it("should update event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
disableOnPrefill: true,
};
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
title: newTitle,
scheduleId: secondSchedule.id,
bookingFields: [
nameBookingField,
{
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: false,
},
],
bookingLimitsCount: {
day: 4,
week: 10,
@@ -694,7 +725,19 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.description).toEqual(eventType.description);
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
];
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
expect(updatedEventType.ownerId).toEqual(user.id);
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
expect(updatedEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
@@ -732,6 +775,7 @@ describe("Event types Endpoints", () => {
eventType.hideCalendarEventDetails = updatedEventType.hideCalendarEventDetails;
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
eventType.color = updatedEventType.color;
eventType.bookingFields = updatedEventType.bookingFields;
});
});
@@ -20,6 +20,8 @@ import {
transformEventColorsApiToInternal,
validateCustomEventName,
transformSeatsApiToInternal,
SystemField,
CustomField,
} from "@calcom/platform-libraries";
import {
CreateEventTypeInput_2024_06_14,
@@ -211,16 +213,27 @@ export class InputEventTypesService_2024_06_14 {
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
const customFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
);
const customNameField = customFields?.find((field) => field.type === "name");
const customEmailField = customFields?.find((field) => field.type === "email");
const defaultFieldsBefore: (SystemField | CustomField)[] = [
customNameField || systemBeforeFieldName,
customEmailField || systemBeforeFieldEmail,
];
// 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 customFields = transformBookingFieldsApiToInternal(inputBookingFields);
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
+172 -10
View File
@@ -5977,6 +5977,10 @@
},
"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 the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
}
},
"required": [
@@ -6010,6 +6014,10 @@
"placeholder": {
"type": "string",
"example": "e.g., 1234 Main St"
},
"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 the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
}
},
"required": [
@@ -6043,6 +6051,10 @@
"placeholder": {
"type": "string",
"example": "e.g., Enter text here"
},
"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 the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
}
},
"required": [
@@ -6076,6 +6088,10 @@
"placeholder": {
"type": "string",
"example": "e.g., 100"
},
"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 the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
}
},
"required": [
@@ -6109,6 +6125,10 @@
"placeholder": {
"type": "string",
"example": "e.g., Detailed description here..."
},
"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 the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
}
},
"required": [
@@ -6152,6 +6172,10 @@
"items": {
"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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
}
},
"required": [
@@ -6192,6 +6216,10 @@
"items": {
"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 the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
}
},
"required": [
@@ -6225,6 +6253,10 @@
"placeholder": {
"type": "string",
"example": "e.g., example@example.com"
},
"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 the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
}
},
"required": [
@@ -6264,6 +6296,10 @@
"items": {
"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 the slug is `notify-me` and the URL contains query parameter `&notify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
}
},
"required": [
@@ -6303,6 +6339,10 @@
"items": {
"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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
}
},
"required": [
@@ -6332,6 +6372,9 @@
},
"required": {
"type": "boolean"
},
"disableOnPrefill": {
"type": "boolean"
}
},
"required": [
@@ -6875,6 +6918,37 @@
"EmailDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
"number",
"textarea",
"select",
"multiselect",
"multiemail",
"checkbox",
"radio",
"boolean"
],
"example": "email",
"description": "only allowed value for type is `email`",
"default": "email"
},
"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 `&email=bob@gmail.com`, the email field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": true,
@@ -6885,24 +6959,51 @@
"type": "string",
"default": "email"
},
"type": {
"type": "string",
"default": "email"
},
"required": {
"type": "boolean"
}
},
"required": [
"type",
"isDefault",
"slug",
"type",
"required"
]
},
"NameDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
"number",
"textarea",
"select",
"multiselect",
"multiemail",
"checkbox",
"radio",
"boolean"
],
"example": "name",
"description": "only allowed value for type is `name`",
"default": "name"
},
"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 `&name=bob`, the name field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": true,
@@ -6913,18 +7014,14 @@
"type": "string",
"default": "name"
},
"type": {
"type": "string",
"default": "name"
},
"required": {
"type": "boolean"
}
},
"required": [
"type",
"isDefault",
"slug",
"type",
"required"
]
},
@@ -7074,6 +7171,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7105,6 +7204,10 @@
"type": "string",
"example": "e.g., 1234 Main St"
},
"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 the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`, the address field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7126,6 +7229,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7153,6 +7258,9 @@
"required": {
"type": "boolean"
},
"disableOnPrefill": {
"type": "boolean"
},
"isDefault": {
"type": "object",
"default": false,
@@ -7174,6 +7282,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7211,6 +7321,10 @@
"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 the slug is `notify-me` and the URL contains query parameter `&notify-me=true`, the checkbox will be selected and the checkbox field will be disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7233,6 +7347,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7264,6 +7380,10 @@
"type": "string",
"example": "e.g., example@example.com"
},
"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 the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`, the these emails will be added and none more can be added."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7285,6 +7405,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7322,6 +7444,10 @@
"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 the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`, the 'en' and 'it' will be selected and the select field will be disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7344,6 +7470,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7375,6 +7503,10 @@
"type": "string",
"example": "e.g., 100"
},
"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 the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`, the number field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7396,6 +7528,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7425,6 +7559,10 @@
"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 the slug is `phone` and the URL contains query parameter `&phone=1234567890`, the phone field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7446,6 +7584,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7483,6 +7623,10 @@
"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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' radio buttom will be selected and the select field will be disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7505,6 +7649,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7546,6 +7692,10 @@
"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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`, the 'italian' will be selected and the select field will be disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7568,6 +7718,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7599,6 +7751,10 @@
"type": "string",
"example": "e.g., Detailed description here..."
},
"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 the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`, the text area will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -7620,6 +7776,8 @@
"type": {
"type": "string",
"enum": [
"name",
"email",
"phone",
"address",
"text",
@@ -7651,6 +7809,10 @@
"type": "string",
"example": "e.g., Enter text here"
},
"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 the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`, the text field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": false,
@@ -21,7 +21,12 @@ import type {
SeatOptionsDisabledSchema,
} from "@calcom/platform-types";
import type { CustomField } from "../internal-to-api/booking-fields";
import {
systemBeforeFieldEmail,
systemBeforeFieldName,
type CustomField,
type SystemField,
} from "../internal-to-api/booking-fields";
import {
transformLocationsApiToInternal,
transformBookingFieldsApiToInternal,
@@ -105,6 +110,59 @@ describe("transformLocationsApiToInternal", () => {
});
describe("transformBookingFieldsApiToInternal", () => {
it("should transform name field", () => {
const bookingField: InputBookingField_2024_06_14 = {
type: "name",
label: "Your name number",
placeholder: "123456789",
disableOnPrefill: true,
};
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedField = {
...systemBeforeFieldName,
label: "Your name number",
placeholder: "123456789",
disableOnPrefill: true,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expectedField.variantsConfig.variants.fullName.fields[0].label = "Your name number";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expectedField.variantsConfig.variants.fullName.fields[0].placeholder = "123456789";
const expectedOutput: SystemField[] = [expectedField];
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform email field", () => {
const bookingField: InputBookingField_2024_06_14 = {
type: "email",
label: "Your email",
placeholder: "bob@gmail.com",
disableOnPrefill: true,
};
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: SystemField[] = [
{
...systemBeforeFieldEmail,
label: "Your email",
placeholder: "bob@gmail.com",
disableOnPrefill: true,
},
];
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform phone field", () => {
const bookingField: InputBookingField_2024_06_14 = {
type: "phone",
@@ -470,6 +528,44 @@ describe("transformBookingFieldsApiToInternal", () => {
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
label: bookingField.label,
labelAsSafeHtml: `<p>${bookingField.label}</p>\n`,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: bookingField.required,
},
];
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should keep disableOnPrefill property", () => {
const disableOnPrefill = true;
const bookingField: InputBookingField_2024_06_14 = {
type: "radio",
slug: "radio",
label: "Your radio buttons",
required: true,
options: ["Radio 1", "Radio 2"],
disableOnPrefill,
};
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
@@ -486,6 +582,8 @@ describe("transformBookingFieldsApiToInternal", () => {
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptionsApiToInternal(bookingField.options),
disableOnPrefill,
},
];
@@ -1,19 +1,70 @@
import { type CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { InputBookingField_2024_06_14 } from "@calcom/platform-types";
import type { CustomField } from "../internal-to-api";
import {
systemBeforeFieldEmail,
systemBeforeFieldName,
type CustomField,
type SystemField,
} from "../internal-to-api";
export function transformBookingFieldsApiToInternal(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]
) {
if (!inputBookingFields) {
return [];
inputBookingFields: InputBookingField_2024_06_14[]
): (SystemField | CustomField)[] {
const customBookingFields = inputBookingFields.map((field) => {
const baseProperties = getBaseProperties(field);
const options =
"options" in field && field.options ? transformSelectOptionsApiToInternal(field.options) : undefined;
if (!options) {
return baseProperties;
}
return {
...baseProperties,
options,
};
});
return customBookingFields;
}
function getBaseProperties(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,
};
}
const customBookingFields = inputBookingFields.map((field) => {
const commonFields: CustomField = {
if (field.type === "email") {
return {
...systemBeforeFieldEmail,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
}
if (field.type === "boolean") {
return {
name: field.slug,
type: field.type,
label: field.label,
labelAsSafeHtml: `<p>${field.label}</p>\n`,
sources: [
{
id: "user",
@@ -24,23 +75,27 @@ export function transformBookingFieldsApiToInternal(
],
editable: "user",
required: field.required,
placeholder: "placeholder" in field && field.placeholder ? field.placeholder : "",
disableOnPrefill: field.disableOnPrefill,
};
}
const options =
"options" in field && field.options ? transformSelectOptionsApiToInternal(field.options) : undefined;
if (!options) {
return commonFields;
}
return {
...commonFields,
options,
};
});
return customBookingFields;
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,
};
}
export function transformSelectOptionsApiToInternal(options: string[]) {
@@ -25,6 +25,9 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
required: field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "email":
return {
@@ -32,6 +35,9 @@ export function transformBookingFieldsInternalToApi(
type: field.type,
slug: field.name,
required: field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "location":
return {
@@ -85,6 +91,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "address":
return {
@@ -94,6 +101,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "text":
return {
@@ -103,6 +111,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "number":
return {
@@ -112,6 +121,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "textarea":
return {
@@ -121,6 +131,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "multiemail":
return {
@@ -130,6 +141,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "boolean":
return {
@@ -138,6 +150,7 @@ export function transformBookingFieldsInternalToApi(
slug: field.name,
label: field.label,
required: field.required,
disableOnPrefill: field.disableOnPrefill,
};
case "select":
return {
@@ -148,6 +161,7 @@ export function transformBookingFieldsInternalToApi(
required: field.required,
placeholder: field.placeholder,
options: field.options ? field.options.map((option) => option.value) : [],
disableOnPrefill: field.disableOnPrefill,
};
case "multiselect":
return {
@@ -157,6 +171,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: field.disableOnPrefill,
};
case "checkbox":
return {
@@ -166,6 +181,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: field.disableOnPrefill,
};
case "radio":
return {
@@ -175,6 +191,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
disableOnPrefill: field.disableOnPrefill,
};
default:
throw new Error(`Unsupported booking field type '${field.type}'.`);
@@ -203,6 +220,7 @@ const CustomFieldsSchema = z.object({
name: z.string(),
type: CustomFieldTypeEnum,
label: z.string(),
labelAsSafeHtml: z.string().optional(),
sources: z.array(
z.object({
id: z.literal("user"),
@@ -211,7 +229,7 @@ const CustomFieldsSchema = z.object({
fieldRequired: z.literal(true),
})
),
editable: z.literal("user"),
editable: z.enum(["user", "user-readonly"]),
required: z.boolean(),
placeholder: z.string().optional(),
options: z
@@ -222,6 +240,7 @@ const CustomFieldsSchema = z.object({
})
)
.optional(),
disableOnPrefill: z.boolean().optional(),
});
const SystemFieldSchema = z.object({
@@ -244,6 +263,7 @@ const SystemFieldSchema = z.object({
)
.optional(),
defaultPlaceholder: z.enum(["", "share_additional_notes", "email", "reschedule_placeholder"]).optional(),
placeholder: z.string().optional(),
hidden: z.boolean().optional(),
required: z.boolean(),
hideWhenJustOneOption: z.boolean().optional(),
@@ -262,12 +282,31 @@ const SystemFieldSchema = z.object({
}),
})
.optional(),
disableOnPrefill: z.boolean().optional(),
});
const NameSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("name"),
type: z.literal("name"),
required: z.literal(true),
variant: z.literal("fullName").optional(),
variantsConfig: z
.object({
variants: z.object({
fullName: z.object({
fields: z.array(
z.object({
name: z.literal("fullName"),
type: z.literal("text"),
label: z.string().optional(),
required: z.literal(true),
placeholder: z.string().optional(),
})
),
}),
}),
})
.optional(),
});
const EmailSystemFieldSchema = SystemFieldSchema.extend({
@@ -306,8 +345,8 @@ const GuestsSystemFieldSchema = SystemFieldSchema.extend({
required: z.literal(false),
});
type NameSystemField = z.infer<typeof NameSystemFieldSchema>;
type EmailSystemField = z.infer<typeof EmailSystemFieldSchema>;
export type NameSystemField = z.infer<typeof NameSystemFieldSchema>;
export type EmailSystemField = z.infer<typeof EmailSystemFieldSchema>;
type RescheduleReasonSystemField = z.infer<typeof RescheduleReasonSystemFieldSchema>;
type LocationReasonSystemField = z.infer<typeof LocationReasonSystemFieldSchema>;
type TitleSystemField = z.infer<typeof TitleSystemFieldSchema>;
@@ -343,6 +382,7 @@ export const systemBeforeFieldName: NameSystemField = {
editable: "system",
defaultLabel: "your_name",
required: true,
variant: "fullName",
sources: [
{
label: "Default",
@@ -350,21 +390,19 @@ export const systemBeforeFieldName: NameSystemField = {
type: "default",
},
],
};
export const systemBeforeFieldNameReadOnly: NameSystemField = {
type: "name",
name: "name",
editable: "user-readonly",
defaultLabel: "your_name",
required: true,
sources: [
{
label: "Default",
id: "default",
type: "default",
variantsConfig: {
variants: {
fullName: {
fields: [
{
name: "fullName",
type: "text",
required: true,
},
],
},
},
],
},
};
export const systemBeforeFieldEmail: EmailSystemField = {
@@ -382,21 +420,6 @@ export const systemBeforeFieldEmail: EmailSystemField = {
],
};
export const systemBeforeFieldEmailReadOnly: EmailSystemField = {
defaultLabel: "email_address",
type: "email",
name: "email",
required: true,
editable: "user-readonly",
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
};
export const systemBeforeFieldLocation: LocationReasonSystemField = {
defaultLabel: "location",
type: "radioInput",
@@ -31,13 +31,18 @@ import {
transformEventTypeColorsInternalToApi,
transformSeatsInternalToApi,
} from ".";
import type { CustomField } from "./booking-fields";
import {
systemBeforeFieldEmail,
systemBeforeFieldName,
type CustomField,
type SystemField,
} from "./booking-fields";
describe("transformLocationsInternalToApi", () => {
it("should reverse transform address location", () => {
const transformedLocation = [
{
type: "inPerson",
type: "inPerson" as const,
address: "1234 Main St",
displayLocationPublicly: true,
},
@@ -59,7 +64,7 @@ describe("transformLocationsInternalToApi", () => {
it("should reverse transform link location", () => {
const transformedLocation = [
{
type: "link",
type: "link" as const,
link: "https://example.com",
displayLocationPublicly: true,
},
@@ -81,7 +86,7 @@ describe("transformLocationsInternalToApi", () => {
it("should reverse transform phone location", () => {
const transformedLocation = [
{
type: "userPhone",
type: "userPhone" as const,
hostPhoneNumber: "123456789",
displayLocationPublicly: true,
},
@@ -103,7 +108,7 @@ describe("transformLocationsInternalToApi", () => {
it("should reverse transform integration location", () => {
const transformedLocation = [
{
type: "integrations:daily",
type: "integrations:daily" as const,
},
];
@@ -121,6 +126,109 @@ describe("transformLocationsInternalToApi", () => {
});
describe("transformBookingFieldsInternalToApi", () => {
it("should reverse transform not modified name default field", () => {
const transformedField: SystemField[] = [systemBeforeFieldName];
const expectedOutput = [
{
type: "name",
slug: "name",
isDefault: true,
required: true,
},
];
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform modified name default field", () => {
const nameField = {
...systemBeforeFieldName,
placeholder: "custom placeholder",
disableOnPrefill: true,
label: "custom label",
variantsConfig: {
variants: {
fullName: {
fields: [
{
name: "fullName",
label: "custom label",
placeholder: "custom placeholder",
type: "text",
required: true,
},
],
},
},
},
};
const transformedField: SystemField[] = [nameField];
const expectedOutput = [
{
type: "name",
slug: "name",
isDefault: true,
required: true,
placeholder: "custom placeholder",
disableOnPrefill: true,
label: "custom label",
},
];
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform not modified email default field", () => {
const transformedField: SystemField[] = [systemBeforeFieldEmail];
const expectedOutput = [
{
type: "email",
slug: "email",
isDefault: true,
required: true,
},
];
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform modified email default field", () => {
const transformedField: SystemField[] = [
{
...systemBeforeFieldEmail,
placeholder: "custom placeholder",
disableOnPrefill: true,
label: "custom label",
},
];
const expectedOutput = [
{
type: "email",
slug: "email",
isDefault: true,
required: true,
placeholder: "custom placeholder",
disableOnPrefill: true,
label: "custom label",
},
];
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform phone field", () => {
const transformedField: CustomField[] = [
{
@@ -511,7 +619,6 @@ describe("transformBookingFieldsInternalToApi", () => {
],
editable: "user",
required: true,
placeholder: "",
},
];
@@ -733,7 +840,7 @@ describe("transformRequiresConfirmationInternalToApi", () => {
};
const result = transformRequiresConfirmationInternalToApi(
transformedField.requiresConfirmation,
!!undefined,
false,
undefined
);
@@ -62,10 +62,6 @@ export type BookerPlatformWrapperAtomProps = Omit<
notes?: string;
rescheduleReason?: string;
} & Record<string, string | string[]>;
readOnlyFormValues?: {
name?: boolean;
email?: boolean;
};
handleCreateBooking?: (input: UseCreateBookingInput) => void;
onCreateBookingSuccess?: (data: ApiSuccessResponse<BookingResponse>) => void;
onCreateBookingError?: (data: ApiErrorResponse | Error) => void;
@@ -153,7 +149,7 @@ export const BookerPlatformWrapper = (
isPending: isTeamPending,
data:
teamEventTypeData && teamEventTypeData.length > 0
? transformApiTeamEventTypeForAtom(teamEventTypeData[0], props.entity, props.readOnlyFormValues)
? transformApiTeamEventTypeForAtom(teamEventTypeData[0], props.entity, props.defaultFormValues)
: undefined,
};
}
@@ -164,7 +160,7 @@ export const BookerPlatformWrapper = (
isPending,
data:
data && data.length > 0
? transformApiEventTypeForAtom(data[0], props.entity, props.readOnlyFormValues)
? transformApiEventTypeForAtom(data[0], props.entity, props.defaultFormValues)
: undefined,
};
}, [
@@ -113,7 +113,7 @@ export function BaseCalProvider({
let translation =
labels?.[resolvedKey as keyof typeof labels] ?? String(getTranslation(resolvedKey, language) ?? "");
if (!translation) {
return "";
return key;
}
if (values) {
const valueKeys = Object.keys(values) as (keyof typeof values)[];
@@ -8,13 +8,13 @@ import {
systemBeforeFieldLocation,
systemAfterFieldRescheduleReason,
transformRecurrenceApiToInternal,
systemBeforeFieldNameReadOnly,
systemBeforeFieldEmailReadOnly,
} 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,
NameDefaultFieldOutput_2024_06_14,
TeamEventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import {
@@ -30,7 +30,7 @@ import type { BookerPlatformWrapperAtomProps } from "../../booker/BookerPlatform
export function transformApiEventTypeForAtom(
eventType: Omit<EventTypeOutput_2024_06_14, "ownerId">,
entity: BookerPlatformWrapperAtomProps["entity"] | undefined,
readOnlyFormValues: BookerPlatformWrapperAtomProps["readOnlyFormValues"] | undefined
defaultFormValues: BookerPlatformWrapperAtomProps["defaultFormValues"] | undefined
) {
const { lengthInMinutes, locations, bookingFields, users, recurrence, ...rest } = eventType;
@@ -50,7 +50,7 @@ export function transformApiEventTypeForAtom(
...rest,
length: lengthInMinutes,
locations: getLocations(locations),
bookingFields: getBookingFields(bookingFields, readOnlyFormValues),
bookingFields: getBookingFields(bookingFields, defaultFormValues),
isDefault,
isDynamic: false,
profile: {
@@ -107,7 +107,7 @@ export function transformApiEventTypeForAtom(
export function transformApiTeamEventTypeForAtom(
eventType: TeamEventTypeOutput_2024_06_14,
entity: BookerPlatformWrapperAtomProps["entity"] | undefined,
readOnlyFormValues: BookerPlatformWrapperAtomProps["readOnlyFormValues"] | undefined
defaultFormValues: BookerPlatformWrapperAtomProps["defaultFormValues"] | undefined
) {
const { lengthInMinutes, locations, hosts, bookingFields, recurrence, ...rest } = eventType;
@@ -126,7 +126,7 @@ export function transformApiTeamEventTypeForAtom(
...rest,
length: lengthInMinutes,
locations: getLocations(locations),
bookingFields: getBookingFields(bookingFields, readOnlyFormValues),
bookingFields: getBookingFields(bookingFields, defaultFormValues),
isDefault,
isDynamic: false,
profile: {
@@ -226,26 +226,48 @@ function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
function getBookingFields(
bookingFields: EventTypeOutput_2024_06_14["bookingFields"],
readOnlyFormValues: BookerPlatformWrapperAtomProps["readOnlyFormValues"] | undefined
defaultFormValues: BookerPlatformWrapperAtomProps["defaultFormValues"] | undefined
) {
// note(Lauris): the peculiar thing about returning atom booking fields using v2 event type is that v2 event type has more possible
// booking field outputs than inputs due to default system fields that cant be passed as inputs, which is why we take v2 from response
// only the custom fields and default editable fields aka fields that can be passed as inputs for event type booking fields.
const customFields: (SystemField | CustomField)[] = bookingFields
? transformBookingFieldsApiToInternal(
bookingFields.filter((field) => isCustomField(field) || isDefaultEditableField(field))
)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
);
const customNameField = customFields?.find((field) => field.type === "name");
const customEmailField = customFields?.find((field) => field.type === "email");
const systemBeforeFields: SystemField[] = [
readOnlyFormValues?.name ? systemBeforeFieldNameReadOnly : systemBeforeFieldName,
readOnlyFormValues?.email ? systemBeforeFieldEmailReadOnly : systemBeforeFieldEmail,
customNameField || systemBeforeFieldName,
customEmailField || systemBeforeFieldEmail,
systemBeforeFieldLocation,
];
const transformedCustomFields: CustomField[] = transformBookingFieldsApiToInternal(
bookingFields.filter((field) => isCustomField(field))
);
const systemAfterFields: SystemField[] = [systemAfterFieldRescheduleReason];
const transformedBookingFields: (SystemField | CustomField)[] = [
...systemBeforeFields,
...transformedCustomFields,
...customFieldsWithoutNameEmail,
...systemAfterFields,
];
// note(Lauris): in web app booking form values can be passed as url query params, but booker atom does not accept booking field values via url,
// so defaultFormValues act as a way to prefill booking form fields, and if the field in database has disableOnPrefill=true and value passed then its read only.
const defaultFormValuesKeys = defaultFormValues ? Object.keys(defaultFormValues) : [];
if (defaultFormValuesKeys.length) {
for (const field of transformedBookingFields) {
if (defaultFormValuesKeys.includes(field.name) && field.disableOnPrefill) {
field.editable = "user-readonly";
}
}
}
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(transformedBookingFields);
}
@@ -254,3 +276,9 @@ function isCustomField(
): field is CustomFieldOutput_2024_06_14 {
return !field.isDefault;
}
function isDefaultEditableField(
field: EventTypeOutput_2024_06_14["bookingFields"][number]
): field is NameDefaultFieldOutput_2024_06_14 | EmailDefaultFieldOutput_2024_06_14 {
return field.type === "name" || field.type === "email";
}
+6 -1
View File
@@ -125,7 +125,12 @@ export {
systemAfterFieldRescheduleReason,
} from "@calcom/lib/event-types/transformers";
export type { SystemField, CustomField } from "@calcom/lib/event-types/transformers";
export type {
SystemField,
CustomField,
NameSystemField,
EmailSystemField,
} from "@calcom/lib/event-types/transformers";
export { parseBookingLimit, parseEventTypeColor } from "@calcom/lib";
@@ -1,11 +1,13 @@
import { BadRequestException } from "@nestjs/common";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiPropertyOptional as DocsPropertyOptional } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { IsString, IsBoolean, IsArray, IsIn, IsOptional } from "class-validator";
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
const inputBookingFieldTypes = [
"name",
"email",
"phone",
"address",
"text",
@@ -19,6 +21,59 @@ const inputBookingFieldTypes = [
"boolean",
] as const;
export class NameFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({ example: "name", description: "only allowed value for type is `name`" })
type!: "name";
@IsString()
@IsOptional()
@DocsProperty()
label?: string;
@IsString()
@IsOptional()
@DocsProperty()
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 `&name=bob`,\
the name field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class EmailFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({ example: "email", description: "only allowed value for type is `email`" })
type!: "email";
@IsString()
@IsOptional()
@DocsProperty()
label?: string;
@IsString()
@IsOptional()
@DocsProperty()
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 `&email=bob@gmail.com`,\
the email 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`" })
@@ -44,6 +99,17 @@ export class PhoneFieldInput_2024_06_14 {
@IsOptional()
@DocsProperty()
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 the slug is `phone` and the URL contains query parameter `&phone=1234567890`,\
the phone field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class AddressFieldInput_2024_06_14 {
@@ -72,6 +138,17 @@ export class AddressFieldInput_2024_06_14 {
@DocsProperty()
@DocsProperty({ example: "e.g., 1234 Main St" })
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 the slug is `address` and the URL contains query parameter `&address=1234 Main St, London`,\
the address field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class TextFieldInput_2024_06_14 {
@@ -100,6 +177,17 @@ export class TextFieldInput_2024_06_14 {
@IsOptional()
@DocsProperty()
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 the slug is `who-referred-you` and the URL contains query parameter `&who-referred-you=bob`,\
the text field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class NumberFieldInput_2024_06_14 {
@@ -128,6 +216,17 @@ export class NumberFieldInput_2024_06_14 {
@IsOptional()
@DocsProperty()
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 the slug is `calories-per-day` and the URL contains query parameter `&calories-per-day=3000`,\
the number field will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class TextAreaFieldInput_2024_06_14 {
@@ -156,6 +255,17 @@ export class TextAreaFieldInput_2024_06_14 {
@IsOptional()
@DocsProperty()
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 the slug is `dear-diary` and the URL contains query parameter `&dear-diary=Today I shipped a feature`,\
the text area will be prefilled with this value and disabled.",
})
disableOnPrefill?: boolean;
}
export class SelectFieldInput_2024_06_14 {
@@ -188,6 +298,17 @@ export class SelectFieldInput_2024_06_14 {
@IsArray()
@DocsProperty({ type: [String], example: ["Option 1", "Option 2"] })
options!: 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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`,\
the 'italian' will be selected and the select field will be disabled.",
})
disableOnPrefill?: boolean;
}
export class MultiSelectFieldInput_2024_06_14 {
@@ -214,6 +335,17 @@ export class MultiSelectFieldInput_2024_06_14 {
@IsArray()
@DocsProperty({ type: [String], example: ["Option 1", "Option 2"] })
options!: 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 the slug is `consultants` and the URL contains query parameter `&consultants=en&language=it`,\
the 'en' and 'it' will be selected and the select field will be disabled.",
})
disableOnPrefill?: boolean;
}
export class MultiEmailFieldInput_2024_06_14 {
@@ -242,6 +374,17 @@ export class MultiEmailFieldInput_2024_06_14 {
@IsOptional()
@DocsProperty()
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 the slug is `consultants` and the URL contains query parameter `&consultants=alice@gmail.com&consultants=bob@gmail.com`,\
the these emails will be added and none more can be added.",
})
disableOnPrefill?: boolean;
}
export class CheckboxGroupFieldInput_2024_06_14 {
@@ -268,6 +411,17 @@ export class CheckboxGroupFieldInput_2024_06_14 {
@IsArray()
@DocsProperty({ type: [String], example: ["Checkbox 1", "Checkbox 2"] })
options!: 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 the slug is `notify-me` and the URL contains query parameter `&notify-me=true`,\
the checkbox will be selected and the checkbox field will be disabled.",
})
disableOnPrefill?: boolean;
}
export class RadioGroupFieldInput_2024_06_14 {
@@ -294,6 +448,17 @@ export class RadioGroupFieldInput_2024_06_14 {
@IsArray()
@DocsProperty({ type: [String], example: ["Radio 1", "Radio 2"] })
options!: 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 the slug is `language` and options of this select field are ['english', 'italian'] and the URL contains query parameter `&language=italian`,\
the 'italian' radio buttom will be selected and the select field will be disabled.",
})
disableOnPrefill?: boolean;
}
export class BooleanFieldInput_2024_06_14 {
@@ -316,9 +481,16 @@ export class BooleanFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty()
required!: boolean;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({ type: Boolean })
disableOnPrefill?: boolean;
}
export type InputBookingField_2024_06_14 =
| NameFieldInput_2024_06_14
| EmailFieldInput_2024_06_14
| PhoneFieldInput_2024_06_14
| AddressFieldInput_2024_06_14
| TextFieldInput_2024_06_14
@@ -334,6 +506,8 @@ 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,
phone: PhoneFieldInput_2024_06_14,
address: AddressFieldInput_2024_06_14,
text: TextFieldInput_2024_06_14,
@@ -347,7 +521,7 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
boolean: BooleanFieldInput_2024_06_14,
};
private reservedSystemSlugs = ["name", "email", "location", "rescheduleReason"];
private reservedSystemSlugs = ["location", "rescheduleReason"];
async validate(bookingFields: { type: string; slug: string }[]) {
if (!Array.isArray(bookingFields)) {
@@ -365,7 +539,8 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
throw new BadRequestException(`Each booking field must have a 'type' property.`);
}
if (!slug) {
const notSystemEditableField = type !== "name" && type !== "email";
if (notSystemEditableField && !slug) {
throw new BadRequestException(`Each booking field must have a 'slug' property.`);
}
@@ -382,7 +557,9 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
);
}
slugs.push(slug);
if (notSystemEditableField) {
slugs.push(slug);
}
const ClassType = this.classTypeMap[type];
if (!ClassType) {
@@ -17,9 +17,11 @@ import {
CheckboxGroupFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
NameFieldInput_2024_06_14,
EmailFieldInput_2024_06_14,
} from "../inputs";
export class NameDefaultFieldOutput_2024_06_14 {
export class NameDefaultFieldOutput_2024_06_14 extends NameFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
@@ -45,7 +47,7 @@ export class NameDefaultFieldOutput_2024_06_14 {
required!: true;
}
export class EmailDefaultFieldOutput_2024_06_14 {
export class EmailDefaultFieldOutput_2024_06_14 extends EmailFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
+336 -1182
View File
File diff suppressed because it is too large Load Diff