Make location required in zodSchema as was there in production
parent
83610f8c1b
commit
2e380580cd
|
@ -295,7 +295,7 @@ const BookingPage = ({
|
||||||
const defaultValues = () => {
|
const defaultValues = () => {
|
||||||
if (!rescheduleUid) {
|
if (!rescheduleUid) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
responses: {} as z.infer<typeof bookingFormSchema>["responses"],
|
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||||
|
@ -322,7 +322,7 @@ const BookingPage = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
responses: {} as z.infer<typeof bookingFormSchema>["responses"],
|
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||||
|
|
|
@ -5,26 +5,28 @@ import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
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
|
// Backward Compatibility for booking created before we had managed booking questions
|
||||||
function getResponsesFromOldBooking(
|
function getResponsesFromOldBooking(
|
||||||
rawBooking: Prisma.BookingGetPayload<{
|
rawBooking: Prisma.BookingGetPayload<{
|
||||||
select: {
|
select: BookingSelect;
|
||||||
description: true;
|
|
||||||
customInputs: true;
|
|
||||||
attendees: {
|
|
||||||
select: {
|
|
||||||
email: true;
|
|
||||||
name: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
location: true;
|
|
||||||
smsReminderNumber: true;
|
|
||||||
};
|
|
||||||
}>
|
}>
|
||||||
) {
|
) {
|
||||||
const customInputs = rawBooking.customInputs || {};
|
const customInputs = rawBooking.customInputs || {};
|
||||||
const responses = Object.keys(customInputs).reduce((acc, key) => {
|
const responses = Object.keys(customInputs).reduce((acc, label) => {
|
||||||
acc[slugify(key) as keyof typeof acc] = customInputs[key as keyof typeof customInputs];
|
acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs];
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
return {
|
return {
|
||||||
|
@ -70,12 +72,10 @@ async function getBooking(
|
||||||
if (!rawBooking) {
|
if (!rawBooking) {
|
||||||
return rawBooking;
|
return rawBooking;
|
||||||
}
|
}
|
||||||
const booking = {
|
|
||||||
...rawBooking,
|
const booking = getBookingWithResponses(rawBooking, {
|
||||||
responses: getBookingResponsesPartialSchema({
|
bookingFields,
|
||||||
bookingFields,
|
});
|
||||||
}).parse(rawBooking.responses || getResponsesFromOldBooking(rawBooking)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (booking) {
|
if (booking) {
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
|
@ -88,4 +88,23 @@ async function getBooking(
|
||||||
|
|
||||||
export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>;
|
export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>;
|
||||||
|
|
||||||
|
export const getBookingWithResponses = <
|
||||||
|
T extends Prisma.BookingGetPayload<{
|
||||||
|
select: BookingSelect & {
|
||||||
|
responses: true;
|
||||||
|
};
|
||||||
|
}>
|
||||||
|
>(
|
||||||
|
booking: T,
|
||||||
|
eventType: {
|
||||||
|
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...booking,
|
||||||
|
responses: getBookingResponsesPartialSchema({
|
||||||
|
bookingFields: eventType.bookingFields,
|
||||||
|
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||||
|
};
|
||||||
|
};
|
||||||
export default getBooking;
|
export default getBooking;
|
||||||
|
|
|
@ -29,7 +29,6 @@ import {
|
||||||
SystemField,
|
SystemField,
|
||||||
getBookingFieldsWithSystemFields,
|
getBookingFieldsWithSystemFields,
|
||||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
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 { FiX, FiChevronLeft, FiCheck, FiCalendar, FiExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
import { getBookingWithResponses } from "@lib/getBooking";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
|
@ -1018,6 +1018,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||||
? getDefaultEvent(eventTypeSlug || "")
|
? getDefaultEvent(eventTypeSlug || "")
|
||||||
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||||
|
@ -1027,10 +1028,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingInfo = {
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
||||||
...bookingInfoRaw,
|
|
||||||
responses: getBookingResponsesPartialSchema(eventTypeRaw).parse(bookingInfoRaw.responses),
|
|
||||||
};
|
|
||||||
|
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
|
|
|
@ -193,7 +193,6 @@ export const ensureBookingInputsHaveSystemFields = ({
|
||||||
defaultLabel: "location",
|
defaultLabel: "location",
|
||||||
type: "radioInput",
|
type: "radioInput",
|
||||||
name: "location",
|
name: "location",
|
||||||
// Even though it should be required it is optional in production with backend choosing CalVideo as the fallback
|
|
||||||
required: false,
|
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.
|
// 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`
|
// options: `eventType.locations`
|
||||||
|
|
|
@ -7,7 +7,7 @@ type EventType = Parameters<typeof preprocess>[0]["eventType"];
|
||||||
export const getBookingResponsesPartialSchema = (eventType: EventType) => {
|
export const getBookingResponsesPartialSchema = (eventType: EventType) => {
|
||||||
const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
|
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
|
// 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)
|
// - 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) {
|
export default function getBookingResponsesSchema(eventType: EventType) {
|
||||||
const schema = bookingResponses.and(z.record(z.any()));
|
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
|
// 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<T extends z.ZodType>({
|
function preprocess<T extends z.ZodType>({
|
||||||
schema,
|
schema,
|
||||||
eventType,
|
eventType,
|
||||||
forQueryParsing,
|
isPartialSchema,
|
||||||
}: {
|
}: {
|
||||||
schema: T;
|
schema: T;
|
||||||
forQueryParsing: boolean;
|
isPartialSchema: boolean;
|
||||||
eventType: {
|
eventType: {
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields> &
|
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||||
z.infer<typeof eventTypeBookingFields> &
|
|
||||||
z.BRAND<"HAS_SYSTEM_FIELDS">;
|
|
||||||
};
|
};
|
||||||
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
|
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
|
||||||
const preprocessed = z.preprocess(
|
const preprocessed = z.preprocess(
|
||||||
|
@ -71,18 +69,18 @@ function preprocess<T extends z.ZodType>({
|
||||||
eventType.bookingFields.forEach((bookingField) => {
|
eventType.bookingFields.forEach((bookingField) => {
|
||||||
const value = responses[bookingField.name];
|
const value = responses[bookingField.name];
|
||||||
const stringSchema = z.string();
|
const stringSchema = z.string();
|
||||||
const emailSchema = forQueryParsing ? z.string() : z.string().email();
|
const emailSchema = isPartialSchema ? z.string() : z.string().email();
|
||||||
const phoneSchema = forQueryParsing
|
const phoneSchema = isPartialSchema
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().refine((val) => isValidPhoneNumber(val));
|
: z.string().refine((val) => isValidPhoneNumber(val));
|
||||||
// Tag the message with the input name so that the message can be shown at appropriate place
|
// 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 m = (message: string) => `{${bookingField.name}}${message}`;
|
||||||
const isRequired = bookingField.required;
|
const isRequired = bookingField.required;
|
||||||
if ((forQueryParsing || !isRequired) && value === undefined) {
|
if ((isPartialSchema || !isRequired) && value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRequired && !forQueryParsing && !value)
|
if (isRequired && !isPartialSchema && !value)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
|
||||||
|
|
||||||
if (bookingField.type === "email") {
|
if (bookingField.type === "email") {
|
||||||
|
@ -178,7 +176,7 @@ function preprocess<T extends z.ZodType>({
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
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
|
// 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(() => {
|
return preprocessed.catch(() => {
|
||||||
console.error("Failed to preprocess query params, prefilling will be skipped");
|
console.error("Failed to preprocess query params, prefilling will be skipped");
|
||||||
|
|
|
@ -407,6 +407,8 @@ async function handler(
|
||||||
isNotAnApiCall = false,
|
isNotAnApiCall = false,
|
||||||
}: {
|
}: {
|
||||||
isNotAnApiCall?: boolean;
|
isNotAnApiCall?: boolean;
|
||||||
|
} = {
|
||||||
|
isNotAnApiCall: false,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { userId } = req;
|
const { userId } = req;
|
||||||
|
|
|
@ -166,11 +166,11 @@ export const FormBuilder = function FormBuilder({
|
||||||
onChange([
|
onChange([
|
||||||
{
|
{
|
||||||
label: "Option 1",
|
label: "Option 1",
|
||||||
value: "1",
|
value: "Option 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Option 2",
|
label: "Option 2",
|
||||||
value: "2",
|
value: "Option 2",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,7 @@ export const bookingCreateSchemaLegacyPropsForApi = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
guests: z.array(z.string()).optional(),
|
guests: z.array(z.string()).optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
location: z.string().optional(),
|
location: z.string(),
|
||||||
smsReminderNumber: z.string().optional().nullable(),
|
smsReminderNumber: z.string().optional().nullable(),
|
||||||
rescheduleReason: z.string().optional(),
|
rescheduleReason: z.string().optional(),
|
||||||
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
|
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
|
||||||
|
|
Loading…
Reference in New Issue