fix: Booker atom name booking field (#18653)

* feat: split name booking field in v2

* fix: booker atom handle split name

* fix: after and before start dates bookings filter

* chore: bump platform libraries
This commit is contained in:
Lauris Skraucis
2025-01-15 14:08:06 +05:30
committed by GitHub
parent 5ccf5e6be8
commit c21ba636d2
10 changed files with 6159 additions and 262 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.81",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.82",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+2 -2
View File
@@ -8001,7 +8001,7 @@
"type": {
"type": "string",
"example": "name",
"description": "only allowed value for type is `name`"
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name."
},
"label": {
"type": "string"
@@ -8795,7 +8795,7 @@
"boolean"
],
"example": "name",
"description": "only allowed value for type is `name`",
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
"default": "name"
},
"label": {
+2 -2
View File
@@ -7411,7 +7411,7 @@
"type": {
"type": "string",
"example": "name",
"description": "only allowed value for type is `name`"
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name."
},
"label": {
"type": "string"
@@ -8137,7 +8137,7 @@
"boolean"
],
"example": "name",
"description": "only allowed value for type is `name`",
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
"default": "name"
},
"label": {
+1 -1
View File
@@ -15,7 +15,7 @@ Once your account is created, the next step is to create an OAuth client. This a
2. Add an OAuth client.
1. Name: anything is fine. You can use your company name or your website.
2. Redirect URIs: Used to validate origin of requests - your website's URLs from which atoms are allowed to make requests to our API. Supports wildcard syntax where "*" would support any origin, and "*app.com" would support "example.app.com", and "https//example.com*" where it would support any origin from that domain.
3. Booking, reschedule and cancel URLs: URLs of pages where users land after a successful booking or if they want to reschedule or cancel a booking. We will pass information in the URL when redirecting to your pages and you will use our hooks and components to implement the pages. See [this guide](/platform/booking-redirects).
3. Booking, reschedule and cancel URLs: URLs of pages where users land after a successful booking or if they want to reschedule or cancel a booking. We will pass information in the URL when redirecting to your pages and you will use our hooks and components to implement the pages. See [this guide](/platform/guides/booking-redirects).
4. Permissions - most likely you need all enabled:
1. Event type: event type is a user event that others can book. For example, Alice is a language teacher and she has an event type “30 minutes Italian lesson” that others can then book.
2. Booking: When Bob books the Italian lesson, a booking is created and shows up on the calendar that Alice connected using atoms.
@@ -11,6 +11,7 @@ import type {
NotesDefaultFieldInput_2024_06_14,
GuestsDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
SplitNameDefaultFieldInput_2024_06_14,
} from "@calcom/platform-types";
import {
@@ -24,6 +25,7 @@ import {
systemAfterFieldRescheduleReason,
type CustomField,
type SystemField,
systemBeforeFieldNameSplit,
} from "../internal-to-api";
type InputBookingField =
@@ -107,6 +109,33 @@ function getBaseProperties(field: InputBookingField): CustomField | SystemField
};
}
if (fieldIsCustomSystemNameSplit(field)) {
const systemNameSplit = { ...systemBeforeFieldNameSplit };
const firstNameField = systemNameSplit.variantsConfig?.variants?.firstAndLastName?.fields?.find(
(field) => field.name === "firstName"
);
const lastNameField = systemNameSplit.variantsConfig?.variants?.firstAndLastName?.fields?.find(
(field) => field.name === "lastName"
);
if (firstNameField) {
firstNameField.label = field.firstNameLabel || "";
firstNameField.placeholder = field.firstNamePlaceholder || "";
}
if (lastNameField) {
lastNameField.label = field.lastNameLabel || "";
lastNameField.placeholder = field.lastNamePlaceholder || "";
lastNameField.required = !!field.lastNameRequired;
}
return {
...systemNameSplit,
disableOnPrefill: !!field.disableOnPrefill,
};
}
if (fieldIsCustomSystemEmail(field)) {
return {
...systemBeforeFieldEmail,
@@ -206,6 +235,12 @@ function fieldIsCustomSystemName(field: InputBookingField): field is NameDefault
return "type" in field && field.type === "name";
}
function fieldIsCustomSystemNameSplit(
field: InputBookingField
): field is SplitNameDefaultFieldInput_2024_06_14 {
return "type" in field && field.type === "splitName";
}
function fieldIsCustomSystemEmail(field: InputBookingField): field is EmailDefaultFieldInput_2024_06_14 {
return "type" in field && field.type === "email";
}
@@ -21,16 +21,38 @@ export function transformBookingFieldsInternalToApi(
const responseDefaultFields: (DefaultFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
defaultFields.map((field) => {
switch (field.name) {
case "name":
case "name": {
if (field.variant === "firstAndLastName") {
const firstNameField = field.variantsConfig?.variants?.firstAndLastName?.fields?.find(
(f) => f.name === "firstName"
);
const lastNameField = field.variantsConfig?.variants?.firstAndLastName?.fields?.find(
(f) => f.name === "lastName"
);
return {
isDefault: true,
type: "splitName",
slug: "splitName",
firstNameLabel: firstNameField?.label,
firstNamePlaceholder: firstNameField?.placeholder,
lastNameLabel: lastNameField?.label,
lastNamePlaceholder: lastNameField?.placeholder,
lastNameRequired: !!lastNameField?.required,
disableOnPrefill: !!field.disableOnPrefill,
};
}
return {
isDefault: true,
type: field.type,
slug: field.name,
required: !!field.required,
label: field.label,
placeholder: field.placeholder,
label: field.variantsConfig?.variants?.fullName?.fields[0]?.label,
placeholder: field.variantsConfig?.variants?.fullName?.fields[0]?.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
};
}
case "email":
return {
isDefault: true,
@@ -344,8 +366,8 @@ const SystemFieldSchema = z.object({
const NameSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("name"),
type: z.literal("name"),
required: z.literal(true),
variant: z.literal("fullName").optional(),
required: z.boolean(),
variant: z.enum(["fullName", "firstAndLastName"]).optional(),
variantsConfig: z
.object({
variants: z.object({
@@ -355,7 +377,18 @@ const NameSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("fullName"),
type: z.literal("text"),
label: z.string().optional(),
required: z.literal(true),
required: z.boolean(),
placeholder: z.string().optional(),
})
),
}),
firstAndLastName: z.object({
fields: z.array(
z.object({
name: z.enum(["firstName", "lastName"]),
type: z.literal("text"),
label: z.string().optional(),
required: z.boolean(),
placeholder: z.string().optional(),
})
),
@@ -467,10 +500,33 @@ export const systemBeforeFieldName: NameSystemField = {
},
],
},
firstAndLastName: {
fields: [
{
name: "firstName",
type: "text",
required: true,
label: "",
placeholder: "",
},
{
name: "lastName",
type: "text",
required: false,
label: "",
placeholder: "",
},
],
},
},
},
};
export const systemBeforeFieldNameSplit: NameSystemField = {
...systemBeforeFieldName,
variant: "firstAndLastName",
};
export const systemBeforeFieldEmail: EmailSystemField = {
defaultLabel: "email_address",
type: "email",
@@ -212,9 +212,20 @@ export const BookerPlatformWrapper = (
const { data: session } = useMe();
const hasSession = !!session;
const { name: defaultName, guests: defaultGuests, ...restFormValues } = props.defaultFormValues ?? {};
const prefillFormParamName = useMemo(() => {
if (defaultName) {
return defaultName;
}
if (restFormValues.firstName) {
return `${restFormValues.firstName} ${restFormValues.lastName}`;
}
return null;
}, [defaultName, restFormValues]);
const prefillFormParams = useMemo(() => {
return {
name: defaultName ?? null,
name: prefillFormParamName,
guests: defaultGuests ?? [],
};
}, [defaultName, defaultGuests]);
@@ -27,7 +27,8 @@ export class NameDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({
example: "name",
description: "only allowed value for type is `name`",
description:
"only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
})
type!: "name";
@@ -53,6 +54,52 @@ export class NameDefaultFieldInput_2024_06_14 {
disableOnPrefill?: boolean;
}
export class SplitNameDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({
example: "splitName",
description:
"only allowed value for type is `splitName`. Used to have 2 booking fields - 1 for first name and 1 for last name.",
})
type!: "splitName";
@IsString()
@IsOptional()
@DocsProperty()
firstNameLabel?: string;
@IsString()
@IsOptional()
@DocsProperty()
firstNamePlaceholder?: string;
@IsString()
@IsOptional()
@DocsProperty()
lastNameLabel?: string;
@IsString()
@IsOptional()
@DocsProperty()
lastNamePlaceholder?: string;
@IsBoolean()
@IsOptional()
@DocsProperty({ description: "First name field is required but last name field is not by default." })
lastNameRequired?: boolean;
@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 `&firstName=bob&lastName=jones`,\
the first name and last name fields will be prefilled with this value and disabled. In case of Booker atom need to pass firstName and lastName to defaultFormValues prop or pass name prop but as a string containing name and surname.",
})
disableOnPrefill?: boolean;
}
export class EmailDefaultFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty({
@@ -755,6 +802,7 @@ export class BooleanFieldInput_2024_06_14 {
type InputDefaultField_2024_06_14 =
| NameDefaultFieldInput_2024_06_14
| SplitNameDefaultFieldInput_2024_06_14
| EmailDefaultFieldInput_2024_06_14
| TitleDefaultFieldInput_2024_06_14
| NotesDefaultFieldInput_2024_06_14
@@ -779,6 +827,7 @@ export type InputBookingField_2024_06_14 =
class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private classMap: { [key: string]: new () => InputBookingField_2024_06_14 } = {
name: NameDefaultFieldInput_2024_06_14,
splitName: SplitNameDefaultFieldInput_2024_06_14,
email: EmailDefaultFieldInput_2024_06_14,
title: TitleDefaultFieldInput_2024_06_14,
notes: NotesDefaultFieldInput_2024_06_14,
@@ -818,7 +867,7 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
);
}
const fieldNeedsSlug = type !== "name" && type !== "email";
const fieldNeedsSlug = type !== "name" && type !== "splitName" && 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.`
@@ -23,6 +23,7 @@ import {
NotesDefaultFieldInput_2024_06_14,
RescheduleReasonDefaultFieldInput_2024_06_14,
TitleDefaultFieldInput_2024_06_14,
SplitNameDefaultFieldInput_2024_06_14,
} from "../inputs";
export class NameDefaultFieldOutput_2024_06_14 extends NameDefaultFieldInput_2024_06_14 {
@@ -51,6 +52,28 @@ export class NameDefaultFieldOutput_2024_06_14 extends NameDefaultFieldInput_202
required!: boolean;
}
export class SplitNameDefaultFieldOutput_2024_06_14 extends SplitNameDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
example: true,
default: true,
})
isDefault = true;
@IsString()
@DocsProperty({
default: "splitName",
})
slug!: "splitName";
@IsString()
@DocsProperty({
default: "splitName",
})
type!: "splitName";
}
export class EmailDefaultFieldOutput_2024_06_14 extends EmailDefaultFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
@@ -550,6 +573,7 @@ export class OutputUnknownBookingField_2024_06_14 {
export type DefaultFieldOutput_2024_06_14 =
| NameDefaultFieldOutput_2024_06_14
| SplitNameDefaultFieldOutput_2024_06_14
| EmailDefaultFieldOutput_2024_06_14
| LocationDefaultFieldOutput_2024_06_14
| RescheduleReasonDefaultFieldOutput_2024_06_14
@@ -584,6 +608,7 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
private defaultOutputNameMap: { [key: string]: new () => DefaultFieldOutput_2024_06_14 } = {
name: NameDefaultFieldOutput_2024_06_14,
splitName: SplitNameDefaultFieldOutput_2024_06_14,
email: EmailDefaultFieldOutput_2024_06_14,
location: LocationDefaultFieldOutput_2024_06_14,
rescheduleReason: RescheduleReasonDefaultFieldOutput_2024_06_14,
+5968 -247
View File
File diff suppressed because it is too large Load Diff