fix(pdf): stabilize AcroForm imports

This commit is contained in:
ephraimduncan
2026-05-27 00:16:03 +00:00
parent 824117d47e
commit 874243700f
8 changed files with 192 additions and 38 deletions
@@ -31,7 +31,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { FileTextIcon, FormInputIcon, PencilIcon, SparklesIcon } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRevalidator, useSearchParams } from 'react-router';
import { isDeepEqual } from 'remeda';
import { match } from 'ts-pattern';
@@ -78,7 +78,8 @@ export const EnvelopeEditorFieldsPage = () => {
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const { envelope, editorFields, navigateToStep, editorConfig } = useCurrentEnvelopeEditor();
const { envelope, editorFields, navigateToStep, editorConfig, flushAutosave, syncEnvelope } =
useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
@@ -86,12 +87,34 @@ export const EnvelopeEditorFieldsPage = () => {
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
const [acroFormHasFieldsByItemRevision, setAcroFormHasFieldsByItemRevision] = useState<Record<string, boolean>>({});
const { revalidate } = useRevalidator();
const { toast } = useToast();
const { mutateAsync: importFieldsFromPdf, isPending: isImportingFieldsFromPdf } =
trpc.envelope.field.importFromPdf.useMutation();
const currentEnvelopeItemRevision = currentEnvelopeItem
? `${currentEnvelopeItem.id}:${currentEnvelopeItem.documentDataId}`
: null;
const currentItemHasAcroForm = currentEnvelopeItemRevision
? acroFormHasFieldsByItemRevision[currentEnvelopeItemRevision] === true
: false;
const onAcroFormDetected = useCallback(
(hasFields: boolean) => {
if (!currentEnvelopeItemRevision) {
return;
}
setAcroFormHasFieldsByItemRevision((prev) =>
prev[currentEnvelopeItemRevision] === hasFields ? prev : { ...prev, [currentEnvelopeItemRevision]: hasFields },
);
},
[currentEnvelopeItemRevision],
);
const envelopeItemPermissions = useMemo(
() => getEnvelopeItemPermissions(envelope, envelope.recipients),
[envelope, envelope.recipients],
@@ -160,6 +183,7 @@ export const EnvelopeEditorFieldsPage = () => {
const onImportFromPdfClick = async () => {
try {
await flushAutosave();
const result = await importFieldsFromPdf({ envelopeId: envelope.id });
if (result.fieldsCreated === 0) {
@@ -172,7 +196,7 @@ export const EnvelopeEditorFieldsPage = () => {
return;
}
await revalidate();
await syncEnvelope();
toast({
title: _(msg`Fields imported`),
@@ -255,6 +279,7 @@ export const EnvelopeEditorFieldsPage = () => {
customPageRenderer={EnvelopeEditorFieldsPageRenderer}
scrollParentRef={scrollableContainerRef}
errorMessage={PDF_VIEWER_ERROR_MESSAGES.editor}
onAcroFormDetected={onAcroFormDetected}
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
@@ -313,23 +338,6 @@ export const EnvelopeEditorFieldsPage = () => {
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
/>
<Button
type="button"
variant="outline"
size="sm"
className="mt-4 w-full"
onClick={() => void onImportFromPdfClick()}
disabled={envelope.status !== DocumentStatus.DRAFT || isImportingFieldsFromPdf}
title={
envelope.status !== DocumentStatus.DRAFT
? _(msg`You can only import fields in draft envelopes`)
: undefined
}
>
<FormInputIcon className="mr-2 -ml-1 h-4 w-4" />
{isImportingFieldsFromPdf ? <Trans>Importing...</Trans> : <Trans>Import from PDF form</Trans>}
</Button>
{editorConfig.fields?.allowAIDetection && (
<>
<Button
@@ -364,6 +372,20 @@ export const EnvelopeEditorFieldsPage = () => {
/>
</>
)}
{currentItemHasAcroForm && envelope.status === DocumentStatus.DRAFT && (
<Button
type="button"
variant="outline"
size="sm"
className="mt-4 w-full"
onClick={() => void onImportFromPdfClick()}
disabled={isImportingFieldsFromPdf}
>
<FormInputIcon className="mr-2 -ml-1 h-4 w-4" />
{isImportingFieldsFromPdf ? <Trans>Importing...</Trans> : <Trans>Import from PDF form</Trans>}
</Button>
)}
</section>
{/* Field details section. */}
@@ -16,7 +16,12 @@ export type EnvelopePdfViewerProps = {
errorMessage: { title: MessageDescriptor; description: MessageDescriptor } | null;
} & Omit<PDFViewerProps, 'data'>;
export const EnvelopePdfViewer = ({ errorMessage, className, ...props }: EnvelopePdfViewerProps) => {
export const EnvelopePdfViewer = ({
errorMessage,
className,
onAcroFormDetected,
...props
}: EnvelopePdfViewerProps) => {
const { t } = useLingui();
const $el = useRef<HTMLDivElement>(null);
@@ -46,10 +51,11 @@ export const EnvelopePdfViewer = ({ errorMessage, className, ...props }: Envelop
return (
<PDFViewerLazy
key={`${currentEnvelopeItem.envelopeId}-${currentEnvelopeItem.id}`}
key={`${currentEnvelopeItem.envelopeId}-${currentEnvelopeItem.id}-${currentEnvelopeItem.documentDataId}`}
{...props}
className={cn('h-full w-full max-w-[800px]', className)}
data={currentEnvelopeItem.data}
onAcroFormDetected={onAcroFormDetected}
/>
);
};
@@ -50,6 +50,7 @@ export type PDFViewerProps = {
scrollParentRef: ScrollTarget;
onDocumentLoad?: () => void;
onAcroFormDetected?: (hasFields: boolean) => void;
/**
* Additional component to render next to the image, such as a Konva canvas
@@ -63,6 +64,7 @@ export default function PDFViewer({
data,
scrollParentRef,
onDocumentLoad,
onAcroFormDetected,
customPageRenderer,
...props
}: PDFViewerProps) {
@@ -124,6 +126,20 @@ export default function PDFViewer({
// eslint-disable-next-line require-atomic-updates
pdfRef.current = loadedPdf;
if (onAcroFormDetected) {
try {
const fieldObjects = await loadedPdf.getFieldObjects();
if (!isCancelled) {
onAcroFormDetected(fieldObjects !== null && Object.keys(fieldObjects).length > 0);
}
} catch {
if (!isCancelled) {
onAcroFormDetected(false);
}
}
}
// Fetch the pages
const pages = await pMap(Array.from({ length: loadedPdf.numPages }), async (_, pageIndex) => {
const page = await loadedPdf.getPage(pageIndex + 1);
@@ -168,7 +184,7 @@ export default function PDFViewer({
pdfRef.current = null;
}
};
}, [data]);
}, [data, onAcroFormDetected]);
// Notify when document is loaded
useEffect(() => {