'use client'; import React, { useCallback, useId, useMemo, useRef, useState } from 'react'; import type { DropResult, SensorAPI } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; import { motion } from 'framer-motion'; import { GripVerticalIcon, Plus, Trash } from 'lucide-react'; import { useSession } from 'next-auth/react'; import { useFieldArray, useForm } from 'react-hook-form'; import { prop, sortBy } from 'remeda'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth'; import { nanoid } from '@documenso/lib/universal/id'; import type { Field, Recipient } from '@documenso/prisma/client'; import { DocumentSigningOrder, RecipientRole, SendStatus } from '@documenso/prisma/client'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select'; import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '../button'; import { Checkbox } from '../checkbox'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { FormErrorMessage } from '../form/form-error-message'; import { Input } from '../input'; import { useStep } from '../stepper'; import { useToast } from '../use-toast'; import type { TAddSignersFormSchema } from './add-signers.types'; import { ZAddSignersFormSchema } from './add-signers.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerHeader, DocumentFlowFormContainerStep, } from './document-flow-root'; import { ShowFieldItem } from './show-field-item'; import type { DocumentFlowStep } from './types'; export type AddSignersFormProps = { documentFlow: DocumentFlowStep; recipients: Recipient[]; fields: Field[]; signingOrder?: DocumentSigningOrder | null; isDocumentEnterprise: boolean; onSubmit: (_data: TAddSignersFormSchema) => void; isDocumentPdfLoaded: boolean; }; export const AddSignersFormPartial = ({ documentFlow, recipients, fields, signingOrder, isDocumentEnterprise, onSubmit, isDocumentPdfLoaded, }: AddSignersFormProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { remaining } = useLimits(); const { data: session } = useSession(); const user = session?.user; const initialId = useId(); const $sensorApi = useRef(null); const { currentStep, totalSteps, previousStep } = useStep(); const defaultRecipients = [ { formId: initialId, name: '', email: '', role: RecipientRole.SIGNER, signingOrder: 1, actionAuth: undefined, }, ]; const form = useForm({ resolver: zodResolver(ZAddSignersFormSchema), defaultValues: { signers: recipients.length > 0 ? sortBy( recipients.map((recipient, index) => ({ nativeId: recipient.id, formId: String(recipient.id), name: recipient.name, email: recipient.email, role: recipient.role, signingOrder: recipient.signingOrder ?? index + 1, actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined, })), [prop('signingOrder'), 'asc'], [prop('nativeId'), 'asc'], ) : defaultRecipients, signingOrder: signingOrder || DocumentSigningOrder.PARALLEL, }, }); // Always show advanced settings if any recipient has auth options. const alwaysShowAdvancedSettings = useMemo(() => { const recipientHasAuthOptions = recipients.find((recipient) => { const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions); return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth; }); const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth); return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined; }, [recipients, form]); const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings); const { setValue, formState: { errors, isSubmitting }, control, watch, } = form; const watchedSigners = watch('signers'); const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL; const normalizeSigningOrders = (signers: typeof watchedSigners) => { return signers .sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0)) .map((signer, index) => ({ ...signer, signingOrder: index + 1 })); }; const onFormSubmit = form.handleSubmit(onSubmit); const { append: appendSigner, fields: signers, remove: removeSigner, } = useFieldArray({ control, name: 'signers', }); const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email); const isUserAlreadyARecipient = watchedSigners.some( (signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(), ); const hasDocumentBeenSent = recipients.some( (recipient) => recipient.sendStatus === SendStatus.SENT, ); const hasBeenSentToRecipientId = useCallback( (id?: number) => { if (!id) { return false; } return recipients.some( (recipient) => recipient.id === id && recipient.sendStatus === SendStatus.SENT && recipient.role !== RecipientRole.CC, ); }, [recipients], ); const onAddSigner = () => { appendSigner({ formId: nanoid(12), name: '', email: '', role: RecipientRole.SIGNER, actionAuth: undefined, signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1, }); }; const onRemoveSigner = (index: number) => { const signer = signers[index]; if (hasBeenSentToRecipientId(signer.nativeId)) { toast({ title: _(msg`Cannot remove signer`), description: _(msg`This signer has already received the document.`), variant: 'destructive', }); return; } removeSigner(index); const updatedSigners = signers.filter((_, idx) => idx !== index); form.setValue('signers', normalizeSigningOrders(updatedSigners)); }; const onAddSelfSigner = () => { if (emptySignerIndex !== -1) { setValue(`signers.${emptySignerIndex}.name`, user?.name ?? ''); setValue(`signers.${emptySignerIndex}.email`, user?.email ?? ''); } else { appendSigner({ formId: nanoid(12), name: user?.name ?? '', email: user?.email ?? '', role: RecipientRole.SIGNER, actionAuth: undefined, signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1, }); } }; const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && event.target instanceof HTMLInputElement) { onAddSigner(); } }; const onDragEnd = useCallback( async (result: DropResult) => { if (!result.destination) return; const items = Array.from(watchedSigners); const [reorderedSigner] = items.splice(result.source.index, 1); let insertIndex = result.destination.index; while (insertIndex < items.length && hasBeenSentToRecipientId(items[insertIndex].nativeId)) { insertIndex++; } items.splice(insertIndex, 0, reorderedSigner); const updatedSigners = items.map((item, index) => ({ ...item, signingOrder: hasBeenSentToRecipientId(item.nativeId) ? item.signingOrder : index + 1, })); updatedSigners.forEach((item, index) => { const keys: (keyof typeof item)[] = [ 'formId', 'nativeId', 'email', 'name', 'role', 'signingOrder', 'actionAuth', ]; keys.forEach((key) => { form.setValue(`signers.${index}.${key}` as const, item[key]); }); }); const currentLength = form.getValues('signers').length; if (currentLength > updatedSigners.length) { for (let i = updatedSigners.length; i < currentLength; i++) { form.unregister(`signers.${i}`); } } await form.trigger('signers'); }, [form, hasBeenSentToRecipientId, watchedSigners], ); const triggerDragAndDrop = useCallback( (fromIndex: number, toIndex: number) => { if (!$sensorApi.current) { return; } const draggableId = signers[fromIndex].id; const preDrag = $sensorApi.current.tryGetLock(draggableId); if (!preDrag) { return; } const drag = preDrag.snapLift(); setTimeout(() => { // Move directly to the target index if (fromIndex < toIndex) { for (let i = fromIndex; i < toIndex; i++) { drag.moveDown(); } } else { for (let i = fromIndex; i > toIndex; i--) { drag.moveUp(); } } setTimeout(() => { drag.drop(); }, 500); }, 0); }, [signers], ); const updateSigningOrders = useCallback( (newIndex: number, oldIndex: number) => { const updatedSigners = form.getValues('signers').map((signer, index) => { if (index === oldIndex) { return { ...signer, signingOrder: newIndex + 1 }; } else if (index >= newIndex && index < oldIndex) { return { ...signer, signingOrder: (signer.signingOrder ?? index + 1) + 1 }; } else if (index <= newIndex && index > oldIndex) { return { ...signer, signingOrder: Math.max(1, (signer.signingOrder ?? index + 1) - 1) }; } return signer; }); updatedSigners.forEach((signer, index) => { form.setValue(`signers.${index}.signingOrder`, signer.signingOrder); }); }, [form], ); const handleSigningOrderChange = useCallback( (index: number, newOrderString: string) => { const newOrder = parseInt(newOrderString, 10); if (!newOrderString.trim()) { return; } if (Number.isNaN(newOrder)) { form.setValue(`signers.${index}.signingOrder`, index + 1); return; } const newIndex = newOrder - 1; if (index !== newIndex) { updateSigningOrders(newIndex, index); triggerDragAndDrop(index, newIndex); } }, [form, triggerDragAndDrop, updateSigningOrders], ); return ( <> {isDocumentPdfLoaded && fields.map((field, index) => ( ))}
( field.onChange( checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL, ) } disabled={isSubmitting || hasDocumentBeenSent} /> Enable signing order )} /> { $sensorApi.current = api; }, ]} > {(provided) => (
{signers.map((signer, index) => ( {(provided, snapshot) => (
{isSigningOrderSequential && ( ( { field.onChange(e); handleSigningOrderChange(index, e.target.value); }} onBlur={(e) => { field.onBlur(); handleSigningOrderChange(index, e.target.value); }} disabled={ snapshot.isDragging || isSubmitting || hasBeenSentToRecipientId(signer.nativeId) } /> )} /> )} ( {!showAdvancedSettings && ( Email )} )} /> ( {!showAdvancedSettings && ( Name )} )} /> {showAdvancedSettings && isDocumentEnterprise && ( ( )} /> )}
( )} />
)}
))} {provided.placeholder}
)}
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
setShowAdvancedSettings(Boolean(value))} />
)}
void onFormSubmit()} /> ); };