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