feat(19-01): add onPersist, fieldsUrl, fileUrl props and Templates nav link

- FieldPlacer: add onPersist and fieldsUrl optional props (backwards-compatible)
- FieldPlacer: 4 persistFields call sites now conditionally use onPersist when provided
- FieldPlacer: loadFields useEffect uses fieldsUrl ?? default documents endpoint
- PdfViewer: add onPersist, fieldsUrl, fileUrl props; pass to FieldPlacer; fileUrl ?? default for PDF source
- PdfViewerWrapper: add and pass through onPersist, fieldsUrl, fileUrl to PdfViewer
- PortalNav: insert Templates link between Clients and Profile
This commit is contained in:
Chandler Copeland
2026-04-06 13:09:19 -06:00
parent 8bc3b2dfbe
commit 57efd91fa2
4 changed files with 33 additions and 13 deletions

View File

@@ -168,9 +168,11 @@ interface FieldPlacerProps {
aiPlacementKey?: number;
signers?: DocumentSigner[];
unassignedFieldIds?: Set<string>;
onPersist?: (fields: SignatureFieldData[]) => Promise<void> | void;
fieldsUrl?: string;
}
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange, aiPlacementKey = 0, signers = [], unassignedFieldIds = new Set() }: FieldPlacerProps) {
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange, aiPlacementKey = 0, signers = [], unassignedFieldIds = new Set(), onPersist, fieldsUrl }: FieldPlacerProps) {
const [fields, setFields] = useState<SignatureFieldData[]>([]);
const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null);
const [activeSignerEmail, setActiveSignerEmail] = useState<string | null>(null);
@@ -221,7 +223,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
useEffect(() => {
async function loadFields() {
try {
const res = await fetch(`/api/documents/${docId}/fields`);
const res = await fetch(fieldsUrl ?? `/api/documents/${docId}/fields`);
if (res.ok) {
const data = await res.json();
setFields(data);
@@ -231,7 +233,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
}
}
loadFields();
}, [docId, aiPlacementKey]);
}, [docId, aiPlacementKey, fieldsUrl]);
// Update containerSize whenever pageInfo changes (page load or zoom change)
// Use pageInfo.width/height (from react-pdf canvas) as the authoritative rendered size.
@@ -321,10 +323,10 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
const next = [...fields, newField];
setFields(next);
persistFields(docId, next);
if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.();
},
[fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail],
[fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail, onPersist],
);
// --- Move / Resize pointer handlers (event delegation on DroppableZone) ---
@@ -505,7 +507,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
};
});
setFields(next);
persistFields(docId, next);
if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.();
} else if (drag.type === 'resize') {
const corner = drag.corner ?? 'se';
@@ -572,10 +574,10 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
return { ...f, x: newPdfX, y: newPdfY, width: newPdfW, height: newPdfH };
});
setFields(next);
persistFields(docId, next);
if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.();
}
}, [docId, onFieldsChanged]);
}, [docId, onFieldsChanged, onPersist]);
// Render placed fields for the current page
// Uses pageInfo.width/height (not getBoundingClientRect) for consistent coordinate math
@@ -742,7 +744,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
e.stopPropagation();
const next = fields.filter((f) => f.id !== field.id);
setFields(next);
persistFields(docId, next);
if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.();
}}
onPointerDown={(e) => {

View File

@@ -4,7 +4,7 @@ import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import { FieldPlacer } from './FieldPlacer';
import type { DocumentSigner } from '@/lib/db/schema';
import type { DocumentSigner, SignatureFieldData } from '@/lib/db/schema';
// Worker setup — must use import.meta.url for local/Docker environments (no CDN)
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
@@ -31,6 +31,9 @@ export function PdfViewer({
aiPlacementKey,
signers,
unassignedFieldIds,
onPersist,
fieldsUrl,
fileUrl,
}: {
docId: string;
docStatus?: string;
@@ -42,6 +45,9 @@ export function PdfViewer({
aiPlacementKey?: number;
signers?: DocumentSigner[];
unassignedFieldIds?: Set<string>;
onPersist?: (fields: SignatureFieldData[]) => Promise<void> | void;
fieldsUrl?: string;
fileUrl?: string;
}) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
@@ -82,7 +88,7 @@ export function PdfViewer({
</button>
{docStatus !== 'Signed' && (
<a
href={`/api/documents/${docId}/file`}
href={fileUrl ?? `/api/documents/${docId}/file`}
download
className="px-3 py-1 border rounded hover:bg-gray-100"
>
@@ -105,9 +111,11 @@ export function PdfViewer({
aiPlacementKey={aiPlacementKey}
signers={signers}
unassignedFieldIds={unassignedFieldIds}
onPersist={onPersist}
fieldsUrl={fieldsUrl}
>
<Document
file={`/api/documents/${docId}/file`}
file={fileUrl ?? `/api/documents/${docId}/file`}
onLoadSuccess={({ numPages }) => setNumPages(numPages)}
className="shadow-lg"
>

View File

@@ -1,6 +1,6 @@
'use client';
import dynamic from 'next/dynamic';
import type { DocumentSigner } from '@/lib/db/schema';
import type { DocumentSigner, SignatureFieldData } from '@/lib/db/schema';
const PdfViewer = dynamic(() => import('./PdfViewer').then(m => m.PdfViewer), { ssr: false });
@@ -15,6 +15,9 @@ export function PdfViewerWrapper({
aiPlacementKey,
signers,
unassignedFieldIds,
onPersist,
fieldsUrl,
fileUrl,
}: {
docId: string;
docStatus?: string;
@@ -26,6 +29,9 @@ export function PdfViewerWrapper({
aiPlacementKey?: number;
signers?: DocumentSigner[];
unassignedFieldIds?: Set<string>;
onPersist?: (fields: SignatureFieldData[]) => Promise<void> | void;
fieldsUrl?: string;
fileUrl?: string;
}) {
return (
<PdfViewer
@@ -39,6 +45,9 @@ export function PdfViewerWrapper({
aiPlacementKey={aiPlacementKey}
signers={signers}
unassignedFieldIds={unassignedFieldIds}
onPersist={onPersist}
fieldsUrl={fieldsUrl}
fileUrl={fileUrl}
/>
);
}

View File

@@ -7,6 +7,7 @@ import { LogoutButton } from "@/components/ui/LogoutButton";
const navLinks = [
{ href: "/portal/dashboard", label: "Dashboard" },
{ href: "/portal/clients", label: "Clients" },
{ href: "/portal/templates", label: "Templates" },
{ href: "/portal/profile", label: "Profile" },
];