import { useEffect, useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { RecipientRole } from '@prisma/client'; import { PlusIcon, TrashIcon } from 'lucide-react'; import { type FieldError, useFieldArray, useForm } from 'react-hook-form'; import { z } from 'zod'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { nanoid } from '@documenso/lib/universal/id'; import { trpc } from '@documenso/trpc/react'; import { RecipientAutoCompleteInput } from '@documenso/ui/components/recipient/recipient-autocomplete-input'; import type { RecipientAutoCompleteOption } from '@documenso/ui/components/recipient/recipient-autocomplete-input'; import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { DialogFooter } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@documenso/ui/primitives/tooltip'; import type { RecipientForCreation } from '~/utils/detect-document-recipients'; const ZSuggestedRecipientSchema = z.object({ formId: z.string().min(1), name: z .string() .min(1, { message: msg`Name is required`.id }) .max(255), email: z .string() .min(1, { message: msg`Email is required`.id }) .email({ message: msg`Invalid email`.id }) .max(254), role: z.nativeEnum(RecipientRole), }); const ZSuggestedRecipientsFormSchema = z.object({ recipients: z .array(ZSuggestedRecipientSchema) .min(1, { message: msg`Please add at least one recipient`.id }), }); type TSuggestedRecipientsFormSchema = z.infer; export type SuggestedRecipientsFormProps = { recipients: RecipientForCreation[] | null; onCancel: () => void; onSubmit: (recipients: RecipientForCreation[]) => Promise | void; onAutoAddFields?: (recipients: RecipientForCreation[]) => Promise | void; isProcessing?: boolean; }; export const SuggestedRecipientsForm = ({ recipients, onCancel, onSubmit, onAutoAddFields, isProcessing = false, }: SuggestedRecipientsFormProps) => { const { t } = useLingui(); const [recipientSearchQuery, setRecipientSearchQuery] = useState(''); const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500); const { data: recipientSuggestionsData, isLoading } = trpc.recipient.suggestions.find.useQuery( { query: debouncedRecipientSearchQuery, }, { enabled: debouncedRecipientSearchQuery.length > 1, }, ); const recipientSuggestions = recipientSuggestionsData?.results || []; const defaultRecipients = useMemo(() => { if (recipients && recipients.length > 0) { const sorted = [...recipients].sort((a, b) => { const orderA = a.signingOrder ?? 0; const orderB = b.signingOrder ?? 0; return orderA - orderB; }); return sorted.map((recipient) => ({ formId: nanoid(), name: recipient.name, email: recipient.email, role: recipient.role, })); } return [ { formId: nanoid(), name: '', email: '', role: RecipientRole.SIGNER, }, ]; }, [recipients]); const form = useForm({ resolver: zodResolver(ZSuggestedRecipientsFormSchema), defaultValues: { recipients: defaultRecipients, }, }); const { formState: { isSubmitting }, } = form; useEffect(() => { form.reset({ recipients: defaultRecipients, }); }, [defaultRecipients, form]); const { fields, append, remove } = useFieldArray({ control: form.control, name: 'recipients', }); const handleRecipientAutoCompleteSelect = ( index: number, suggestion: RecipientAutoCompleteOption, ) => { form.setValue(`recipients.${index}.email`, suggestion.email); form.setValue(`recipients.${index}.name`, suggestion.name ?? suggestion.email); }; const handleAddSigner = () => { append({ formId: nanoid(), name: '', email: '', role: RecipientRole.SIGNER, }); }; const handleRemoveSigner = (index: number) => { remove(index); }; const handleSubmit = form.handleSubmit(async (values) => { const normalizedRecipients: RecipientForCreation[] = values.recipients.map( (recipient, index) => ({ name: recipient.name.trim(), email: recipient.email.trim(), role: recipient.role, signingOrder: index + 1, }), ); try { await onSubmit(normalizedRecipients); } catch (error) { console.error('Failed to submit recipients:', error); } }); const handleAutoAddFields = form.handleSubmit(async (values) => { if (!onAutoAddFields) { return; } const normalizedRecipients: RecipientForCreation[] = values.recipients.map( (recipient, index) => ({ name: recipient.name.trim(), email: recipient.email.trim(), role: recipient.role, signingOrder: index + 1, }), ); try { await onAutoAddFields(normalizedRecipients); } catch (error) { console.error('Failed to auto-add fields:', error); } }); const getRecipientsRootError = ( error: typeof form.formState.errors.recipients, ): FieldError | undefined => { if (typeof error !== 'object' || !error || !('root' in error)) { return undefined; } const candidate = (error as { root?: FieldError }).root; return typeof candidate === 'object' ? candidate : undefined; }; const recipientsRootError = getRecipientsRootError(form.formState.errors.recipients); return (
{fields.map((field, index) => (
( {index === 0 && ( Email )} handleRecipientAutoCompleteSelect(index, suggestion) } onSearchQueryChange={(query) => { emailField.onChange(query); setRecipientSearchQuery(query); }} loading={isLoading} disabled={isSubmitting} maxLength={254} /> )} /> ( {index === 0 && ( Name )} handleRecipientAutoCompleteSelect(index, suggestion) } onSearchQueryChange={(query) => { nameField.onChange(query); setRecipientSearchQuery(query); }} loading={isLoading} disabled={isSubmitting} maxLength={255} /> )} /> ( {index === 0 && ( Role )} )} />
))}
{onAutoAddFields && (
{(fields.length === 0 || !form.formState.isValid) && ( Please add at least one valid recipient to auto-detect fields )}
)}
); };