cal.pub0.org/packages/features/bookings/lib/getBookingFields.ts

337 lines
9.6 KiB
TypeScript
Raw Normal View History

Feature/ Manage Booking Questions (#6560) * WIP * Create Booking Questions builder * Renaming things * wip * wip * Implement Add Guests and other fixes * Fixes after testing * Fix wrong status code 404 * Fixes * Lint fixes * Self review comments addressed * More self review comments addressed * Feedback from zomars * BugFixes after testing * More fixes discovered during review * Update packages/lib/hooks/useHasPaidPlan.ts Co-authored-by: Omar López <zomars@me.com> * More fixes discovered during review * Update packages/ui/components/form/inputs/Input.tsx Co-authored-by: Omar López <zomars@me.com> * More fixes discovered during review * Update packages/features/bookings/lib/getBookingFields.ts Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * More PR review fixes * Hide label using labelSrOnly * Fix Carinas feedback and implement 2 workflows thingy * Misc fixes * Fixes from Loom comments and PR * Fix a lint errr * Fix cancellation reason * Fix regression in edit due to name conflict check * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Fix options not set when default value is used * Restoring reqBody to avoid uneeded conflicts with main * Type fix * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López <zomars@me.com> * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Omar López <zomars@me.com> * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López <zomars@me.com> * Apply suggestions from code review Co-authored-by: Omar López <zomars@me.com> * Show fields but mark them disabled * Apply suggestions from code review Co-authored-by: Omar López <zomars@me.com> * More comments * Fix booking success page crash when a booking doesnt have newly added required fields response * Dark theme asterisk not visible * Make location required in zodSchema as was there in production * Linting * Remove _metadata.ts files for apps that have config.json * Revert "Remove _metadata.ts files for apps that have config.json" This reverts commit d79bdd336cf312a30a8943af94c059947bd91ccd. * Fix lint error * Fix missing condition for samlSPConfig * Delete unexpectedly added file * yarn.lock change not required * fix types * Make checkboxes rounded * Fix defaultLabel being stored as label due to SSR rendering * Shaved 16kb from booking page * Explicit types for profile * Show payment value only if price is greater than 0 * Fix type error * Add back inferred types as they are failing * Fix duplicate label on number --------- Co-authored-by: zomars <zomars@me.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Efraín Rochín <roae.85@gmail.com>
2023-03-02 18:15:28 +00:00
import type { EventTypeCustomInput, EventType, Prisma, Workflow } from "@prisma/client";
import { z } from "zod";
import slugify from "@calcom/lib/slugify";
import {
BookingFieldType,
customInputSchema,
eventTypeBookingFields,
EventTypeMetaDataSchema,
} from "@calcom/prisma/zod-utils";
export const SMS_REMINDER_NUMBER_FIELD = "smsReminderNumber";
export const getSmsReminderNumberField = () =>
({
name: SMS_REMINDER_NUMBER_FIELD,
type: "phone",
defaultLabel: "number_sms_notifications",
defaultPlaceholder: "enter_phone_number",
editable: "system",
} as const);
export const getSmsReminderNumberSource = ({
workflowId,
isSmsReminderNumberRequired,
}: {
workflowId: Workflow["id"];
isSmsReminderNumberRequired: boolean;
}) => ({
id: "" + workflowId,
type: "workflow",
label: "Workflow",
fieldRequired: isSmsReminderNumberRequired,
editUrl: `/workflows/${workflowId}`,
});
type Fields = z.infer<typeof eventTypeBookingFields>;
const EventTypeCustomInputType = {
TEXT: "TEXT",
TEXTLONG: "TEXTLONG",
NUMBER: "NUMBER",
BOOL: "BOOL",
RADIO: "RADIO",
PHONE: "PHONE",
} as const;
export const SystemField = z.enum([
"name",
"email",
"location",
"notes",
"guests",
"rescheduleReason",
"smsReminderNumber",
]);
export const SystemFieldsEditability: Record<z.infer<typeof SystemField>, Fields[number]["editable"]> = {
name: "system",
email: "system",
location: "system",
notes: "system-but-optional",
guests: "system-but-optional",
rescheduleReason: "system",
smsReminderNumber: "system",
};
/**
* This fn is the key to ensure on the fly mapping of customInputs to bookingFields and ensuring that all the systems fields are present and correctly ordered in bookingFields
*/
export const getBookingFieldsWithSystemFields = ({
bookingFields,
disableGuests,
customInputs,
metadata,
workflows,
}: {
bookingFields: Fields | EventType["bookingFields"];
disableGuests: boolean;
customInputs: EventTypeCustomInput[] | z.infer<typeof customInputSchema>[];
metadata: EventType["metadata"] | z.infer<typeof EventTypeMetaDataSchema>;
workflows: Prisma.EventTypeGetPayload<{
select: {
workflows: {
select: {
workflow: {
select: {
id: true;
steps: true;
};
};
};
};
};
}>["workflows"];
}) => {
const parsedMetaData = EventTypeMetaDataSchema.parse(metadata || {});
const parsedBookingFields = eventTypeBookingFields.parse(bookingFields || []);
const parsedCustomInputs = customInputSchema.array().parse(customInputs || []);
workflows = workflows || [];
return ensureBookingInputsHaveSystemFields({
bookingFields: parsedBookingFields,
disableGuests,
additionalNotesRequired: parsedMetaData?.additionalNotesRequired || false,
customInputs: parsedCustomInputs,
workflows,
});
};
export const ensureBookingInputsHaveSystemFields = ({
bookingFields,
disableGuests,
additionalNotesRequired,
customInputs,
workflows,
}: {
bookingFields: Fields;
disableGuests: boolean;
additionalNotesRequired: boolean;
customInputs: z.infer<typeof customInputSchema>[];
workflows: Prisma.EventTypeGetPayload<{
select: {
workflows: {
select: {
workflow: {
select: {
id: true;
steps: true;
};
};
};
};
};
}>["workflows"];
}) => {
// If bookingFields is set already, the migration is done.
const handleMigration = !bookingFields.length;
const CustomInputTypeToFieldType = {
[EventTypeCustomInputType.TEXT]: BookingFieldType.text,
[EventTypeCustomInputType.TEXTLONG]: BookingFieldType.textarea,
[EventTypeCustomInputType.NUMBER]: BookingFieldType.number,
[EventTypeCustomInputType.BOOL]: BookingFieldType.boolean,
[EventTypeCustomInputType.RADIO]: BookingFieldType.radio,
[EventTypeCustomInputType.PHONE]: BookingFieldType.phone,
};
const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
workflows.forEach((workflow) => {
workflow.workflow.steps.forEach((step) => {
if (step.action === "SMS_ATTENDEE") {
const workflowId = workflow.workflow.id;
smsNumberSources.push(
getSmsReminderNumberSource({
workflowId,
isSmsReminderNumberRequired: !!step.numberRequired,
})
);
}
});
});
// These fields should be added before other user fields
const systemBeforeFields: typeof bookingFields = [
{
defaultLabel: "your_name",
defaultPlaceholder: "example_name",
type: "name",
name: "name",
required: true,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "email_address",
defaultPlaceholder: "you@example.com",
type: "email",
name: "email",
required: true,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "location",
type: "radioInput",
name: "location",
required: false,
// Populated on the fly from locations. I don't want to duplicate storing locations and instead would like to be able to refer to locations in eventType.
// options: `eventType.locations`
optionsInputs: {
attendeeInPerson: {
type: "address",
required: true,
placeholder: "",
},
phone: {
type: "phone",
required: true,
placeholder: "",
},
},
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
];
// These fields should be added after other user fields
const systemAfterFields: typeof bookingFields = [
{
defaultLabel: "additional_notes",
type: "textarea",
name: "notes",
required: additionalNotesRequired,
defaultPlaceholder: "share_additional_notes",
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "additional_guests",
type: "multiemail",
name: "guests",
required: false,
hidden: disableGuests,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "reschedule_reason",
type: "textarea",
name: "rescheduleReason",
defaultPlaceholder: "reschedule_placeholder",
required: false,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
];
const missingSystemBeforeFields = [];
for (const field of systemBeforeFields) {
// Only do a push, we must not update existing system fields as user could have modified any property in it,
if (!bookingFields.find((f) => f.name === field.name)) {
missingSystemBeforeFields.push(field);
}
}
bookingFields = missingSystemBeforeFields.concat(bookingFields);
// Backward Compatibility for SMS Reminder Number
// Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields`
// Also, note that even if Workflows don't explicity add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic
if (smsNumberSources.length && !bookingFields.find((f) => f.name !== SMS_REMINDER_NUMBER_FIELD)) {
const indexForLocation = bookingFields.findIndex((f) => f.name === "location");
// Add the SMS Reminder Number field after `location` field always
bookingFields.splice(indexForLocation + 1, 0, {
...getSmsReminderNumberField(),
sources: smsNumberSources,
});
}
Feature/ Manage Booking Questions (#6560) * WIP * Create Booking Questions builder * Renaming things * wip * wip * Implement Add Guests and other fixes * Fixes after testing * Fix wrong status code 404 * Fixes * Lint fixes * Self review comments addressed * More self review comments addressed * Feedback from zomars * BugFixes after testing * More fixes discovered during review * Update packages/lib/hooks/useHasPaidPlan.ts Co-authored-by: Omar López <zomars@me.com> * More fixes discovered during review * Update packages/ui/components/form/inputs/Input.tsx Co-authored-by: Omar López <zomars@me.com> * More fixes discovered during review * Update packages/features/bookings/lib/getBookingFields.ts Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * More PR review fixes * Hide label using labelSrOnly * Fix Carinas feedback and implement 2 workflows thingy * Misc fixes * Fixes from Loom comments and PR * Fix a lint errr * Fix cancellation reason * Fix regression in edit due to name conflict check * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Fix options not set when default value is used * Restoring reqBody to avoid uneeded conflicts with main * Type fix * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López <zomars@me.com> * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Omar López <zomars@me.com> * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López <zomars@me.com> * Apply suggestions from code review Co-authored-by: Omar López <zomars@me.com> * Show fields but mark them disabled * Apply suggestions from code review Co-authored-by: Omar López <zomars@me.com> * More comments * Fix booking success page crash when a booking doesnt have newly added required fields response * Dark theme asterisk not visible * Make location required in zodSchema as was there in production * Linting * Remove _metadata.ts files for apps that have config.json * Revert "Remove _metadata.ts files for apps that have config.json" This reverts commit d79bdd336cf312a30a8943af94c059947bd91ccd. * Fix lint error * Fix missing condition for samlSPConfig * Delete unexpectedly added file * yarn.lock change not required * fix types * Make checkboxes rounded * Fix defaultLabel being stored as label due to SSR rendering * Shaved 16kb from booking page * Explicit types for profile * Show payment value only if price is greater than 0 * Fix type error * Add back inferred types as they are failing * Fix duplicate label on number --------- Co-authored-by: zomars <zomars@me.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Efraín Rochín <roae.85@gmail.com>
2023-03-02 18:15:28 +00:00
// Backward Compatibility: If we are migrating from old system, we need to map `customInputs` to `bookingFields`
if (handleMigration) {
customInputs.forEach((input) => {
bookingFields.push({
label: input.label,
editable: "user",
// Custom Input's slugified label was being used as query param for prefilling. So, make that the name of the field
name: slugify(input.label),
placeholder: input.placeholder,
type: CustomInputTypeToFieldType[input.type],
required: input.required,
options: input.options
? input.options.map((o) => {
return {
...o,
// Send the label as the value without any trimming or lowercase as this is what customInput are doing. It maintains backward compatibility
value: o.label,
};
})
: [],
});
});
}
const missingSystemAfterFields = [];
for (const field of systemAfterFields) {
// Only do a push, we must not update existing system fields as user could have modified any property in it,
if (!bookingFields.find((f) => f.name === field.name)) {
missingSystemAfterFields.push(field);
}
}
bookingFields = bookingFields.concat(missingSystemAfterFields);
bookingFields = bookingFields.map((field) => {
const foundEditableMap = SystemFieldsEditability[field.name as keyof typeof SystemFieldsEditability];
if (!foundEditableMap) {
return field;
}
// Ensure that system fields editability, even if modified to something else in DB(accidentally), get's reset to what's in the code.
return {
...field,
editable: foundEditableMap,
};
});
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(bookingFields);
};