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 { FileTextIcon, InfoIcon, Plus, UploadCloudIcon, 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 { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META, } from '@documenso/lib/constants/trpc'; 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 { SpinnerBox } from '@documenso/ui/primitives/spinner'; 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 .array( z.object({ title: z.string(), data: z.instanceof(File).optional(), envelopeItemId: z.string(), }), ) .optional(), recipients: z.array( z.object({ id: z.number(), email: z.string().email(), name: z.string(), signingOrder: z.number().optional(), }), ), }); type TAddRecipientsForNewDocumentSchema = z.infer; export type TemplateUseDialogProps = { envelopeId: string; templateId: number; templateSigningOrder?: DocumentSigningOrder | null; recipients: Recipient[]; documentDistributionMethod?: DocumentDistributionMethod; documentRootPath: string; trigger?: React.ReactNode; }; export function TemplateUseDialog({ recipients, documentDistributionMethod = DocumentDistributionMethod.EMAIL, documentRootPath, envelopeId, 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: [], 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 { replace, fields: localCustomDocumentData } = useFieldArray({ control: form.control, name: 'customDocumentData', }); const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery( { envelopeId, }, { placeholderData: (previousData) => previousData, ...SKIP_QUERY_BATCH_META, ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, }, ); const envelopeItems = response?.data ?? []; const { mutateAsync: createDocumentFromTemplate } = trpc.template.createDocumentFromTemplate.useMutation(); const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { try { const customFilesToUpload = (data.customDocumentData || []).filter( (item): item is { data: File; envelopeItemId: string; title: string } => item.data !== undefined && item.envelopeItemId !== undefined && item.title !== undefined, ); const customDocumentData = await Promise.all( customFilesToUpload.map(async (item) => { const customDocumentData = await putPdfFile(item.data); return { documentDataId: customDocumentData.id, envelopeItemId: item.envelopeItemId, }; }), ); const { envelopeId } = await createDocumentFromTemplate({ templateId, recipients: data.recipients, distributeDocument: data.distributeDocument, customDocumentData, }); toast({ title: _(msg`Document created`), description: _(msg`Your document has been created from the template successfully.`), duration: 5000, }); let documentPath = `${documentRootPath}/${envelopeId}`; 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]); useEffect(() => { if (envelopeItems.length > 0 && localCustomDocumentData.length === 0) { replace( envelopeItems.map((item) => ({ title: item.title, data: undefined, envelopeItemId: item.id, })), ); } }, [envelopeItems, form, open]); 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') && (
{isLoadingEnvelopeItems ? ( ) : ( localCustomDocumentData.map((item, i) => ( (

{item.title}

{field.value ? (

Custom {(field.value.size / (1024 * 1024)).toFixed(2)}{' '} MB file
) : ( Default file )}

{field.value ? (
) : ( )} { const file = e.target.files?.[0]; if (!file) { field.onChange(undefined); return; } if (file.type !== 'application/pdf') { form.setError('customDocumentData', { type: 'manual', message: _(msg`Please select a PDF file`), }); return; } if ( file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024 ) { form.setError('customDocumentData', { type: 'manual', message: _( msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`, ), }); return; } field.onChange(file); }} />
)} /> )) )}
)}
); }