'use client'; import { useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { DateTime } from 'luxon'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { Field, FieldType } from '@documenso/prisma/client'; import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from '@documenso/ui/primitives/document-flow/document-flow-root'; import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { Input } from '@documenso/ui/primitives/input'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { ZAddSignatureFormSchema } from './add-signature.types'; import { SinglePlayerModeCustomTextField, SinglePlayerModeSignatureField, } from './single-player-mode-fields'; export type AddSignatureFormProps = { defaultValues?: TAddSignatureFormSchema; documentFlow: DocumentFlowStep; fields: FieldWithSignature[]; numberOfSteps: number; onSubmit: (_data: TAddSignatureFormSchema) => Promise | void; requireName?: boolean; requireSignature?: boolean; }; export const AddSignatureFormPartial = ({ defaultValues, documentFlow, fields, numberOfSteps, onSubmit, requireName = false, requireSignature = true, }: AddSignatureFormProps) => { const [validateUninsertedFields, setValidateUninsertedFields] = 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 (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: '', }, }); /** * 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; } 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('yyyy-MM-dd hh:mm a'), inserted: true, })) .with(FieldType.EMAIL, () => ({ ...field, customText: form.getValues('email'), inserted: true, })) .with(FieldType.NAME, () => ({ ...field, customText: form.getValues('name'), 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 { onFormValueChange(FieldType.SIGNATURE); field.onChange(value); }} /> )} /> )}
{validateUninsertedFields && uninsertedFields[0] && ( Click to insert field )} {localFields.map((field) => match(field.type) .with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => { return ( ); }) .with(FieldType.SIGNATURE, () => ( )) .otherwise(() => { return null; }), )}
); };