2023-02-07 12:35:36 +00:00
|
|
|
import { useEffect } from "react";
|
2023-01-24 14:45:22 +00:00
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
import Widgets, {
|
2023-01-30 03:11:35 +00:00
|
|
|
TextLikeComponentProps,
|
|
|
|
SelectLikeComponentProps,
|
2023-01-24 14:45:22 +00:00
|
|
|
} from "@calcom/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets";
|
2023-02-06 04:11:43 +00:00
|
|
|
import { classNames } from "@calcom/lib";
|
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2023-02-07 12:35:36 +00:00
|
|
|
import type { BookingFieldType } from "@calcom/prisma/zod-utils";
|
2023-02-06 04:11:43 +00:00
|
|
|
import { PhoneInput, AddressInput, Button, Label, Group, RadioField, EmailField, Tooltip } from "@calcom/ui";
|
2023-02-07 12:35:36 +00:00
|
|
|
import { FiUserPlus, FiX } from "@calcom/ui/components/icon";
|
2023-01-24 14:45:22 +00:00
|
|
|
|
|
|
|
import { ComponentForField } from "./FormBuilder";
|
|
|
|
import { fieldsSchema } from "./FormBuilderFieldsSchema";
|
|
|
|
|
2023-02-07 12:35:36 +00:00
|
|
|
type Component =
|
|
|
|
| {
|
|
|
|
propsType: "text";
|
|
|
|
factory: <TProps extends TextLikeComponentProps>(props: TProps) => JSX.Element;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
propsType: "textList";
|
|
|
|
factory: <TProps extends TextLikeComponentProps<string[]>>(props: TProps) => JSX.Element;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
propsType: "select";
|
|
|
|
factory: <TProps extends SelectLikeComponentProps>(props: TProps) => JSX.Element;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
propsType: "boolean";
|
|
|
|
factory: <TProps extends TextLikeComponentProps<boolean>>(props: TProps) => JSX.Element;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
propsType: "multiselect";
|
|
|
|
factory: <TProps extends SelectLikeComponentProps<string[]>>(props: TProps) => JSX.Element;
|
|
|
|
}
|
|
|
|
| {
|
2023-02-08 13:56:18 +00:00
|
|
|
// Objective type question with option having a possible input
|
2023-02-07 12:35:36 +00:00
|
|
|
propsType: "objectiveWithInput";
|
|
|
|
factory: <
|
|
|
|
TProps extends SelectLikeComponentProps<{
|
|
|
|
value: string;
|
|
|
|
optionValue: string;
|
|
|
|
}> & {
|
|
|
|
optionsInputs: NonNullable<z.infer<typeof fieldsSchema>[number]["optionsInputs"]>;
|
|
|
|
value: { value: string; optionValue: string };
|
|
|
|
} & {
|
|
|
|
name?: string;
|
|
|
|
}
|
|
|
|
>(
|
|
|
|
props: TProps
|
|
|
|
) => JSX.Element;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const Components: Record<BookingFieldType, Component> = {
|
2023-01-24 14:45:22 +00:00
|
|
|
text: {
|
|
|
|
propsType: "text",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => <Widgets.TextWidget {...props} />,
|
2023-01-24 14:45:22 +00:00
|
|
|
},
|
|
|
|
textarea: {
|
|
|
|
propsType: "text",
|
2023-02-08 13:56:18 +00:00
|
|
|
// TODO: Make rows configurable in the form builder
|
|
|
|
factory: (props) => <Widgets.TextAreaWidget rows={3} {...props} />,
|
2023-01-24 14:45:22 +00:00
|
|
|
},
|
|
|
|
number: {
|
|
|
|
propsType: "text",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => <Widgets.NumberWidget {...props} />,
|
2023-01-24 14:45:22 +00:00
|
|
|
},
|
2023-02-01 05:18:42 +00:00
|
|
|
name: {
|
|
|
|
propsType: "text",
|
|
|
|
// Keep special "name" type field and later build split(FirstName and LastName) variant of it.
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => <Widgets.TextWidget {...props} />,
|
2023-02-01 05:18:42 +00:00
|
|
|
},
|
2023-01-24 14:45:22 +00:00
|
|
|
phone: {
|
|
|
|
propsType: "text",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: ({ setValue, ...props }) => {
|
2023-01-24 14:45:22 +00:00
|
|
|
if (!props) {
|
|
|
|
return <div />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<PhoneInput
|
|
|
|
onChange={(val: string) => {
|
2023-01-30 03:11:35 +00:00
|
|
|
setValue(val);
|
2023-01-24 14:45:22 +00:00
|
|
|
}}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
email: {
|
|
|
|
propsType: "text",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => {
|
2023-01-24 14:45:22 +00:00
|
|
|
if (!props) {
|
|
|
|
return <div />;
|
|
|
|
}
|
|
|
|
// FIXME: type=email is removed so that RHF validations can work
|
|
|
|
// But, RHF errors are not integrated in Routing Forms form
|
|
|
|
return <Widgets.TextWidget {...props} />;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
address: {
|
|
|
|
propsType: "text",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => {
|
2023-01-24 14:45:22 +00:00
|
|
|
return (
|
|
|
|
<AddressInput
|
|
|
|
onChange={(val) => {
|
|
|
|
props.setValue(val);
|
|
|
|
}}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2023-01-30 03:11:35 +00:00
|
|
|
multiemail: {
|
2023-02-06 04:11:43 +00:00
|
|
|
propsType: "textList",
|
2023-02-08 13:56:18 +00:00
|
|
|
//TODO: Make it a ui component
|
|
|
|
factory: function MultiEmail({ value, label, setValue, ...props }) {
|
2023-02-06 04:11:43 +00:00
|
|
|
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 ? (
|
2023-02-08 13:56:18 +00:00
|
|
|
<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] && (
|
2023-02-06 04:11:43 +00:00
|
|
|
<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>
|
|
|
|
)} */}
|
2023-02-08 13:56:18 +00:00
|
|
|
</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>
|
2023-02-06 04:11:43 +00:00
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<></>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!value.length && (
|
|
|
|
<Button
|
|
|
|
color="minimal"
|
|
|
|
variant="button"
|
|
|
|
StartIcon={FiUserPlus}
|
|
|
|
onClick={() => {
|
|
|
|
value.push("");
|
|
|
|
setValue(value);
|
|
|
|
}}
|
|
|
|
className="mr-auto">
|
|
|
|
{label}
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
2023-01-30 03:11:35 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
multiselect: {
|
|
|
|
propsType: "multiselect",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => {
|
2023-01-30 03:11:35 +00:00
|
|
|
const newProps = {
|
|
|
|
...props,
|
|
|
|
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
|
|
|
|
};
|
|
|
|
return <Widgets.MultiSelectWidget {...newProps} />;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
propsType: "select",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: (props) => {
|
2023-01-30 03:11:35 +00:00
|
|
|
const newProps = {
|
|
|
|
...props,
|
|
|
|
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
|
|
|
|
};
|
|
|
|
return <Widgets.SelectWidget {...newProps} />;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
checkbox: {
|
|
|
|
propsType: "multiselect",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: ({ options, readOnly, setValue, value }) => {
|
|
|
|
value = value || [];
|
2023-01-30 03:11:35 +00:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{options.map((option, i) => {
|
|
|
|
return (
|
|
|
|
<label key={i} className="block">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
disabled={readOnly}
|
|
|
|
onChange={(e) => {
|
|
|
|
const newValue = value.filter((v) => v !== option.value);
|
|
|
|
if (e.target.checked) {
|
|
|
|
newValue.push(option.value);
|
|
|
|
}
|
|
|
|
setValue(newValue);
|
|
|
|
}}
|
|
|
|
// disabled={!!disableLocations}
|
|
|
|
//TODO: ManageBookings: What does this location class do?
|
|
|
|
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
|
|
|
value={option.value}
|
|
|
|
checked={value.includes(option.value)}
|
|
|
|
/>
|
|
|
|
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
|
|
|
|
{option.label ?? ""}
|
|
|
|
</span>
|
|
|
|
</label>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
radio: {
|
|
|
|
propsType: "select",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: ({ setValue, value, options }) => {
|
2023-01-30 03:11:35 +00:00
|
|
|
return (
|
|
|
|
<Group
|
|
|
|
value={value}
|
|
|
|
onValueChange={(e) => {
|
|
|
|
setValue(e);
|
|
|
|
}}>
|
|
|
|
<>
|
|
|
|
{options.map((option, i) => (
|
|
|
|
<RadioField
|
|
|
|
label={option.label}
|
|
|
|
key={`option.${i}.radio`}
|
|
|
|
value={option.label}
|
|
|
|
id={`option.${i}.radio`}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
</Group>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
radioInput: {
|
|
|
|
propsType: "objectiveWithInput",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: function RadioInputWithLabel({ name, options, optionsInputs, value, setValue, readOnly }) {
|
2023-01-24 14:45:22 +00:00
|
|
|
useEffect(() => {
|
|
|
|
setValue({
|
|
|
|
value: options[0]?.value,
|
|
|
|
optionValue: "",
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div
|
|
|
|
className="mb-4"
|
|
|
|
onChange={(e) => {
|
|
|
|
setValue({
|
|
|
|
// TODO: ManageBookings: onChange fires on parent of radio inputs but onChange isn't allowed to have a value for div in TS
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
value: e.target.value || "",
|
|
|
|
optionValue: "",
|
|
|
|
});
|
|
|
|
}}>
|
|
|
|
{options.length > 1 ? (
|
|
|
|
options.map((option, i) => {
|
|
|
|
return (
|
|
|
|
<label key={i} className="block">
|
|
|
|
<input
|
|
|
|
type="radio"
|
|
|
|
disabled={readOnly}
|
|
|
|
name={name}
|
|
|
|
// disabled={!!disableLocations}
|
|
|
|
//TODO: ManageBookings: What does this location class do?
|
|
|
|
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
|
|
|
value={option.value}
|
2023-02-08 13:56:18 +00:00
|
|
|
defaultChecked={i === 0}
|
2023-01-24 14:45:22 +00:00
|
|
|
/>
|
|
|
|
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
|
|
|
|
{option.label ?? ""}
|
|
|
|
</span>
|
|
|
|
</label>
|
|
|
|
);
|
|
|
|
})
|
|
|
|
) : (
|
|
|
|
<span className="text-sm">{options[0].label}</span>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{(() => {
|
|
|
|
const optionField = optionsInputs[value?.value];
|
|
|
|
if (!optionField) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<ComponentForField
|
2023-02-08 13:56:18 +00:00
|
|
|
readOnly={!!readOnly}
|
2023-02-07 12:35:36 +00:00
|
|
|
field={{
|
|
|
|
...optionField,
|
|
|
|
// Option Input is considered required only. Configuration not supported yet
|
|
|
|
required: true,
|
|
|
|
name: "optionField",
|
|
|
|
}}
|
2023-01-24 14:45:22 +00:00
|
|
|
value={value?.optionValue}
|
|
|
|
setValue={(val: string) => {
|
|
|
|
setValue({
|
|
|
|
value: value?.value,
|
|
|
|
optionValue: val,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2023-01-30 03:11:35 +00:00
|
|
|
boolean: {
|
|
|
|
propsType: "boolean",
|
2023-02-07 12:35:36 +00:00
|
|
|
factory: ({ readOnly, label, value, setValue }) => {
|
2023-01-30 03:11:35 +00:00
|
|
|
return (
|
|
|
|
<div className="flex">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
onChange={(e) => {
|
|
|
|
if (e.target.checked) {
|
|
|
|
setValue(true);
|
|
|
|
} else {
|
|
|
|
setValue(false);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
|
|
|
placeholder=""
|
|
|
|
checked={value}
|
|
|
|
disabled={readOnly}
|
|
|
|
/>
|
|
|
|
<Label className="-mt-px block text-sm font-medium text-gray-700 dark:text-white">{label}</Label>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2023-01-24 14:45:22 +00:00
|
|
|
} as const;
|
2023-02-01 05:18:42 +00:00
|
|
|
// Should use `statisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config
|