cal.pub0.org/packages/features/form-builder/Components.tsx

416 lines
14 KiB
TypeScript

import { useEffect } from "react";
import { z } from "zod";
import Widgets, {
TextLikeComponentProps,
SelectLikeComponentProps,
} from "@calcom/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { BookingFieldType } from "@calcom/prisma/zod-utils";
import { PhoneInput, AddressInput, Button, Label, Group, RadioField, EmailField, Tooltip } from "@calcom/ui";
import { FiUserPlus, FiX } from "@calcom/ui/components/icon";
import { ComponentForField } from "./FormBuilder";
import { fieldsSchema } from "./FormBuilderFieldsSchema";
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;
}
| {
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> = {
text: {
propsType: "text",
factory: (props) => <Widgets.TextWidget {...props} />,
},
textarea: {
propsType: "text",
factory: (props) => <Widgets.TextAreaWidget {...props} />,
},
number: {
propsType: "text",
factory: (props) => <Widgets.NumberWidget {...props} />,
},
name: {
propsType: "text",
// Keep special "name" type field and later build split(FirstName and LastName) variant of it.
factory: (props) => <Widgets.TextWidget {...props} />,
},
phone: {
propsType: "text",
factory: ({ setValue, ...props }) => {
if (!props) {
return <div />;
}
return (
<PhoneInput
onChange={(val: string) => {
setValue(val);
}}
{...props}
/>
);
},
},
email: {
propsType: "text",
factory: (props) => {
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",
factory: (props) => {
return (
<AddressInput
onChange={(val) => {
props.setValue(val);
}}
{...props}
/>
);
},
},
multiemail: {
propsType: "textList",
factory: ({ value, label, setValue, ...props }) => {
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>
)}
</>
);
},
},
multiselect: {
propsType: "multiselect",
factory: (props) => {
const newProps = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.MultiSelectWidget {...newProps} />;
},
},
select: {
propsType: "select",
factory: (props) => {
const newProps = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.SelectWidget {...newProps} />;
},
},
checkbox: {
propsType: "multiselect",
factory: ({ options, readOnly, setValue, value }) => {
value = value || [];
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",
factory: ({ setValue, value, options }) => {
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",
factory: function RadioInputWithLabel({ name, options, optionsInputs, value, setValue, readOnly }) {
useEffect(() => {
setValue({
value: options[0]?.value,
optionValue: "",
});
}, []);
// const getLocationInputField = () => {
// return (
// <div className="mb-4">
// {/* <Label>
// </Label> */}
// {Field ? (
// <div>
// <div className="mt-1">{Field}</div>
// {bookingForm.formState.errors.phone && (
// <div className="mt-2 flex items-center text-sm text-red-700 ">
// <Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
// <p>{t("invalid_number")}</p>
// </div>
// )}
// </div>
// ) : null}
// </div>
// );
// };
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}
/>
<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
readOnly
field={{
...optionField,
// Option Input is considered required only. Configuration not supported yet
required: true,
name: "optionField",
}}
value={value?.optionValue}
setValue={(val: string) => {
setValue({
value: value?.value,
optionValue: val,
});
}}
/>
</div>
);
})()}
</div>
);
},
},
boolean: {
propsType: "boolean",
factory: ({ readOnly, label, value, setValue }) => {
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>
);
},
},
} as const;
// Should use `statisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config