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:
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user