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; aiPlacementKey?: number;
signers?: DocumentSigner[]; signers?: DocumentSigner[];
unassignedFieldIds?: Set<string>; 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 [fields, setFields] = useState<SignatureFieldData[]>([]);
const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null); const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null);
const [activeSignerEmail, setActiveSignerEmail] = useState<string | null>(null); const [activeSignerEmail, setActiveSignerEmail] = useState<string | null>(null);
@@ -221,7 +223,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
useEffect(() => { useEffect(() => {
async function loadFields() { async function loadFields() {
try { try {
const res = await fetch(`/api/documents/${docId}/fields`); const res = await fetch(fieldsUrl ?? `/api/documents/${docId}/fields`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setFields(data); setFields(data);
@@ -231,7 +233,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
} }
} }
loadFields(); loadFields();
}, [docId, aiPlacementKey]); }, [docId, aiPlacementKey, fieldsUrl]);
// Update containerSize whenever pageInfo changes (page load or zoom change) // Update containerSize whenever pageInfo changes (page load or zoom change)
// Use pageInfo.width/height (from react-pdf canvas) as the authoritative rendered size. // 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]; const next = [...fields, newField];
setFields(next); setFields(next);
persistFields(docId, next); if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.(); onFieldsChanged?.();
}, },
[fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail], [fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail, onPersist],
); );
// --- Move / Resize pointer handlers (event delegation on DroppableZone) --- // --- Move / Resize pointer handlers (event delegation on DroppableZone) ---
@@ -505,7 +507,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
}; };
}); });
setFields(next); setFields(next);
persistFields(docId, next); if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.(); onFieldsChanged?.();
} else if (drag.type === 'resize') { } else if (drag.type === 'resize') {
const corner = drag.corner ?? 'se'; 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 }; return { ...f, x: newPdfX, y: newPdfY, width: newPdfW, height: newPdfH };
}); });
setFields(next); setFields(next);
persistFields(docId, next); if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.(); onFieldsChanged?.();
} }
}, [docId, onFieldsChanged]); }, [docId, onFieldsChanged, onPersist]);
// Render placed fields for the current page // Render placed fields for the current page
// Uses pageInfo.width/height (not getBoundingClientRect) for consistent coordinate math // Uses pageInfo.width/height (not getBoundingClientRect) for consistent coordinate math
@@ -742,7 +744,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
e.stopPropagation(); e.stopPropagation();
const next = fields.filter((f) => f.id !== field.id); const next = fields.filter((f) => f.id !== field.id);
setFields(next); setFields(next);
persistFields(docId, next); if (onPersist) { onPersist(next); } else { persistFields(docId, next); }
onFieldsChanged?.(); onFieldsChanged?.();
}} }}
onPointerDown={(e) => { 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/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css'; import 'react-pdf/dist/Page/TextLayer.css';
import { FieldPlacer } from './FieldPlacer'; 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) // Worker setup — must use import.meta.url for local/Docker environments (no CDN)
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL(
@@ -31,6 +31,9 @@ export function PdfViewer({
aiPlacementKey, aiPlacementKey,
signers, signers,
unassignedFieldIds, unassignedFieldIds,
onPersist,
fieldsUrl,
fileUrl,
}: { }: {
docId: string; docId: string;
docStatus?: string; docStatus?: string;
@@ -42,6 +45,9 @@ export function PdfViewer({
aiPlacementKey?: number; aiPlacementKey?: number;
signers?: DocumentSigner[]; signers?: DocumentSigner[];
unassignedFieldIds?: Set<string>; unassignedFieldIds?: Set<string>;
onPersist?: (fields: SignatureFieldData[]) => Promise<void> | void;
fieldsUrl?: string;
fileUrl?: string;
}) { }) {
const [numPages, setNumPages] = useState(0); const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
@@ -82,7 +88,7 @@ export function PdfViewer({
</button> </button>
{docStatus !== 'Signed' && ( {docStatus !== 'Signed' && (
<a <a
href={`/api/documents/${docId}/file`} href={fileUrl ?? `/api/documents/${docId}/file`}
download download
className="px-3 py-1 border rounded hover:bg-gray-100" className="px-3 py-1 border rounded hover:bg-gray-100"
> >
@@ -105,9 +111,11 @@ export function PdfViewer({
aiPlacementKey={aiPlacementKey} aiPlacementKey={aiPlacementKey}
signers={signers} signers={signers}
unassignedFieldIds={unassignedFieldIds} unassignedFieldIds={unassignedFieldIds}
onPersist={onPersist}
fieldsUrl={fieldsUrl}
> >
<Document <Document
file={`/api/documents/${docId}/file`} file={fileUrl ?? `/api/documents/${docId}/file`}
onLoadSuccess={({ numPages }) => setNumPages(numPages)} onLoadSuccess={({ numPages }) => setNumPages(numPages)}
className="shadow-lg" className="shadow-lg"
> >

View File

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

View File

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