Implement Add Guests and other fixes

pull/6560/head
Hariom Balhara 2023-02-06 09:41:43 +05:30
parent 70f19289dd
commit e83c83d951
12 changed files with 287 additions and 127 deletions

View File

@ -33,25 +33,8 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import {
AddressInput,
Button,
EmailField,
EmailInput,
Form,
Group,
PhoneInput,
RadioField,
Tooltip,
} from "@calcom/ui";
import {
FiUserPlus,
FiCalendar,
FiCreditCard,
FiRefreshCw,
FiUser,
FiAlertTriangle,
} from "@calcom/ui/components/icon";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiCalendar, FiCreditCard, FiRefreshCw, FiUser, FiAlertTriangle } from "@calcom/ui/components/icon";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
@ -87,6 +70,7 @@ const BookingFields = ({
if (field.hidden) return null;
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
//TODO: `rescheduleReason` should be an enum or similar to avoid typos
if (field.name === "rescheduleReason") {
if (!rescheduleUid) {
return null;
@ -120,6 +104,8 @@ const BookingFields = ({
);
}
field.label = t(field.label);
return <FormBuilderField field={field} readOnly={readOnly} key={index} />;
})}
</>
@ -222,19 +208,26 @@ const BookingPage = ({
const rescheduleUid = router.query.rescheduleUid as string;
useTheme(profile.theme);
const date = asStringOrNull(router.query.date);
const querySchema = getBookingResponsesSchema(
{
bookingFields: eventType.bookingFields,
},
true
);
// string value for - text, textarea, select, radio,
// string value with , for checkbox and multiselect
// Object {value:"", optionValue:""} for radioInput
const parsedQuery = querySchema.parse({
...router.query,
guests: router.query.guest,
});
const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1);
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
const locations: LocationObject[] = useMemo(
() => (eventType.locations as LocationObject[]) || [],
[eventType.locations]
);
useEffect(() => {
if (router.query.guest) {
setGuestToggle(true);
}
}, [router.query.guest]);
const [isClientTimezoneAvailable, setIsClientTimezoneAvailable] = useState(false);
useEffect(() => {
// THis is to fix hydration error that comes because of different timezone on server and client
@ -242,24 +235,9 @@ const BookingPage = ({
}, []);
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
const guestListEmails = !isDynamicGroupBooking
? booking?.attendees.slice(1).map((attendee) => {
return { email: attendee.email };
})
: [];
//FIXME: We need to be backward compatible in terms of pre-filling the form
const getFormBuilderFieldValueFromQuery = (paramName: string) => {
const schema = getBookingResponsesSchema(
{
bookingFields: eventType.bookingFields,
},
true
);
// string value for - text, textarea, select, radio,
// string value with , for checkbox and multiselect
// Object {value:"", optionValue:""} for radioInput
return schema.parse(router.query)[paramName];
return parsedQuery[paramName];
};
// There should only exists one default userData variable for primaryAttendee.
@ -323,17 +301,7 @@ const BookingPage = ({
.passthrough();
type BookingFormValues = {
name: string;
email: string;
notes?: string;
locationType?: EventLocationType["type"];
guests?: string[];
address?: string;
attendeeAddress?: string;
phone?: string;
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
rescheduleReason?: string;
smsReminderNumber?: string;
responses: z.infer<typeof bookingFormSchema>["responses"];
};

View File

@ -41,6 +41,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);

View File

@ -1050,6 +1050,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
notFound: true,
};
}
const bookingInfo = {
...bookingInfoRaw,
responses: getBookingResponsesSchema(eventTypeRaw).parse(bookingInfoRaw.responses),

View File

@ -190,7 +190,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
startDate: periodDates.startDate,
endDate: periodDates.endDate,
},
bookingFields: eventType.bookingFields,
bookingFields: eventType.bookingFields.map((field) => ({
...field,
label: t(field.label),
})),
periodType: eventType.periodType,
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType,

View File

@ -231,7 +231,8 @@ export const useIsEmbed = (embedSsr?: boolean) => {
const _isValidNamespace = isValidNamespace(namespace);
if (parent !== window && !_isValidNamespace) {
log(
"Looks like you have iframed cal.com but not using Embed Snippet. Directly using an iframe isn't recommended."
`Looks like you have iframed cal.com but not using Embed Snippet.
Directly using an iframe isn't recommended.`
);
}
setIsEmbed(window?.isEmbed?.() || false);

View File

@ -5,11 +5,13 @@ import { bookingResponses, eventTypeBookingFields } from "@calcom/prisma/zod-uti
export default function getBookingResponsesSchema(
eventType: {
bookingFields: z.infer<typeof eventTypeBookingFields>;
bookingFields: z.infer<typeof eventTypeBookingFields> &
z.infer<typeof eventTypeBookingFields> &
z.BRAND<"HAS_SYSTEM_FIELDS">;
},
partial = false
forgiving = false
) {
const schema = partial
const schema = forgiving
? bookingResponses.partial().and(z.record(z.any()))
: bookingResponses.and(z.record(z.any()));
@ -32,53 +34,64 @@ export default function getBookingResponsesSchema(
});
return newResponses;
},
schema.superRefine((response, ctx) => {
eventType.bookingFields.forEach((input) => {
const value = response[input.name];
schema.superRefine((responses, ctx) => {
eventType.bookingFields.forEach((bookingField) => {
const value = responses[bookingField.name];
const emailSchema = forgiving ? z.string() : z.string().email();
const phoneSchema = forgiving ? z.string() : z.string().refine((val) => isValidPhoneNumber(val));
// Tag the message with the input name so that the message can be shown at appropriate plae
const m = (message: string) => `{${input.name}}${message}`;
if ((partial || !input.required) && value === undefined) {
const m = (message: string) => `{${bookingField.name}}${message}`;
if ((forgiving || !bookingField.required) && value === undefined) {
return;
}
if (input.required && !partial && !value)
if (bookingField.required && !forgiving && !value)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`Required`) });
if (input.type === "email") {
if (bookingField.type === "email") {
// Email RegExp to validate if the input is a valid email
if (!z.string().email().safeParse(value).success)
if (!emailSchema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
//TODO: How to do translation in booker language here?
message: m("That doesn't look like an email address"),
});
return;
}
}
if (bookingField.type === "multiemail") {
if (!emailSchema.array().safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
//TODO: How to do translation in booker language here?
message: m("That doesn't look like an email address"),
});
return;
}
return;
}
if (input.type === "phone") {
if (
!z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional()
.nullable()
.safeParse(value).success
) {
if (bookingField.type === "phone") {
if (!phoneSchema.safeParse(value).success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid Phone") });
}
}
if (input.type === "boolean") {
const schema = z.preprocess((val) => {
return val === "true";
}, z.boolean());
if (bookingField.type === "boolean") {
const schema = z.boolean();
if (!schema.safeParse(value).success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid Boolean") });
}
}
if (input.type === "radioInput" && input.optionsInputs) {
if (bookingField.type === "radioInput" && bookingField.optionsInputs) {
//FIXME: ManageBookings: If there is just one option then it is not required to show the radio options
// Also, if the option is there with one input, we need to show just the input and not radio
if (input.required && input.optionsInputs[value?.value]?.required && !value?.optionValue) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Required") });
if (
bookingField.required &&
bookingField.optionsInputs[value?.value]?.required &&
!value?.optionValue
) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Required Option Value") });
}
}

View File

@ -328,7 +328,7 @@ function getBookingData({
...reqBody,
name: responses.name,
email: responses.email,
guests: responses.guests ? responses.guests.split(",") : [],
guests: responses.guests ? responses.guests : [],
location: responses.location?.optionValue || responses.location?.value || "",
smsReminderNumber: responses.smsReminderNumber,
notes: responses.notes,

View File

@ -1,11 +1,14 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { z } from "zod";
import Widgets, {
TextLikeComponentProps,
SelectLikeComponentProps,
} from "@calcom/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets";
import { PhoneInput, AddressInput, Label, Group, RadioField } from "@calcom/ui";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PhoneInput, AddressInput, Button, Label, Group, RadioField, EmailField, Tooltip } from "@calcom/ui";
import { FiInfo, FiUserPlus, FiX } from "@calcom/ui/components/icon";
import { ComponentForField } from "./FormBuilder";
import { fieldsSchema } from "./FormBuilderFieldsSchema";
@ -71,12 +74,110 @@ export const Components = {
},
},
multiemail: {
propsType: "text",
factory: <TProps extends TextLikeComponentProps>(props: TProps) => {
//TODO: ManageBookings: Make it use multiemail
return <Widgets.TextWidget type="email" {...props} />;
propsType: "textList",
factory: <TProps extends SelectLikeComponentProps<string[]>>({
value,
label,
setValue,
...props
}: TProps) => {
const placeholder = props.placeholder;
const { t } = useLocale();
value = value || [];
const inputClassName =
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
return (
<>
{value.length ? (
<div className="mb-4">
<div>
<label
htmlFor="guests"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{label}
</label>
<ul>
{value.map((field, index) => (
<li key={index}>
<EmailField
value={value[index]}
onChange={(e) => {
value[index] = e.target.value;
setValue(value);
}}
className={classNames(
inputClassName,
// bookingForm.formState.errors.guests?.[index] &&
// "!focus:ring-red-700 !border-red-700",
"border-r-0"
)}
addOnClassname={classNames(
"border-gray-300 border block border-l-0 disabled:bg-gray-200 disabled:hover:cursor-not-allowed bg-transparent disabled:text-gray-500 dark:border-darkgray-300 "
// bookingForm.formState.errors.guests?.[index] &&
// "!focus:ring-red-700 !border-red-700"
)}
placeholder={placeholder}
label={<></>}
required
addOnSuffix={
<Tooltip content="Remove email">
<button
className="m-1 disabled:hover:cursor-not-allowed"
type="button"
onClick={() => {
value.splice(index, 1);
setValue(value);
}}>
<FiX className="text-gray-600" />
</button>
</Tooltip>
}
/>
{/* {bookingForm.formState.errors.guests?.[index] && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-red-700">
{bookingForm.formState.errors.guests?.[index]?.message}
</p>
</div>
)} */}
</li>
))}
</ul>
<Button
type="button"
color="minimal"
StartIcon={FiUserPlus}
className="my-2.5"
// className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"
onClick={() => {
value.push("");
setValue(value);
}}>
{t("add_another")}
</Button>
</div>
</div>
) : (
<></>
)}
{!value.length && (
<Button
color="minimal"
variant="button"
StartIcon={FiUserPlus}
onClick={() => {
value.push("");
setValue(value);
}}
className="mr-auto">
{label}
</Button>
)}
</>
);
},
valuePlaceholder: "Enter Email Addresses",
},
multiselect: {
propsType: "multiselect",

View File

@ -33,6 +33,10 @@ type RhfForm = {
type RhfFormFields = RhfForm["fields"];
type RhfFormField = RhfFormFields[number];
/**
* It works with a react-hook-form only.
* `formProp` specifies the name of the property in the react-hook-form that has the fields. This is where fields would be updated.
*/
export const FormBuilder = function FormBuilder({
title,
description,
@ -49,62 +53,76 @@ export const FormBuilder = function FormBuilder({
label: string;
needsOptions?: boolean;
systemOnly?: boolean;
isTextType?: boolean;
}[] = [
{
label: "Name",
value: "name",
isTextType: true,
},
{
label: "Email",
value: "email",
isTextType: true,
},
{
label: "Phone",
value: "phone",
isTextType: true,
},
{
label: "Short Text",
value: "text",
isTextType: true,
},
{
label: "Number",
value: "number",
isTextType: true,
},
{
label: "Long Text",
value: "textarea",
isTextType: true,
},
{
label: "Select",
value: "select",
needsOptions: true,
isTextType: true,
},
{
label: "MultiSelect",
value: "multiselect",
needsOptions: true,
isTextType: false,
},
{
label: "Multiple Emails",
value: "multiemail",
isTextType: true,
},
{
label: "Radio Input",
value: "radioInput",
isTextType: false,
},
{
label: "Checkbox Group",
value: "checkbox",
needsOptions: true,
isTextType: false,
},
{
label: "Radio Group",
value: "radio",
needsOptions: true,
isTextType: false,
},
{
label: "Checkbox",
value: "boolean",
isTextType: false,
},
];
// I would have like to give Form Builder it's own Form but nested Forms aren't something that browsers support.
@ -143,7 +161,6 @@ export const FormBuilder = function FormBuilder({
value: "2",
},
];
// const [optionsState, setOptionsState] = useState(options);
return (
<div className={className}>
<Label>{label}</Label>
@ -363,7 +380,6 @@ export const FormBuilder = function FormBuilder({
options={FieldTypes.filter((f) => !f.systemOnly)}
label="Input Type"
/>
<InputField {...fieldForm.register("label")} required containerClassName="mt-6" label="Label" />
<InputField
required
{...fieldForm.register("name")}
@ -374,6 +390,15 @@ export const FormBuilder = function FormBuilder({
}
label="Name"
/>
<InputField {...fieldForm.register("label")} required containerClassName="mt-6" label="Label" />
{fieldType?.isTextType ? (
<InputField
{...fieldForm.register("placeholder")}
containerClassName="mt-6"
label="Placeholder"
/>
) : null}
{fieldType?.needsOptions ? (
<Controller
name="options"
@ -420,17 +445,16 @@ const WithLabel = ({
readOnly: boolean;
children: React.ReactNode;
}) => {
const { t } = useLocale();
return (
<div>
<div className="mb-2 flex items-center">
{field.type !== "boolean" && <Label className="!mb-0">{field.label}</Label>}
{!readOnly && !field.required && (
<Badge className="ml-2" variant="gray">
{t("optional")}
</Badge>
)}
</div>
{field.type !== "boolean" && field.type !== "multiemail" && (
<div className="mb-2 flex items-center">
<Label className="!mb-0 flex items-center">{field.label}</Label>
<span className="ml-1 -mb-1 text-sm font-medium leading-none">
{!readOnly && field.required ? "*" : ""}
</span>
</div>
)}
{children}
</div>
);
@ -463,21 +487,16 @@ export const ComponentForField = ({
setValue,
readOnly,
}: {
field: {
field: RhfFormField & {
// Label is optional because radioInput doesn't have a label
label?: string;
required?: boolean;
name?: string;
options?: RhfFormField["options"];
optionsInputs?: RhfFormField["optionsInputs"];
type: RhfFormField["type"];
};
readOnly: boolean;
} & ValueProps) => {
const fieldType = field.type;
const componentConfig = Components[fieldType];
const isObjectiveWithInputValue = (value: any): value is { value: string } => {
return typeof value === "object" ? "value" in value : false;
return typeof value === "object" && value !== null ? "value" in value : false;
};
if (componentConfig.propsType === "text" || componentConfig.propsType === "boolean") {
@ -499,6 +518,7 @@ export const ComponentForField = ({
readOnly={readOnly}
value={value}
setValue={setValue}
placeholder={field.placeholder}
/>
</WithLabel>
);
@ -507,9 +527,33 @@ export const ComponentForField = ({
if (value !== undefined && typeof value !== "string") {
throw new Error(`${value}: Value is not of type string for field ${field.name}`);
}
return (
<WithLabel field={field} readOnly={readOnly}>
<componentConfig.factory label={field.label} readOnly={readOnly} value={value} setValue={setValue} />
<componentConfig.factory
placeholder={field.placeholder}
label={field.label}
readOnly={readOnly}
value={value}
setValue={setValue}
/>
</WithLabel>
);
}
if (componentConfig.propsType === "textList") {
if (value !== undefined && !(value instanceof Array)) {
throw new Error(`${value}: Value is not of type array for ${field.name}`);
}
return (
<WithLabel field={field} readOnly={readOnly}>
<componentConfig.factory
placeholder={field.placeholder}
label={field.label}
readOnly={readOnly}
value={value}
setValue={setValue}
/>
</WithLabel>
);
}
@ -527,6 +571,7 @@ export const ComponentForField = ({
<componentConfig.factory
readOnly={readOnly}
value={value}
placeholder={field.placeholder}
setValue={setValue}
options={field.options.map((o) => ({ ...o, title: o.label }))}
/>
@ -544,6 +589,7 @@ export const ComponentForField = ({
return (
<WithLabel field={field} readOnly>
<componentConfig.factory
placeholder={field.placeholder}
readOnly={readOnly}
value={value}
setValue={setValue as (value: string[]) => void}
@ -566,6 +612,7 @@ export const ComponentForField = ({
return field.options.length ? (
<WithLabel field={field} readOnly>
<componentConfig.factory
placeholder={field.placeholder}
readOnly={readOnly}
name={field.name}
value={value}
@ -579,7 +626,6 @@ export const ComponentForField = ({
throw new Error(`Field ${field.name} does not have a valid propsType`);
};
//TODO: ManageBookings: Move it along FormBuilder - Also create a story for it.
export const FormBuilderField = ({
field,
readOnly,
@ -594,7 +640,7 @@ export const FormBuilderField = ({
window.form = form;
}
return (
<div className="reloading mb-4">
<div data-form-builder-field-name={field.name} className="reloading mb-4">
<Controller
control={control}
// Make it a variable
@ -607,7 +653,7 @@ export const FormBuilderField = ({
value={value}
// Choose b/w disabled and readOnly
readOnly={readOnly}
setValue={(val: any) => {
setValue={(val: unknown) => {
onChange(val);
}}
/>
@ -620,7 +666,6 @@ export const FormBuilderField = ({
if (name !== field.name) {
return null;
}
// console.error(name, field.name, message, "ErrorMesg");
message = message.replace(/\{[^}]+\}(.*)/, "$1");
return (

View File

@ -2,7 +2,14 @@ import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import { userSelect } from "@calcom/prisma/selects";
import { CustomInputSchema, EventTypeMetaDataSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import {
CustomInputSchema,
EventTypeMetaDataSchema,
customInputSchema,
eventTypeBookingFields,
} from "@calcom/prisma/zod-utils";
import { ensureBookingInputsHaveSystemFields } from "./getEventTypeById";
type User = Prisma.UserGetPayload<typeof userSelect>;
@ -87,7 +94,12 @@ const commons = {
users: [user],
hosts: [],
metadata: EventTypeMetaDataSchema.parse({}),
bookingFields: eventTypeBookingFields.parse([]),
bookingFields: ensureBookingInputsHaveSystemFields({
bookingFields: eventTypeBookingFields.parse([]),
disableGuests: true,
additionalNotesRequired: false,
customInputs: customInputSchema.array().parse([]),
}),
};
const min15Event = {

View File

@ -49,9 +49,10 @@ export const ensureBookingInputsHaveSystemFields = ({
[EventTypeCustomInputType.PHONE]: BookingFieldType.phone,
};
const systemFields: typeof bookingFields = [
// These fields should be added before other user fields
const systemBeforeFields: typeof bookingFields = [
{
label: "Your name",
label: "your_name",
type: "name",
name: "name",
required: true,
@ -65,7 +66,7 @@ export const ensureBookingInputsHaveSystemFields = ({
],
},
{
label: "Your email",
label: "email_address",
type: "email",
name: "email",
required: true,
@ -79,7 +80,7 @@ export const ensureBookingInputsHaveSystemFields = ({
],
},
{
label: "Location",
label: "location",
type: "radioInput",
name: "location",
editable: "system",
@ -106,8 +107,12 @@ export const ensureBookingInputsHaveSystemFields = ({
},
],
},
];
// These fields should be added after other user fields
const systemAfterFields: typeof bookingFields = [
{
label: "Additional Notes",
label: "additional_notes",
type: "textarea",
name: "notes",
editable: "system-but-optional",
@ -121,7 +126,7 @@ export const ensureBookingInputsHaveSystemFields = ({
],
},
{
label: "Add guests",
label: "additional_guests",
type: "multiemail",
name: "guests",
editable: "system-but-optional",
@ -137,7 +142,7 @@ export const ensureBookingInputsHaveSystemFields = ({
},
{
//TODO: How to translate in user language?
label: "Reschedule Reason",
label: "reschedule_reason",
type: "textarea",
name: "rescheduleReason",
editable: "system",
@ -154,16 +159,16 @@ export const ensureBookingInputsHaveSystemFields = ({
},
];
const missingSystemFields = [];
// Push system fields first if any of them don't exist. We can't simply live without system fields
for (const field of systemFields) {
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)) {
missingSystemFields.push(field);
missingSystemBeforeFields.push(field);
}
}
bookingFields = missingSystemFields.concat(bookingFields);
bookingFields = missingSystemBeforeFields.concat(bookingFields);
// If we are migrating from old system, we need to add custom inputs to the end of the list
if (handleMigration) {
customInputs.forEach((input) => {
@ -188,6 +193,16 @@ export const ensureBookingInputsHaveSystemFields = ({
});
}
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);
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(bookingFields);
};

View File

@ -55,11 +55,11 @@ export const EventTypeMetaDataSchema = z
export const eventTypeBookingFields = formBuilderFieldsSchema;
// Real validation happens using getBookingResponsesSchema which requires eventType. Is there a better way to do it?
// Validation of user added bookingFields's responses happen using getBookingResponsesSchema which requires eventType.
export const bookingResponses = z.object({
email: z.string(),
name: z.string(),
guests: z.string().optional(),
guests: z.array(z.string()).optional(),
notes: z.string().optional(),
location: z
.object({