'use client'; import { useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans } from '@lingui/macro'; import { DateTime } from 'luxon'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { Field } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { FieldToolTip } from '../../components/field/field-tooltip'; import { cn } from '../../lib/utils'; import { Card, CardContent } from '../card'; import { ElementVisible } from '../element-visible'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { Input } from '../input'; import { SignaturePad } from '../signature-pad'; import { useStep } from '../stepper'; import type { TAddSignatureFormSchema } from './add-signature.types'; import { ZAddSignatureFormSchema } from './add-signature.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerHeader, DocumentFlowFormContainerStep, } from './document-flow-root'; import { SinglePlayerModeCustomTextField, SinglePlayerModeSignatureField, } from './single-player-mode-fields'; export type AddSignatureFormProps = { defaultValues?: TAddSignatureFormSchema; documentFlow: DocumentFlowStep; fields: FieldWithSignature[]; onSubmit: (_data: TAddSignatureFormSchema) => Promise | void; requireName?: boolean; requireCustomText?: boolean; requireSignature?: boolean; }; export const AddSignatureFormPartial = ({ defaultValues, documentFlow, fields, onSubmit, requireName = false, requireCustomText = false, requireSignature = true, }: AddSignatureFormProps) => { const { currentStep, totalSteps } = useStep(); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [isSignatureValid, setIsSignatureValid] = useState(false); // Refined schema which takes into account whether to allow an empty name or signature. const refinedSchema = ZAddSignatureFormSchema.superRefine((val, ctx) => { if (requireName && val.name.length === 0) { ctx.addIssue({ path: ['name'], code: 'custom', message: 'Name is required', }); } if (requireCustomText && val.customText.length === 0) { ctx.addIssue({ path: ['customText'], code: 'custom', message: 'Text is required', }); } if (requireSignature && val.signature.length === 0) { ctx.addIssue({ path: ['signature'], code: 'custom', message: 'Signature is required', }); } }); const form = useForm({ resolver: zodResolver(refinedSchema), defaultValues: defaultValues ?? { name: '', email: '', signature: '', customText: '', }, }); /** * A local copy of the provided fields to modify. */ const [localFields, setLocalFields] = useState(JSON.parse(JSON.stringify(fields))); const uninsertedFields = useMemo(() => { const fields = localFields.filter((field) => !field.inserted); return sortFieldsByPosition(fields); }, [localFields]); const onValidateFields = async (values: TAddSignatureFormSchema) => { setValidateUninsertedFields(true); const isFieldsValid = validateFieldsInserted(localFields); if (!isFieldsValid) { return; } await onSubmit(values); }; /** * Validates whether the corresponding form for a given field type is valid. * * @returns `true` if the form associated with the provided field is valid, `false` otherwise. */ const validateFieldForm = async (fieldType: Field['type']): Promise => { if (fieldType === FieldType.SIGNATURE) { await form.trigger('signature'); return !form.formState.errors.signature; } if (fieldType === FieldType.NAME) { await form.trigger('name'); return !form.formState.errors.name; } if (fieldType === FieldType.EMAIL) { await form.trigger('email'); return !form.formState.errors.email; } if (fieldType === FieldType.TEXT) { await form.trigger('customText'); return !form.formState.errors.customText; } if (fieldType === FieldType.NUMBER) { await form.trigger('number'); return !form.formState.errors.number; } if (fieldType === FieldType.RADIO) { await form.trigger('radio'); return !form.formState.errors.radio; } if (fieldType === FieldType.CHECKBOX) { await form.trigger('checkbox'); return !form.formState.errors.checkbox; } if (fieldType === FieldType.DROPDOWN) { await form.trigger('dropdown'); return !form.formState.errors.dropdown; } return true; }; /** * Insert the corresponding form value into a given field. */ const insertFormValueIntoField = (field: Field) => { return match(field.type) .with(FieldType.DATE, () => ({ ...field, customText: DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT), inserted: true, })) .with(FieldType.EMAIL, () => ({ ...field, customText: form.getValues('email'), inserted: true, })) .with(FieldType.NAME, () => ({ ...field, customText: form.getValues('name'), inserted: true, })) .with(FieldType.TEXT, FieldType.NUMBER, FieldType.RADIO, FieldType.CHECKBOX, () => ({ ...field, customText: form.getValues('customText'), inserted: true, })) .with(FieldType.SIGNATURE, () => { const value = form.getValues('signature'); return { ...field, value, Signature: { id: -1, recipientId: -1, fieldId: -1, created: new Date(), signatureImageAsBase64: value, typedSignature: null, }, inserted: true, }; }) .otherwise(() => { throw new Error('Unsupported field'); }); }; const insertField = (field: Field) => async () => { const isFieldFormValid = await validateFieldForm(field.type); if (!isFieldFormValid) { return; } setLocalFields((prev) => prev.map((prevField) => { if (prevField.id !== field.id) { return prevField; } return insertFormValueIntoField(field); }), ); }; /** * When a form value changes, reset all the corresponding fields to be uninserted. */ const onFormValueChange = (fieldType: FieldType) => { setLocalFields((fields) => fields.map((field) => { if (field.type !== fieldType) { return field; } return { ...field, inserted: false, }; }), ); }; return ( <>
( Email { onFormValueChange(FieldType.EMAIL); field.onChange(value); }} /> )} /> {requireName && ( ( Name { onFormValueChange(FieldType.NAME); field.onChange(value); }} /> )} /> )} {requireSignature && ( ( Signature { setIsSignatureValid(isValid); if (!isValid) { field.onChange(null); } }} onChange={(value) => { if (isSignatureValid) { onFormValueChange(FieldType.SIGNATURE); field.onChange(value); } }} /> )} /> )} {requireCustomText && ( ( Custom Text { onFormValueChange(FieldType.TEXT); field.onChange(value); }} /> )} /> )}
{validateUninsertedFields && uninsertedFields[0] && ( Click to insert field )} {localFields.map((field) => match(field.type) .with( FieldType.DATE, FieldType.TEXT, FieldType.EMAIL, FieldType.NAME, FieldType.NUMBER, FieldType.CHECKBOX, FieldType.RADIO, FieldType.DROPDOWN, () => { return ( ); }, ) .with(FieldType.SIGNATURE, () => ( )) .otherwise(() => { return null; }), )}
); };