From 824117d47e0d0ee712605cdc87381e986ca85fa5 Mon Sep 17 00:00:00 2001 From: ephraimduncan Date: Fri, 22 May 2026 18:48:08 +0000 Subject: [PATCH] refactor(pdf): move AcroForm import from upload to editor button Per-product direction: AcroForm widget to Documenso field creation should not happen automatically on upload. It must be a deliberate, opt-in action on a draft envelope. - Revert AcroForm extraction from create-envelope (route) and create-envelope-items upload paths. They no longer thread acroFormFields into envelope items or run the extractor. - Stop flattening on upload (flattenForm: false) so widgets survive in the stored PDF until the user opts in. - New tRPC mutation envelope.field.importFromPdf is the single entry point. It loads each item's stored PDF, extracts widgets, creates Field rows assigned to the first signable recipient (creating a placeholder Recipient 1 SIGNER when none exist), flattens the PDF in place, swaps documentDataId, and emits FIELD_CREATED audit log entries on DOCUMENT envelopes. - Editor fields panel gains an "Import from PDF form" button next to "Detect with AI", gated to DRAFT envelopes. Success toasts the count and revalidates the editor. - Rewrite acroform-import.spec.ts e2e to the new flow: upload preserves widgets and creates zero fields; service call creates fields, flattens PDF, audits, and cleans up old DocumentData. - Invert four DOCUMENT-upload assertions in form-flattening.spec.ts to match the new preserve-widgets, no-auto-flatten contract. Template and template-to-doc flatten behavior is unchanged. --- .../envelope-editor-fields-page.tsx | 58 ++- .../e2e/scenarios/acroform-import.spec.ts | 227 ++++++++---- .../e2e/scenarios/form-flattening.spec.ts | 34 +- .../envelope-item/create-envelope-items.ts | 130 +------ .../envelope-item/import-acroform-fields.ts | 337 ++++++++++++++++++ .../server-only/envelope/create-envelope.ts | 138 ------- .../server/envelope-router/create-envelope.ts | 59 +-- .../envelope-router/import-acroform-fields.ts | 76 ++++ .../import-acroform-fields.types.ts | 24 ++ .../trpc/server/envelope-router/router.ts | 2 + 10 files changed, 686 insertions(+), 399 deletions(-) create mode 100644 packages/lib/server-only/envelope-item/import-acroform-fields.ts create mode 100644 packages/trpc/server/envelope-router/import-acroform-fields.ts create mode 100644 packages/trpc/server/envelope-router/import-acroform-fields.types.ts diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx index a5ec4ed16..9d6087061 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx @@ -18,17 +18,19 @@ import { } from '@documenso/lib/types/field-meta'; import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; +import { trpc } from '@documenso/trpc/react'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { cn } from '@documenso/ui/lib/utils'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Separator } from '@documenso/ui/primitives/separator'; +import { useToast } from '@documenso/ui/primitives/use-toast'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client'; -import { FileTextIcon, PencilIcon, SparklesIcon } from 'lucide-react'; +import { FileTextIcon, FormInputIcon, PencilIcon, SparklesIcon } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useRevalidator, useSearchParams } from 'react-router'; import { isDeepEqual } from 'remeda'; @@ -85,6 +87,10 @@ export const EnvelopeEditorFieldsPage = () => { const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false); const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false); const { revalidate } = useRevalidator(); + const { toast } = useToast(); + + const { mutateAsync: importFieldsFromPdf, isPending: isImportingFieldsFromPdf } = + trpc.envelope.field.importFromPdf.useMutation(); const envelopeItemPermissions = useMemo( () => getEnvelopeItemPermissions(envelope, envelope.recipients), @@ -152,6 +158,39 @@ export const EnvelopeEditorFieldsPage = () => { }); }; + const onImportFromPdfClick = async () => { + try { + const result = await importFieldsFromPdf({ envelopeId: envelope.id }); + + if (result.fieldsCreated === 0) { + toast({ + title: _(msg`No form fields found`), + description: _(msg`This PDF does not contain any importable form fields.`), + duration: 5000, + }); + + return; + } + + await revalidate(); + + toast({ + title: _(msg`Fields imported`), + description: _( + msg`Imported ${result.fieldsCreated} field${result.fieldsCreated === 1 ? '' : 's'} from the PDF form. Review and reassign in the editor.`, + ), + duration: 5000, + }); + } catch { + toast({ + title: _(msg`Could not import fields`), + description: _(msg`Something went wrong while importing fields from the PDF.`), + variant: 'destructive', + duration: 5000, + }); + } + }; + return (
@@ -274,6 +313,23 @@ export const EnvelopeEditorFieldsPage = () => { selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null} /> + + {editorConfig.fields?.allowAIDetection && ( <>