"use client"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { ClipboardEvent } from "react"; import type { UseFormReturn } from "react-hook-form"; import { Controller, useFieldArray, useWatch } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BooleanToggleGroupField, Button, EmptyScreen, FormCard, Icon, Label, SelectField, Skeleton, TextField, } from "@calcom/ui"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; import SingleForm, { getServerSidePropsForSingleFormView as getServerSideProps, } from "../../components/SingleForm"; import { FieldTypes } from "../../lib/FieldTypes"; export { getServerSideProps }; type SelectOption = { label: string; id: string | null }; type HookForm = UseFormReturn; const appendArray = ({ target, arrayToAppend, appendAt, }: { arrayToAppend: T[]; target: T[]; appendAt: number; }) => { // Avoid mutating the original array const copyOfTarget = [...target]; const numItemsToRemove = arrayToAppend.length; copyOfTarget.splice(appendAt, numItemsToRemove, ...arrayToAppend); return copyOfTarget; }; const PASTE_OPTIONS_SEPARATOR_REGEX = /\n+/; function Field({ hookForm, hookFieldNamespace, deleteField, moveUp, moveDown, appUrl, }: { fieldIndex: number; hookForm: HookForm; hookFieldNamespace: `fields.${number}`; deleteField: { check: () => boolean; fn: () => void; }; moveUp: { check: () => boolean; fn: () => void; }; moveDown: { check: () => boolean; fn: () => void; }; appUrl: string; }) { const { t } = useLocale(); const [animationRef] = useAutoAnimate(); const watchedOptions = useWatch({ control: hookForm.control, name: `${hookFieldNamespace}.options`, defaultValue: [ { label: "", id: uuidv4() }, { label: "", id: uuidv4() }, { label: "", id: uuidv4() }, { label: "", id: uuidv4() }, ], }) || []; const setOptions = (updatedOptions: SelectOption[]) => { hookForm.setValue(`${hookFieldNamespace}.options`, updatedOptions, { shouldDirty: true }); }; const handleRemoveOptions = (index: number) => { const updatedOptions = watchedOptions.filter((_, i) => i !== index); // We can't let the user remove the last option if (updatedOptions.length === 0) { return; } setOptions(updatedOptions); }; const addOption = () => { setOptions([ ...watchedOptions, { label: "", id: uuidv4(), }, ]); }; const router = hookForm.getValues(`${hookFieldNamespace}.router`); const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`); const updateLabelAtIndex = ({ label, optionIndex }: { label: string; optionIndex: number }) => { const updatedOptions = watchedOptions.map((opt, index) => ({ ...opt, ...(index === optionIndex ? { label } : {}), })); setOptions(updatedOptions); }; const label = useWatch({ control: hookForm.control, name: `${hookFieldNamespace}.label`, }); const identifier = useWatch({ control: hookForm.control, name: `${hookFieldNamespace}.identifier`, }); function move(index: number, increment: 1 | -1) { const newList = [...watchedOptions]; const type = watchedOptions[index]; const tmp = watchedOptions[index + increment]; if (tmp) { newList[index] = tmp; newList[index + increment] = type; } setOptions(newList); } const handlePasteInOptionAtIndex = ({ event, optionIndex, }: { event: ClipboardEvent; optionIndex: number; }) => { const paste = event.clipboardData.getData("text"); // The value being pasted could be a list of options const optionsBeingPasted = paste .split(PASTE_OPTIONS_SEPARATOR_REGEX) .map((optionLabel) => optionLabel.trim()) .filter((optionLabel) => optionLabel) .map((optionLabel) => ({ label: optionLabel.trim(), id: uuidv4() })); if (optionsBeingPasted.length === 1) { // If there is only one option, we would just let that option be pasted return; } // Don't allow pasting that value, as we would update the options through state update event.preventDefault(); const updatedOptions = appendArray({ target: watchedOptions, arrayToAppend: optionsBeingPasted, appendAt: optionIndex, }); setOptions(updatedOptions); }; const optionsPlaceholders = ["< 10", "10 - 100", "100 - 500", "> 500"]; return (
{ hookForm.setValue(`${hookFieldNamespace}.label`, e.target.value, { shouldDirty: true }); }} />
{ hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value, { shouldDirty: true }); }} />
{ const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value); return ( ({ ...baseStyles, fontSize: "14px", }), option: (baseStyles) => ({ ...baseStyles, fontSize: "14px", }), }} label="Type" isDisabled={!!router} containerClassName="data-testid-field-type" options={FieldTypes} onChange={(option) => { if (!option) { return; } onChange(option.value); }} defaultValue={defaultValue} /> ); }} />
{["select", "multiselect"].includes(hookForm.watch(`${hookFieldNamespace}.type`)) ? (
{t("options")}
    {watchedOptions.map((option, index) => (
  • handlePasteInOptionAtIndex({ event, optionIndex: index }) }>
    {watchedOptions.length && index !== 0 ? ( ) : null} {watchedOptions.length && index !== watchedOptions.length - 1 ? ( ) : null}
    updateLabelAtIndex({ label: e.target.value, optionIndex: index })} dataTestid={`${hookFieldNamespace}.options.${index}`} addOnSuffix={ watchedOptions.length > 1 ? ( ) : null } />
  • ))}
) : null}
{ return ( ); }} />
); } const FormEdit = ({ hookForm, form, appUrl, }: { hookForm: HookForm; form: inferSSRProps["form"]; appUrl: string; }) => { const fieldsNamespace = "fields"; const { fields: hookFormFields, append: appendHookFormField, remove: removeHookFormField, swap: swapHookFormField, // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore https://github.com/react-hook-form/react-hook-form/issues/6679 } = useFieldArray({ control: hookForm.control, name: fieldsNamespace, }); const [animationRef] = useAutoAnimate(); const addField = () => { appendHookFormField({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore id: uuidv4(), // This is same type from react-awesome-query-builder type: "text", label: "", }); }; // hookForm.reset(form); if (!form.fields) { form.fields = []; } return hookFormFields.length ? (
{hookFormFields.map((field, key) => { return ( hookFormFields.length > 1, fn: () => { removeHookFormField(key); }, }} moveUp={{ check: () => key !== 0, fn: () => { swapHookFormField(key, key - 1); }, }} moveDown={{ check: () => key !== hookFormFields.length - 1, fn: () => { if (key === hookFormFields.length - 1) { return; } swapHookFormField(key, key + 1); }, }} key={key} /> ); })}
{hookFormFields.length ? (
) : null}
) : (
Create Field } />
); }; export default function FormEditPage({ appUrl, ...props }: inferSSRProps & { appUrl: string }) { return ( } /> ); }