diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 45f06c81a7..796cd8a81a 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -295,7 +295,7 @@ const BookingPage = ({ const defaultValues = () => { if (!rescheduleUid) { const defaults = { - responses: {} as z.infer["responses"], + responses: {} as Partial["responses"]>, }; const responses = eventType.bookingFields.reduce((responses, field) => { @@ -322,7 +322,7 @@ const BookingPage = ({ } const defaults = { - responses: {} as z.infer["responses"], + responses: {} as Partial["responses"]>, }; const responses = eventType.bookingFields.reduce((responses, field) => { diff --git a/apps/web/lib/getBooking.tsx b/apps/web/lib/getBooking.tsx index d43f793196..87be46b45d 100644 --- a/apps/web/lib/getBooking.tsx +++ b/apps/web/lib/getBooking.tsx @@ -5,26 +5,28 @@ import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/ import slugify from "@calcom/lib/slugify"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; +type BookingSelect = { + description: true; + customInputs: true; + attendees: { + select: { + email: true; + name: true; + }; + }; + location: true; + smsReminderNumber: true; +}; + // Backward Compatibility for booking created before we had managed booking questions function getResponsesFromOldBooking( rawBooking: Prisma.BookingGetPayload<{ - select: { - description: true; - customInputs: true; - attendees: { - select: { - email: true; - name: true; - }; - }; - location: true; - smsReminderNumber: true; - }; + select: BookingSelect; }> ) { const customInputs = rawBooking.customInputs || {}; - const responses = Object.keys(customInputs).reduce((acc, key) => { - acc[slugify(key) as keyof typeof acc] = customInputs[key as keyof typeof customInputs]; + const responses = Object.keys(customInputs).reduce((acc, label) => { + acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs]; return acc; }, {}); return { @@ -70,12 +72,10 @@ async function getBooking( if (!rawBooking) { return rawBooking; } - const booking = { - ...rawBooking, - responses: getBookingResponsesPartialSchema({ - bookingFields, - }).parse(rawBooking.responses || getResponsesFromOldBooking(rawBooking)), - }; + + const booking = getBookingWithResponses(rawBooking, { + bookingFields, + }); if (booking) { // @NOTE: had to do this because Server side cant return [Object objects] @@ -88,4 +88,23 @@ async function getBooking( export type GetBookingType = Prisma.PromiseReturnType; +export const getBookingWithResponses = < + T extends Prisma.BookingGetPayload<{ + select: BookingSelect & { + responses: true; + }; + }> +>( + booking: T, + eventType: { + bookingFields: z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">; + } +) => { + return { + ...booking, + responses: getBookingResponsesPartialSchema({ + bookingFields: eventType.bookingFields, + }).parse(booking.responses || getResponsesFromOldBooking(booking)), + }; +}; export default getBooking; diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index fe09575a49..e49b02fe12 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -29,7 +29,6 @@ import { SystemField, getBookingFieldsWithSystemFields, } from "@calcom/features/bookings/lib/getBookingFields"; -import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { parseRecurringEvent } from "@calcom/lib"; import CustomBranding from "@calcom/lib/CustomBranding"; import { APP_NAME } from "@calcom/lib/constants"; @@ -48,6 +47,7 @@ import { Button, EmailInput, HeadSeo, Label } from "@calcom/ui"; import { FiX, FiChevronLeft, FiCheck, FiCalendar, FiExternalLink } from "@calcom/ui/components/icon"; import { timeZone } from "@lib/clock"; +import { getBookingWithResponses } from "@lib/getBooking"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import CancelBooking from "@components/booking/CancelBooking"; @@ -1018,6 +1018,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { notFound: true, }; } + const eventTypeRaw = !bookingInfoRaw.eventTypeId ? getDefaultEvent(eventTypeSlug || "") : await getEventTypesFromDB(bookingInfoRaw.eventTypeId); @@ -1027,10 +1028,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - const bookingInfo = { - ...bookingInfoRaw, - responses: getBookingResponsesPartialSchema(eventTypeRaw).parse(bookingInfoRaw.responses), - }; + const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw); // @NOTE: had to do this because Server side cant return [Object objects] // probably fixable with json.stringify -> json.parse diff --git a/packages/features/bookings/lib/getBookingFields.ts b/packages/features/bookings/lib/getBookingFields.ts index bdd510296b..c5efb1413a 100644 --- a/packages/features/bookings/lib/getBookingFields.ts +++ b/packages/features/bookings/lib/getBookingFields.ts @@ -193,7 +193,6 @@ export const ensureBookingInputsHaveSystemFields = ({ defaultLabel: "location", type: "radioInput", name: "location", - // Even though it should be required it is optional in production with backend choosing CalVideo as the fallback 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` diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index e6ed2479f8..2227215054 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -7,7 +7,7 @@ type EventType = Parameters[0]["eventType"]; export const getBookingResponsesPartialSchema = (eventType: EventType) => { const schema = bookingResponses.unwrap().partial().and(z.record(z.any())); - return preprocess({ schema, eventType, forQueryParsing: true }); + return preprocess({ schema, eventType, isPartialSchema: true }); }; // Should be used when we know that not all fields responses are present @@ -15,7 +15,7 @@ export const getBookingResponsesPartialSchema = (eventType: EventType) => { // - Can happen when we are parsing a booking's responses (which was created before we added a new required field) export default function getBookingResponsesSchema(eventType: EventType) { const schema = bookingResponses.and(z.record(z.any())); - return preprocess({ schema, eventType, forQueryParsing: false }); + return preprocess({ schema, eventType, isPartialSchema: false }); } // TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder @@ -23,14 +23,12 @@ export default function getBookingResponsesSchema(eventType: EventType) { function preprocess({ schema, eventType, - forQueryParsing, + isPartialSchema, }: { schema: T; - forQueryParsing: boolean; + isPartialSchema: boolean; eventType: { - bookingFields: z.infer & - z.infer & - z.BRAND<"HAS_SYSTEM_FIELDS">; + bookingFields: z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">; }; }): z.ZodType, z.infer, z.infer> { const preprocessed = z.preprocess( @@ -71,18 +69,18 @@ function preprocess({ eventType.bookingFields.forEach((bookingField) => { const value = responses[bookingField.name]; const stringSchema = z.string(); - const emailSchema = forQueryParsing ? z.string() : z.string().email(); - const phoneSchema = forQueryParsing + const emailSchema = isPartialSchema ? z.string() : z.string().email(); + const phoneSchema = isPartialSchema ? z.string() : z.string().refine((val) => isValidPhoneNumber(val)); // Tag the message with the input name so that the message can be shown at appropriate place const m = (message: string) => `{${bookingField.name}}${message}`; const isRequired = bookingField.required; - if ((forQueryParsing || !isRequired) && value === undefined) { + if ((isPartialSchema || !isRequired) && value === undefined) { return; } - if (isRequired && !forQueryParsing && !value) + if (isRequired && !isPartialSchema && !value) ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) }); if (bookingField.type === "email") { @@ -178,7 +176,7 @@ function preprocess({ }); }) ); - if (forQueryParsing) { + if (isPartialSchema) { // Query Params can be completely invalid, try to preprocess as much of it in correct format but in worst case simply don't prefill instead of crashing return preprocessed.catch(() => { console.error("Failed to preprocess query params, prefilling will be skipped"); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index bff459fc4c..ae895a7b0a 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -407,6 +407,8 @@ async function handler( isNotAnApiCall = false, }: { isNotAnApiCall?: boolean; + } = { + isNotAnApiCall: false, } ) { const { userId } = req; diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index dac6a7a366..72ffb92e07 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -166,11 +166,11 @@ export const FormBuilder = function FormBuilder({ onChange([ { label: "Option 1", - value: "1", + value: "Option 1", }, { label: "Option 2", - value: "2", + value: "Option 2", }, ]); } diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index c40bc61bfe..122344ca87 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -204,7 +204,7 @@ export const bookingCreateSchemaLegacyPropsForApi = z.object({ name: z.string(), guests: z.array(z.string()).optional(), notes: z.string().optional(), - location: z.string().optional(), + location: z.string(), smsReminderNumber: z.string().optional().nullable(), rescheduleReason: z.string().optional(), customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),