import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { trpc } from '@documenso/trpc/react'; import { ZDocumentTitleSchema } from '@documenso/trpc/server/document-router/schema'; import type { TReplaceEnvelopeItemPdfPayload } from '@documenso/trpc/server/envelope-router/replace-envelope-item-pdf.types'; import { buildDropzoneRejectionDescription } from '@documenso/ui/lib/handle-dropzone-rejection'; import { cn } from '@documenso/ui/lib/utils'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; 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 { useToast } from '@documenso/ui/primitives/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; import { Plural, Trans, useLingui } from '@lingui/react/macro'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { AlertTriangleIcon, FileIcon, UploadIcon, XIcon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { type FileRejection, useDropzone } from 'react-dropzone'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; const ZEditEnvelopeItemFormSchema = z.object({ title: ZDocumentTitleSchema, }); type TEditEnvelopeItemFormSchema = z.infer; /** * Note: This should only be visible if the envelope item is editable. */ export type EnvelopeItemEditDialogProps = { envelopeItem: { id: string; title: string }; allowConfigureTitle: boolean; trigger: React.ReactNode; } & Omit; export const EnvelopeItemEditDialog = ({ envelopeItem, allowConfigureTitle, trigger, ...props }: EnvelopeItemEditDialogProps) => { const { t, i18n } = useLingui(); const { toast } = useToast(); const { envelope, editorFields, setLocalEnvelope, isEmbedded } = useCurrentEnvelopeEditor(); const [isOpen, setIsOpen] = useState(false); const [replacementFile, setReplacementFile] = useState<{ file: File; pageCount: number } | null>(null); const [isDropping, setIsDropping] = useState(false); const form = useForm({ resolver: zodResolver(ZEditEnvelopeItemFormSchema), defaultValues: { title: envelopeItem.title, }, }); const { mutateAsync: replaceEnvelopeItemPdf } = trpc.envelope.item.replacePdf.useMutation({ onSuccess: ({ data, fields }) => { setLocalEnvelope({ envelopeItems: envelope.envelopeItems.map((item) => item.id === data.id ? { ...item, documentDataId: data.documentDataId, title: data.title } : item, ), }); if (fields) { setLocalEnvelope({ fields }); editorFields.resetForm(fields); } }, }); const fieldsOnExcessPages = replacementFile !== null ? envelope.fields.filter( (field) => field.envelopeItemId === envelopeItem.id && field.page > replacementFile.pageCount, ) : []; const onFileDropRejected = (fileRejections: FileRejection[]) => { toast({ title: t`Upload failed`, description: i18n._(buildDropzoneRejectionDescription(fileRejections)), duration: 5000, variant: 'destructive', }); }; const onFileDrop = async (files: File[]) => { const file = files[0]; if (!file || isDropping) { return; } setIsDropping(true); try { const arrayBuffer = await file.arrayBuffer(); const fileData = new Uint8Array(arrayBuffer.slice(0)); const { PDF } = await import('@libpdf/core'); const pdfDoc = await PDF.load(fileData); setReplacementFile({ file, pageCount: pdfDoc.getPageCount(), }); } catch (err) { console.error(err); toast({ title: t`Failed to read file`, description: t`The file is not a valid PDF.`, variant: 'destructive', }); } setIsDropping(false); }; const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { 'application/pdf': ['.pdf'] }, maxFiles: 1, maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), disabled: form.formState.isSubmitting, onDrop: (files) => void onFileDrop(files), onDropRejected: onFileDropRejected, }); const onSubmit = async (data: TEditEnvelopeItemFormSchema) => { if (isDropping || !replacementFile) { return; } try { const { file, pageCount } = replacementFile; if (isEmbedded) { const arrayBuffer = await file.arrayBuffer(); const fileData = new Uint8Array(arrayBuffer.slice(0)); const remainingFields = envelope.fields.filter( (field) => field.envelopeItemId !== envelopeItem.id || field.page <= pageCount, ); setLocalEnvelope({ envelopeItems: envelope.envelopeItems.map((item) => item.id === envelopeItem.id ? { ...item, title: data.title, data: fileData } : item, ), fields: remainingFields, }); editorFields.resetForm(remainingFields); } else { const payload = { envelopeId: envelope.id, envelopeItemId: envelopeItem.id, title: data.title, } satisfies TReplaceEnvelopeItemPdfPayload; const formData = new FormData(); formData.append('payload', JSON.stringify(payload)); formData.append('file', file); await replaceEnvelopeItemPdf(formData); } setIsOpen(false); } catch { toast({ title: t`Failed to update item`, description: t`Something went wrong while updating the envelope item.`, variant: 'destructive', }); } }; useEffect(() => { if (!isOpen) { form.reset({ title: envelopeItem.title }); setReplacementFile(null); setIsDropping(false); } }, [isOpen, form, envelopeItem.title]); const formatFileSize = (bytes: number) => { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return ( !form.formState.isSubmitting && setIsOpen(value)}> e.stopPropagation()} asChild> {trigger} Edit Item Update the title or replace the PDF file.
( Document Title )} />
Replace PDF {replacementFile ? (

{replacementFile.file.name}

{formatFileSize(replacementFile.file.size)} {isDropping ? ' · …' : ' · '} {!isDropping && replacementFile.pageCount !== null && ( )}

{fieldsOnExcessPages.length > 0 && ( )}
) : (
Drop PDF here or click to select
)}
); };