Implement Add Guests and other fixes
parent
70f19289dd
commit
e83c83d951
|
@ -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"];
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1050,6 +1050,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const bookingInfo = {
|
||||
...bookingInfoRaw,
|
||||
responses: getBookingResponsesSchema(eventTypeRaw).parse(bookingInfoRaw.responses),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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") });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue