import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { Recipient } from '@prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client'; import { InfoIcon, Plus, Upload, X } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import * as z from 'zod'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX, TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX, } from '@documenso/lib/constants/template'; import { AppError } from '@documenso/lib/errors/app-error'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import type { Toast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ZAddRecipientsForNewDocumentSchema = z .object({ distributeDocument: z.boolean(), useCustomDocument: z.boolean().default(false), customDocumentData: z .any() .refine((data) => data instanceof File || data === undefined) .optional(), recipients: z.array( z.object({ id: z.number(), email: z.string().email(), name: z.string(), signingOrder: z.number().optional(), }), ), }) // Display exactly which rows are duplicates. .superRefine((items, ctx) => { const uniqueEmails = new Map(); for (const [index, recipients] of items.recipients.entries()) { const email = recipients.email.toLowerCase(); const firstFoundIndex = uniqueEmails.get(email); if (firstFoundIndex === undefined) { uniqueEmails.set(email, index); continue; } ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Emails must be unique', path: ['recipients', index, 'email'], }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Emails must be unique', path: ['recipients', firstFoundIndex, 'email'], }); } }); type TAddRecipientsForNewDocumentSchema = z.infer; export type TemplateUseDialogProps = { templateId: number; templateSigningOrder?: DocumentSigningOrder | null; recipients: Recipient[]; documentDistributionMethod?: DocumentDistributionMethod; documentRootPath: string; trigger?: React.ReactNode; }; export function TemplateUseDialog({ recipients, documentDistributionMethod = DocumentDistributionMethod.EMAIL, documentRootPath, templateId, templateSigningOrder, trigger, }: TemplateUseDialogProps) { const { toast } = useToast(); const { _ } = useLingui(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const form = useForm({ resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), defaultValues: { distributeDocument: false, useCustomDocument: false, customDocumentData: undefined, recipients: recipients .sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0)) .map((recipient) => { const isRecipientEmailPlaceholder = recipient.email.match( TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX, ); const isRecipientNamePlaceholder = recipient.name.match( TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX, ); return { id: recipient.id, name: !isRecipientNamePlaceholder ? recipient.name : '', email: !isRecipientEmailPlaceholder ? recipient.email : '', signingOrder: recipient.signingOrder ?? undefined, }; }), }, }); const { mutateAsync: createDocumentFromTemplate } = trpc.template.createDocumentFromTemplate.useMutation(); const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { try { let customDocumentDataId: string | undefined = undefined; if (data.useCustomDocument && data.customDocumentData) { const customDocumentData = await putPdfFile(data.customDocumentData); customDocumentDataId = customDocumentData.id; } const { id } = await createDocumentFromTemplate({ templateId, recipients: data.recipients, distributeDocument: data.distributeDocument, customDocumentDataId, }); toast({ title: _(msg`Document created`), description: _(msg`Your document has been created from the template successfully.`), duration: 5000, }); let documentPath = `${documentRootPath}/${id}`; if ( data.distributeDocument && documentDistributionMethod === DocumentDistributionMethod.NONE ) { documentPath += '?action=view-signing-links'; } await navigate(documentPath); } catch (err) { const error = AppError.parseError(err); const toastPayload: Toast = { title: _(msg`Error`), description: _(msg`An error occurred while creating document from template.`), variant: 'destructive', }; if (error.code === 'DOCUMENT_SEND_FAILED') { toastPayload.description = _( msg`The document was created but could not be sent to recipients.`, ); } toast(toastPayload); } }; const { fields: formRecipients } = useFieldArray({ control: form.control, name: 'recipients', }); useEffect(() => { if (!open) { form.reset(); } }, [open, form]); return ( !form.formState.isSubmitting && setOpen(value)}> {trigger || ( )} Create document from template {recipients.length === 0 ? ( A draft document will be created ) : ( Add the recipients to create the document with )}
{formRecipients.map((recipient, index) => (
{templateSigningOrder === DocumentSigningOrder.SEQUENTIAL && ( ( )} /> )} ( {index === 0 && ( Email )} )} /> ( {index === 0 && ( Name )} )} />
))} {recipients.length > 0 && (
(
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && ( )} {documentDistributionMethod === DocumentDistributionMethod.NONE && ( )}
)} />
)} (
{ field.onChange(checked); if (!checked) { form.setValue('customDocumentData', undefined); } }} />
)} /> {form.watch('useCustomDocument') && (
(
)} />
)}
); }