import { useEffect } from "react"; import type { z } from "zod"; import Widgets from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets"; import type { TextLikeComponentProps, SelectLikeComponentProps, } from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { PhoneInput, AddressInput, Button, Label, Group, RadioField, EmailField, Tooltip, InputField, CheckboxField, } from "@calcom/ui"; import { UserPlus, X } from "@calcom/ui/components/icon"; import { ComponentForField } from "./FormBuilderField"; import { propsTypes } from "./propsTypes"; import type { FieldType, variantsConfigSchema, fieldSchema } from "./schema"; import { preprocessNameFieldDataWithVariant } from "./utils"; export const isValidValueProp: Record boolean> = { boolean: (val) => typeof val === "boolean", multiselect: (val) => val instanceof Array && val.every((v) => typeof v === "string"), objectiveWithInput: (val) => (typeof val === "object" && val !== null ? "value" in val : false), select: (val) => typeof val === "string", text: (val) => typeof val === "string", textList: (val) => val instanceof Array && val.every((v) => typeof v === "string"), variants: (val) => (typeof val === "object" && val !== null) || typeof val === "string", }; type Component = | { propsType: "text"; factory: (props: TProps) => JSX.Element; } | { propsType: "textList"; factory: >(props: TProps) => JSX.Element; } | { propsType: "select"; factory: (props: TProps) => JSX.Element; } | { propsType: "boolean"; factory: >(props: TProps) => JSX.Element; } | { propsType: "multiselect"; factory: >(props: TProps) => JSX.Element; } | { // Objective type question with option having a possible input propsType: "objectiveWithInput"; factory: < TProps extends SelectLikeComponentProps<{ value: string; optionValue: string; }> & { optionsInputs: NonNullable["optionsInputs"]>; value: { value: string; optionValue: string }; } & { name?: string; required?: boolean; } >( props: TProps ) => JSX.Element; } | { propsType: "variants"; factory: < TProps extends Omit & { variant: string | undefined; variants: z.infer["variants"]; value: Record | string | undefined; setValue: (value: string | Record) => void; } >( props: TProps ) => JSX.Element; }; // TODO: Share FormBuilder components across react-query-awesome-builder(for Routing Forms) widgets. // There are certain differences b/w two. Routing Forms expect label to be provided by the widget itself and FormBuilder adds label itself and expect no label to be added by component. // Routing Form approach is better as it provides more flexibility to show the label in complex components. But that can't be done right now because labels are missing consistent asterisk required support across different components export const Components: Record = { text: { propsType: propsTypes.text, factory: (props) => , }, textarea: { propsType: propsTypes.textarea, // TODO: Make rows configurable in the form builder factory: (props) => , }, number: { propsType: propsTypes.number, factory: (props) => , }, name: { propsType: propsTypes.name, // Keep special "name" type field and later build split(FirstName and LastName) variant of it. factory: (props) => { const { variant: variantName = "fullName" } = props; const onChange = (name: string, value: string) => { let currentValue = props.value; if (typeof currentValue !== "object") { currentValue = {}; } props.setValue({ ...currentValue, [name]: value, }); }; if (!props.variants) { throw new Error("'variants' is required for 'name' type of field"); } if (variantName !== "firstAndLastName" && variantName !== "fullName") { throw new Error(`Invalid variant name '${variantName}' for 'name' type of field`); } const value = preprocessNameFieldDataWithVariant(variantName, props.value); if (variantName === "fullName") { if (typeof value !== "string") { throw new Error("Invalid value for 'fullName' variant"); } const variant = props.variants[variantName]; const variantField = variant.fields[0]; return ( { props.setValue(e.target.value); }} /> ); } const variant = props.variants[variantName]; if (typeof value !== "object") { throw new Error("Invalid value for 'fullName' variant"); } return (
{variant.fields.map((variantField) => ( onChange(variantField.name, e.target.value)} /> ))}
); }, }, phone: { propsType: propsTypes.phone, factory: ({ setValue, readOnly, ...props }) => { if (!props) { return
; } return ( { setValue(val); }} {...props} /> ); }, }, email: { propsType: propsTypes.email, factory: (props) => { if (!props) { return
; } return ; }, }, address: { propsType: propsTypes.address, factory: (props) => { return ( { props.setValue(val); }} {...props} /> ); }, }, multiemail: { propsType: propsTypes.multiemail, //TODO: Make it a ui component factory: function MultiEmail({ value, readOnly, label, setValue, ...props }) { const placeholder = props.placeholder; const { t } = useLocale(); value = value || []; const inputClassName = "dark:placeholder:text-muted focus:border-emphasis border-subtle block w-full rounded-md border-default text-sm focus:ring-black disabled:bg-emphasis disabled:hover:cursor-not-allowed dark:selection:bg-green-500 disabled:dark:text-subtle bg-default"; return ( <> {value.length ? (
    {value.map((field, index) => (
  • { value[index] = e.target.value; setValue(value); }} placeholder={placeholder} label={<>} required onClickAddon={() => { value.splice(index, 1); setValue(value); }} addOnSuffix={ !readOnly ? ( ) : null } />
  • ))}
{!readOnly && ( )}
) : ( <> )} {!value.length && !readOnly && ( )} ); }, }, multiselect: { propsType: propsTypes.multiselect, factory: (props) => { const newProps = { ...props, listValues: props.options.map((o) => ({ title: o.label, value: o.value })), }; return ; }, }, select: { propsType: propsTypes.select, factory: (props) => { const newProps = { ...props, listValues: props.options.map((o) => ({ title: o.label, value: o.value })), }; return ; }, }, checkbox: { propsType: propsTypes.checkbox, factory: ({ options, readOnly, setValue, value }) => { value = value || []; return (
{options.map((option, i) => { return ( ); })}
); }, }, radio: { propsType: propsTypes.radio, factory: ({ setValue, name, value, options }) => { return ( { setValue(e); }}> <> {options.map((option, i) => ( ))} ); }, }, radioInput: { propsType: propsTypes.radioInput, factory: function RadioInputWithLabel({ name, options, optionsInputs, value, setValue, readOnly }) { useEffect(() => { if (!value) { setValue({ value: options[0]?.value, optionValue: "", }); } }, [options, setValue, value]); return (
{options.length > 1 ? ( options.map((option, i) => { return ( ); }) ) : ( // Show option itself as label because there is just one option <> )}
{(() => { const optionField = optionsInputs[value?.value]; if (!optionField) { return null; } return (
{ setValue({ value: value?.value, optionValue: val || "", }); }} />
); })()}
); }, }, boolean: { propsType: propsTypes.boolean, factory: ({ readOnly, label, value, setValue }) => { return (
{ if (e.target.checked) { setValue(true); } else { setValue(false); } }} placeholder="" checked={value} disabled={readOnly} description="" // Form Builder ensures that it would be safe HTML in here if the field type supports it. So, we can safely use label value in `descriptionAsSafeHtml` descriptionAsSafeHtml={label ?? ""} />
); }, }, } as const; // Should use `statisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config