Add frontend autoformatting and set CI to require formatted code for all languages (#6052)

# Description of Changes
Changes the strategy for autoformatting to reject PRs if they are not
formatted correctly instead of allowing them to merge and then spawning
a new PR to fix the formatting. The old strategy just caused more work
for us because we'd have to manually approve the followup PR and get it
merged, which required 2 reviewers so in practice it rarely got done and
just meant everyone's PRs ended up containing reformatting for unrelated
files, which makes code review unnecessarily difficult. If the PR's code
is not formatted correctly after this PR, a comment will be added
automatically to tell the author how to run the formatter script to fix
their code so it can go in.

This also enables autoformatting for the frontend code, using Prettier.
I've enabled it for pretty much everything in the frontend folder, other
than 3rd party files and files it doesn't make sense for. I also
excluded Markdown because it sounds likely to be more annoying to have
to autoformat the Markdown in the frontend folder but nowhere else. Open
to changing this though if people disagree.

> [!note]
> 
> Advice to reviewers: The first commit contains all of the actual logic
I've introduced (CI changes, Prettier config, etc.)
> The second commit is just the reformatting of the entire frontend
folder.
> The first commit needs proper review, the second one just give it a
spot-check that it's doing what you'd expect.
This commit is contained in:
James Brunton
2026-04-10 17:41:19 +01:00
committed by GitHub
parent 33b2b5827a
commit a3e45bc182
1359 changed files with 57784 additions and 57460 deletions
@@ -1,55 +1,58 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Box, Center, Text, ActionIcon } from '@mantine/core';
import CloseIcon from '@mui/icons-material/Close';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Box, Center, Text, ActionIcon } from "@mantine/core";
import CloseIcon from "@mui/icons-material/Close";
import { useFileState, useFileActions } from "@app/contexts/FileContext";
import { useFileWithUrl } from "@app/hooks/useFileWithUrl";
import { useViewer } from "@app/contexts/ViewerContext";
import { LocalEmbedPDF } from '@app/components/viewer/LocalEmbedPDF';
import { PdfViewerToolbar } from '@app/components/viewer/PdfViewerToolbar';
import { ThumbnailSidebar } from '@app/components/viewer/ThumbnailSidebar';
import { BookmarkSidebar } from '@app/components/viewer/BookmarkSidebar';
import { AttachmentSidebar } from '@app/components/viewer/AttachmentSidebar';
import { LayerSidebar } from '@app/components/viewer/LayerSidebar';
import { useNavigationGuard, useNavigationState } from '@app/contexts/NavigationContext';
import { useSignature } from '@app/contexts/SignatureContext';
import { useRedaction } from '@app/contexts/RedactionContext';
import type { RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker';
import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers';
import { isStirlingFile, getFormFillFileId } from '@app/types/fileContext';
import { useViewerRightRailButtons } from '@app/components/viewer/useViewerRightRailButtons';
import { StampPlacementOverlay } from '@app/components/viewer/StampPlacementOverlay';
import { RulerOverlay, type PageMeasureScales, type PageScaleInfo, type ViewportScale } from '@app/components/viewer/RulerOverlay';
import type { PDFDict, PDFNumber } from '@cantoo/pdf-lib';
import { useWheelZoom } from '@app/hooks/useWheelZoom';
import { useFormFill } from '@app/tools/formFill/FormFillContext';
import { FormSaveBar } from '@app/tools/formFill/FormSaveBar';
import { useViewerKeyCommand } from '@app/hooks/useViewerKeyCommand';
import { LocalEmbedPDF } from "@app/components/viewer/LocalEmbedPDF";
import { PdfViewerToolbar } from "@app/components/viewer/PdfViewerToolbar";
import { ThumbnailSidebar } from "@app/components/viewer/ThumbnailSidebar";
import { BookmarkSidebar } from "@app/components/viewer/BookmarkSidebar";
import { AttachmentSidebar } from "@app/components/viewer/AttachmentSidebar";
import { LayerSidebar } from "@app/components/viewer/LayerSidebar";
import { useNavigationGuard, useNavigationState } from "@app/contexts/NavigationContext";
import { useSignature } from "@app/contexts/SignatureContext";
import { useRedaction } from "@app/contexts/RedactionContext";
import type { RedactionPendingTrackerAPI } from "@app/components/viewer/RedactionPendingTracker";
import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers";
import { isStirlingFile, getFormFillFileId } from "@app/types/fileContext";
import { useViewerRightRailButtons } from "@app/components/viewer/useViewerRightRailButtons";
import { StampPlacementOverlay } from "@app/components/viewer/StampPlacementOverlay";
import {
RulerOverlay,
type PageMeasureScales,
type PageScaleInfo,
type ViewportScale,
} from "@app/components/viewer/RulerOverlay";
import type { PDFDict, PDFNumber } from "@cantoo/pdf-lib";
import { useWheelZoom } from "@app/hooks/useWheelZoom";
import { useFormFill } from "@app/tools/formFill/FormFillContext";
import { FormSaveBar } from "@app/tools/formFill/FormSaveBar";
import { useViewerKeyCommand } from "@app/hooks/useViewerKeyCommand";
// ─── Measure dictionary extraction ────────────────────────────────────────────
async function extractPageMeasureScales(file: Blob): Promise<PageMeasureScales | null> {
try {
const { PDFDocument, PDFDict, PDFName, PDFArray, PDFNumber, PDFString, PDFHexString } = await import('@cantoo/pdf-lib');
const { PDFDocument, PDFDict, PDFName, PDFArray, PDFNumber, PDFString, PDFHexString } = await import("@cantoo/pdf-lib");
const pdfDoc = await PDFDocument.load(await file.arrayBuffer(), { ignoreEncryption: true });
// Parse a Measure dict into a MeasureScale, or return null if malformed.
const parseScale = (measureObj: unknown) => {
if (!(measureObj instanceof PDFDict)) return null;
const rObj = measureObj.lookup(PDFName.of('R'));
const ratioLabel = (rObj instanceof PDFString || rObj instanceof PDFHexString)
? rObj.decodeText() : '';
const rObj = measureObj.lookup(PDFName.of("R"));
const ratioLabel = rObj instanceof PDFString || rObj instanceof PDFHexString ? rObj.decodeText() : "";
// D = distance array, X = x-axis fallback
let fmtArray = measureObj.lookup(PDFName.of('D'));
if (!(fmtArray instanceof PDFArray)) fmtArray = measureObj.lookup(PDFName.of('X'));
let fmtArray = measureObj.lookup(PDFName.of("D"));
if (!(fmtArray instanceof PDFArray)) fmtArray = measureObj.lookup(PDFName.of("X"));
if (!(fmtArray instanceof PDFArray)) return null;
const firstFmt = fmtArray.lookup(0);
if (!(firstFmt instanceof PDFDict)) return null;
const cObj = firstFmt.lookup(PDFName.of('C'));
const uObj = firstFmt.lookup(PDFName.of('U'));
const cObj = firstFmt.lookup(PDFName.of("C"));
const uObj = firstFmt.lookup(PDFName.of("U"));
if (!(cObj instanceof PDFNumber) || cObj.asNumber() <= 0) return null;
const unit = (uObj instanceof PDFString || uObj instanceof PDFHexString)
? uObj.decodeText() : 'units';
const unit = uObj instanceof PDFString || uObj instanceof PDFHexString ? uObj.decodeText() : "units";
return { factor: cObj.asNumber(), unit, ratioLabel };
};
@@ -62,15 +65,15 @@ async function extractPageMeasureScales(file: Blob): Promise<PageMeasureScales |
const viewports: ViewportScale[] = [];
// Spec-conformant: /VP array — each viewport can have its own scale and BBox
const vpObj = pageNode.lookup(PDFName.of('VP'));
const vpObj = pageNode.lookup(PDFName.of("VP"));
if (vpObj instanceof PDFArray) {
for (let j = 0; j < vpObj.size(); j++) {
const vpEntry = vpObj.lookup(j);
if (!(vpEntry instanceof PDFDict)) continue;
const scale = parseScale(vpEntry.lookup(PDFName.of('Measure')));
const scale = parseScale(vpEntry.lookup(PDFName.of("Measure")));
if (!scale) continue;
let bbox: ViewportScale['bbox'] = null;
const bboxObj = vpEntry.lookup(PDFName.of('BBox'));
let bbox: ViewportScale["bbox"] = null;
const bboxObj = vpEntry.lookup(PDFName.of("BBox"));
if (bboxObj instanceof PDFArray && bboxObj.size() >= 4) {
bbox = [
(bboxObj.lookup(0) as PDFNumber).asNumber(),
@@ -85,7 +88,7 @@ async function extractPageMeasureScales(file: Blob): Promise<PageMeasureScales |
// Fallback: /Measure directly on page (non-conforming but seen in the wild)
if (viewports.length === 0) {
const scale = parseScale(pageNode.lookup(PDFName.of('Measure')));
const scale = parseScale(pageNode.lookup(PDFName.of("Measure")));
if (scale) viewports.push({ bbox: null, scale });
}
@@ -191,29 +194,36 @@ const EmbedPdfViewerContent = ({
const activeFiles = selectors.getFiles();
const activeFilesRef = useRef(activeFiles);
activeFilesRef.current = activeFiles;
const activeFileIds = activeFiles.map(f => f.fileId);
const activeFileIds = activeFiles.map((f) => f.fileId);
// Navigation guard for unsaved changes
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard();
const {
setHasUnsavedChanges,
registerUnsavedChangesChecker,
unregisterUnsavedChangesChecker,
registerNavigationWarningHandlers,
unregisterNavigationWarningHandlers,
} = useNavigationGuard();
const { selectedTool } = useNavigationState();
// Form fill context
const { fetchFields: fetchFormFields, setProviderMode } = useFormFill();
const isInAnnotationTool = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage' || selectedTool === 'annotate';
const isInAnnotationTool =
selectedTool === "sign" || selectedTool === "addText" || selectedTool === "addImage" || selectedTool === "annotate";
const isSignatureMode = isInAnnotationTool;
const isManualRedactMode = selectedTool === 'redact';
const isManualRedactMode = selectedTool === "redact";
// Enable annotations when annotation tool is selected OR when annotations are visible
// (so users can interact with existing comment annotations in reader/viewer mode)
const shouldEnableAnnotations = selectedTool === 'annotate' || isSignatureMode || isAnnotationsVisible;
const shouldEnableAnnotations = selectedTool === "annotate" || isSignatureMode || isAnnotationsVisible;
// Enable redaction only when redaction tool is selected
const shouldEnableRedaction = selectedTool === 'redact';
const shouldEnableRedaction = selectedTool === "redact";
// FormFill tool mode — uses PDFBox backend for full-fidelity form handling
const isFormFillToolActive = (selectedTool as string) === 'formFill';
const isFormFillToolActive = (selectedTool as string) === "formFill";
// Form overlays are shown in BOTH modes:
// - Normal viewer: form overlays visible (PDFium WASM, frontend-only)
@@ -222,7 +232,7 @@ const EmbedPdfViewerContent = ({
// Switch the provider when the tool mode changes
useEffect(() => {
setProviderMode(isFormFillToolActive ? 'pdfbox' : 'pdflib');
setProviderMode(isFormFillToolActive ? "pdfbox" : "pdflib");
}, [isFormFillToolActive, setProviderMode]);
// Track previous annotation/redaction state to detect tool switches
@@ -268,9 +278,7 @@ const EmbedPdfViewerContent = ({
setAnnotationMode(true);
}
}, [isInAnnotationTool, setAnnotationMode]);
const isPlacementOverlayActive = Boolean(
isInAnnotationTool && isPlacementMode && signatureConfig
);
const isPlacementOverlayActive = Boolean(isInAnnotationTool && isPlacementMode && signatureConfig);
// Track which file tab is active
const [internalActiveFileIndex, setInternalActiveFileIndex] = useState(0);
@@ -284,12 +292,12 @@ const EmbedPdfViewerContent = ({
// Stable string key representing the current file list order.
// Using a joined ID string avoids depending on the activeFiles array reference,
// which is a new object every render and would cause an infinite effect loop.
const fileIdsKey = activeFiles.map(f => f.fileId).join(',');
const fileIdsKey = activeFiles.map((f) => f.fileId).join(",");
// When the file list actually changes, re-derive activeFileIndex from the stable activeFileId.
useEffect(() => {
if (!activeFileId || activeFiles.length === 0) return;
const newIndex = activeFiles.findIndex(f => f.fileId === activeFileId);
const newIndex = activeFiles.findIndex((f) => f.fileId === activeFileId);
if (newIndex !== -1 && newIndex !== activeFileIndex) {
setActiveFileIndex(newIndex);
}
@@ -348,7 +356,7 @@ const EmbedPdfViewerContent = ({
}
if (previewFile) {
const uniquePreviewId = `${previewFile.name}-${previewFile.size}-${previewFile.lastModified ?? 'na'}`;
const uniquePreviewId = `${previewFile.name}-${previewFile.size}-${previewFile.lastModified ?? "na"}`;
return `preview-${uniquePreviewId}`;
}
@@ -358,7 +366,7 @@ const EmbedPdfViewerContent = ({
if (effectiveFile?.file instanceof File) {
const fileObj = effectiveFile.file;
return `file-${fileObj.name}-${fileObj.size}-${fileObj.lastModified ?? 'na'}`;
return `file-${fileObj.name}-${fileObj.size}-${fileObj.lastModified ?? "na"}`;
}
return undefined;
@@ -370,12 +378,14 @@ const EmbedPdfViewerContent = ({
return [bookmarkCacheKey].filter(Boolean) as string[];
}
return activeFiles.map(file => {
if (isStirlingFile(file)) {
return file.fileId;
}
return undefined;
}).filter(Boolean) as string[];
return activeFiles
.map((file) => {
if (isStirlingFile(file)) {
return file.fileId;
}
return undefined;
})
.filter(Boolean) as string[];
}, [activeFiles, previewFile, bookmarkCacheKey]);
useWheelZoom({
@@ -400,16 +410,14 @@ const EmbedPdfViewerContent = ({
if (mod) {
const target = event.target as Element;
const isInTextInput =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
(target as HTMLElement).isContentEditable;
target.tagName === "INPUT" || target.tagName === "TEXTAREA" || (target as HTMLElement).isContentEditable;
if (!isInTextInput) {
const wasOverridden = viewerKeyCommand(event)
if (!wasOverridden){
const wasOverridden = viewerKeyCommand(event);
if (!wasOverridden) {
switch (event.key) {
case 'p':
case 'P':
case "p":
case "P":
event.preventDefault();
printActions.print();
return;
@@ -426,37 +434,37 @@ const EmbedPdfViewerContent = ({
// Modifier key shortcuts (Ctrl/Cmd + key)
if (mod) {
switch (event.key) {
case '=':
case '+':
case "=":
case "+":
event.preventDefault();
zoomActions.zoomIn();
return;
case '-':
case '_':
case "-":
case "_":
event.preventDefault();
zoomActions.zoomOut();
return;
case '0':
case "0":
// Ctrl+0: Reset zoom to fit width
event.preventDefault();
zoomActions.requestZoom('fit-width');
zoomActions.requestZoom("fit-width");
return;
case 'a':
case 'A':
case "a":
case "A":
// Ctrl+A: Prevent browser from selecting all UI text
event.preventDefault();
return;
case 'f':
case 'F':
case "f":
case "F":
event.preventDefault();
if (isSearchInterfaceVisible) {
window.dispatchEvent(new CustomEvent('refocus-search-input'));
window.dispatchEvent(new CustomEvent("refocus-search-input"));
} else {
searchInterfaceActions.open();
}
return;
case 's':
case 'S':
case "s":
case "S":
// Ctrl+S: Save/apply changes
if (!event.shiftKey) {
event.preventDefault();
@@ -466,8 +474,8 @@ const EmbedPdfViewerContent = ({
}
return;
case 'z':
case 'Z':
case "z":
case "Z":
// Ctrl+Z: Undo; Ctrl+Shift+Z: Redo
event.preventDefault();
if (event.shiftKey) {
@@ -476,8 +484,8 @@ const EmbedPdfViewerContent = ({
historyApiRef.current?.undo?.();
}
return;
case 'y':
case 'Y':
case "y":
case "Y":
// Ctrl+Y: Redo
event.preventDefault();
historyApiRef.current?.redo?.();
@@ -488,23 +496,23 @@ const EmbedPdfViewerContent = ({
// Non-modifier shortcuts
switch (event.key) {
case 'Home':
case "Home":
event.preventDefault();
scrollActions.scrollToFirstPage();
return;
case 'End':
case "End":
event.preventDefault();
scrollActions.scrollToLastPage();
return;
case 'PageUp':
case "PageUp":
event.preventDefault();
scrollActions.scrollToPreviousPage();
return;
case 'PageDown':
case "PageDown":
event.preventDefault();
scrollActions.scrollToNextPage();
return;
case 'Escape':
case "Escape":
if (isSearchInterfaceVisible) {
event.preventDefault();
searchInterfaceActions.close();
@@ -513,14 +521,23 @@ const EmbedPdfViewerContent = ({
}
};
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [
isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions,
scrollActions, printActions, exportActions, rotationActions, historyApiRef,
viewerApplyChanges, cyclePdfRenderMode, viewerKeyCommand,
isViewerHovered,
isSearchInterfaceVisible,
zoomActions,
searchInterfaceActions,
scrollActions,
printActions,
exportActions,
rotationActions,
historyApiRef,
viewerApplyChanges,
cyclePdfRenderMode,
viewerKeyCommand,
]);
// Watch the annotation history API to detect when the document becomes "dirty".
@@ -542,7 +559,7 @@ const EmbedPdfViewerContent = ({
const unsubscribe = historyApi.subscribe(updateHasChanges);
return () => {
if (typeof unsubscribe === 'function') {
if (typeof unsubscribe === "function") {
unsubscribe();
}
};
@@ -572,14 +589,21 @@ const EmbedPdfViewerContent = ({
return () => {
unregisterUnsavedChangesChecker();
};
}, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, isManualRedactMode, redactionsApplied]);
}, [
historyApiRef,
previewFile,
registerUnsavedChangesChecker,
unregisterUnsavedChangesChecker,
isManualRedactMode,
redactionsApplied,
]);
// Save changes - save annotations and redactions to file (overwrites active file)
const applyChanges = useCallback(async () => {
if (!currentFile || activeFileIds.length === 0) return;
try {
console.log('[Viewer] Applying changes - exporting PDF with annotations/redactions');
console.log("[Viewer] Applying changes - exporting PDF with annotations/redactions");
// Use the continuously tracked scroll position - more reliable than reading at this moment
const pageToRestore = lastKnownScrollPageRef.current;
@@ -597,32 +621,32 @@ const EmbedPdfViewerContent = ({
}
if (hadPendingRedactions) {
console.log('[Viewer] Committing pending redactions before export');
console.log("[Viewer] Committing pending redactions before export");
redactionTrackerRef.current?.commitAllPending();
// Give a small delay for the commit to process
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise((resolve) => setTimeout(resolve, 100));
}
// Step 1: Export PDF with annotations using EmbedPDF
const arrayBuffer = await exportActions.saveAsCopy();
if (!arrayBuffer) {
throw new Error('Failed to export PDF');
throw new Error("Failed to export PDF");
}
// Step 2: Convert ArrayBuffer to File
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const filename = currentFile.name || 'document.pdf';
const file = new File([blob], filename, { type: 'application/pdf' });
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
const filename = currentFile.name || "document.pdf";
const file = new File([blob], filename, { type: "application/pdf" });
// Step 3: Create StirlingFiles and stubs for version history
// Only consume the current file, not all active files
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error('Current file ID not found');
if (!currentFileId) throw new Error("Current file ID not found");
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error('Parent stub not found');
if (!parentStub) throw new Error("Parent stub not found");
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? "multiTool");
// Store the page to restore after file replacement triggers re-render
pendingScrollRestoreRef.current = pageToRestore;
@@ -643,61 +667,74 @@ const EmbedPdfViewerContent = ({
setHasUnsavedChanges(false);
setRedactionsApplied(false);
} catch (error) {
console.error('Apply changes failed:', error);
console.error("Apply changes failed:", error);
}
}, [currentFile, activeFiles, activeFileIndex, exportActions, actions, selectors, setHasUnsavedChanges, setRedactionsApplied, rotationState.rotation]);
}, [
currentFile,
activeFiles,
activeFileIndex,
exportActions,
actions,
selectors,
setHasUnsavedChanges,
setRedactionsApplied,
rotationState.rotation,
]);
// Apply form fill changes - reload the filled PDF into the viewer
const handleFormApply = useCallback(async (filledBlob: Blob) => {
if (formApplyInProgressRef.current) return;
if (!currentFile || activeFileIds.length === 0) return;
const handleFormApply = useCallback(
async (filledBlob: Blob) => {
if (formApplyInProgressRef.current) return;
if (!currentFile || activeFileIds.length === 0) return;
formApplyInProgressRef.current = true;
try {
console.log('[Viewer] Applying form fill changes - reloading filled PDF');
formApplyInProgressRef.current = true;
try {
console.log("[Viewer] Applying form fill changes - reloading filled PDF");
// Use the continuously tracked scroll position
const pageToRestore = lastKnownScrollPageRef.current;
// Use the continuously tracked scroll position
const pageToRestore = lastKnownScrollPageRef.current;
// Save the current rotation to restore after reload
const currentRotation = rotationState.rotation ?? 0;
// Save the current rotation to restore after reload
const currentRotation = rotationState.rotation ?? 0;
// Convert Blob to File
const filename = currentFile.name || 'document.pdf';
const file = new File([filledBlob], filename, { type: 'application/pdf' });
// Convert Blob to File
const filename = currentFile.name || "document.pdf";
const file = new File([filledBlob], filename, { type: "application/pdf" });
// Get current file info for creating the updated version
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error('Current file ID not found');
// Get current file info for creating the updated version
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error("Current file ID not found");
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error('Parent stub not found');
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error("Parent stub not found");
// Create StirlingFiles and stubs for version history
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
// Create StirlingFiles and stubs for version history
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? "multiTool");
// Store the page to restore after file replacement
pendingScrollRestoreRef.current = pageToRestore;
scrollRestoreAttemptsRef.current = 0;
// Store the page to restore after file replacement
pendingScrollRestoreRef.current = pageToRestore;
scrollRestoreAttemptsRef.current = 0;
// Store the rotation to restore after file replacement
pendingRotationRestoreRef.current = currentRotation;
rotationRestoreAttemptsRef.current = 0;
// Store the rotation to restore after file replacement
pendingRotationRestoreRef.current = currentRotation;
rotationRestoreAttemptsRef.current = 0;
// Track the new file ID so the viewer follows it after the list reorders
const newFileId = stubs[0]?.id;
if (newFileId) setActiveFileId(newFileId);
// Track the new file ID so the viewer follows it after the list reorders
const newFileId = stubs[0]?.id;
if (newFileId) setActiveFileId(newFileId);
// Replace the current file in context
await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
// Replace the current file in context
await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
console.log('[Viewer] Form fill changes applied successfully');
} catch (error) {
console.error('[Viewer] Apply form changes failed:', error);
} finally {
formApplyInProgressRef.current = false;
}
}, [currentFile, activeFiles, activeFileIndex, actions, selectors, activeFileIds.length, rotationState.rotation]);
console.log("[Viewer] Form fill changes applied successfully");
} catch (error) {
console.error("[Viewer] Apply form changes failed:", error);
} finally {
formApplyInProgressRef.current = false;
}
},
[currentFile, activeFiles, activeFileIndex, actions, selectors, activeFileIds.length, rotationState.rotation],
);
useEffect(() => {
const handler = (e: Event) => {
@@ -706,47 +743,50 @@ const EmbedPdfViewerContent = ({
handleFormApply(blob);
}
};
window.addEventListener('formfill:apply', handler);
return () => window.removeEventListener('formfill:apply', handler);
window.addEventListener("formfill:apply", handler);
return () => window.removeEventListener("formfill:apply", handler);
}, [handleFormApply]);
// Apply layer visibility changes - reload the modified PDF into the viewer
const layerApplyInProgressRef = useRef(false);
const handleLayerApply = useCallback(async (modifiedBlob: Blob) => {
if (layerApplyInProgressRef.current) return;
if (!currentFile || activeFileIds.length === 0) return;
const handleLayerApply = useCallback(
async (modifiedBlob: Blob) => {
if (layerApplyInProgressRef.current) return;
if (!currentFile || activeFileIds.length === 0) return;
layerApplyInProgressRef.current = true;
try {
const pageToRestore = lastKnownScrollPageRef.current;
const currentRotation = rotationState.rotation ?? 0;
layerApplyInProgressRef.current = true;
try {
const pageToRestore = lastKnownScrollPageRef.current;
const currentRotation = rotationState.rotation ?? 0;
const filename = currentFile.name || 'document.pdf';
const file = new File([modifiedBlob], filename, { type: 'application/pdf' });
const filename = currentFile.name || "document.pdf";
const file = new File([modifiedBlob], filename, { type: "application/pdf" });
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error('Current file ID not found');
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error("Current file ID not found");
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error('Parent stub not found');
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error("Parent stub not found");
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? "multiTool");
pendingScrollRestoreRef.current = pageToRestore;
scrollRestoreAttemptsRef.current = 0;
pendingRotationRestoreRef.current = currentRotation;
rotationRestoreAttemptsRef.current = 0;
pendingScrollRestoreRef.current = pageToRestore;
scrollRestoreAttemptsRef.current = 0;
pendingRotationRestoreRef.current = currentRotation;
rotationRestoreAttemptsRef.current = 0;
const newFileId = stubs[0]?.id;
if (newFileId) setActiveFileId(newFileId);
const newFileId = stubs[0]?.id;
if (newFileId) setActiveFileId(newFileId);
await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
} catch (error) {
console.error('[Viewer] Apply layer changes failed:', error);
} finally {
layerApplyInProgressRef.current = false;
}
}, [currentFile, activeFiles, activeFileIndex, actions, selectors, activeFileIds.length, rotationState.rotation]);
await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
} catch (error) {
console.error("[Viewer] Apply layer changes failed:", error);
} finally {
layerApplyInProgressRef.current = false;
}
},
[currentFile, activeFiles, activeFileIndex, actions, selectors, activeFileIds.length, rotationState.rotation],
);
// Discard pending redactions but save already-applied ones
// This is called when user clicks "Discard & Leave" - we want to:
@@ -759,7 +799,7 @@ const EmbedPdfViewerContent = ({
}
try {
console.log('[Viewer] Discarding pending marks but saving applied redactions');
console.log("[Viewer] Discarding pending marks but saving applied redactions");
// Save current view state to restore after file replacement
const pageToRestore = lastKnownScrollPageRef.current;
@@ -768,22 +808,22 @@ const EmbedPdfViewerContent = ({
// Export PDF WITHOUT committing pending marks - this saves only applied redactions
const arrayBuffer = await exportActions.saveAsCopy();
if (!arrayBuffer) {
throw new Error('Failed to export PDF');
throw new Error("Failed to export PDF");
}
// Convert ArrayBuffer to File
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const filename = currentFile.name || 'document.pdf';
const file = new File([blob], filename, { type: 'application/pdf' });
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
const filename = currentFile.name || "document.pdf";
const file = new File([blob], filename, { type: "application/pdf" });
// Create StirlingFiles and stubs for version history
const currentFileId = activeFiles[activeFileIndex]?.fileId;
if (!currentFileId) throw new Error('Current file ID not found');
if (!currentFileId) throw new Error("Current file ID not found");
const parentStub = selectors.getStirlingFileStub(currentFileId);
if (!parentStub) throw new Error('Parent stub not found');
if (!parentStub) throw new Error("Parent stub not found");
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? "multiTool");
// Store view state to restore after file replacement
pendingScrollRestoreRef.current = pageToRestore;
@@ -798,11 +838,22 @@ const EmbedPdfViewerContent = ({
hasAnnotationChangesRef.current = false;
setRedactionsApplied(false);
console.log('[Viewer] Applied redactions saved, pending marks discarded');
console.log("[Viewer] Applied redactions saved, pending marks discarded");
} catch (error) {
console.error('Failed to save applied redactions:', error);
console.error("Failed to save applied redactions:", error);
}
}, [redactionsApplied, currentFile, activeFiles, activeFileIndex, activeFileIds.length, exportActions, actions, selectors, setRedactionsApplied, rotationState.rotation]);
}, [
redactionsApplied,
currentFile,
activeFiles,
activeFileIndex,
activeFileIds.length,
exportActions,
actions,
selectors,
setRedactionsApplied,
rotationState.rotation,
]);
// Register navigation warning handlers so the global modal can call our save/discard logic
useEffect(() => {
@@ -824,7 +875,13 @@ const EmbedPdfViewerContent = ({
},
});
return () => unregisterNavigationWarningHandlers();
}, [previewFile, applyChanges, discardAndSaveApplied, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]);
}, [
previewFile,
applyChanges,
discardAndSaveApplied,
registerNavigationWarningHandlers,
unregisterNavigationWarningHandlers,
]);
// Restore scroll position after file replacement or tool switch
// Uses polling with retries to ensure the scroll succeeds
@@ -841,7 +898,7 @@ const EmbedPdfViewerContent = ({
// Only attempt if we have valid state (totalPages > 0 means PDF is loaded)
if (currentState.totalPages > 0 && targetPage > 0) {
scrollActions.scrollToPage(targetPage, 'instant');
scrollActions.scrollToPage(targetPage, "instant");
// Check if scroll succeeded after a brief delay
setTimeout(() => {
@@ -943,10 +1000,17 @@ const EmbedPdfViewerContent = ({
useEffect(() => {
const file = effectiveFile?.file;
if (!file) { setPageMeasureScales(null); return; }
if (!file) {
setPageMeasureScales(null);
return;
}
let cancelled = false;
extractPageMeasureScales(file).then(scales => { if (!cancelled) setPageMeasureScales(scales); });
return () => { cancelled = true; };
extractPageMeasureScales(file).then((scales) => {
if (!cancelled) setPageMeasureScales(scales);
});
return () => {
cancelled = true;
};
}, [effectiveFile]);
// Register viewer right-rail buttons
@@ -969,7 +1033,7 @@ const EmbedPdfViewerContent = ({
formFillProviderRef.current = isFormFillToolActive;
if (fileChanged) {
console.log('[FormFill] File changed. Old:', formFillFileIdRef.current, 'New:', currentFileId);
console.log("[FormFill] File changed. Old:", formFillFileIdRef.current, "New:", currentFileId);
formFillFileIdRef.current = currentFileId;
// NOTE: Don't call resetFormFill() here — fetchFormFields() handles
// clearing old state internally. Calling reset() before fetch() would
@@ -978,7 +1042,7 @@ const EmbedPdfViewerContent = ({
}
if (currentFile && (fileChanged || providerChanged)) {
console.log('[FormFill] Fetching form fields for:', currentFileId);
console.log("[FormFill] Fetching form fields for:", currentFileId);
fetchFormFields(currentFile, currentFileId ?? undefined);
}
}, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields]);
@@ -998,20 +1062,21 @@ const EmbedPdfViewerContent = ({
onMouseEnter={() => setIsViewerHovered(true)}
onMouseLeave={() => setIsViewerHovered(false)}
style={{
position: 'relative',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
contain: 'layout style paint'
}}>
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
contain: "layout style paint",
}}
>
{/* Close Button - Only show in preview mode */}
{onClose && previewFile && (
<ActionIcon
variant="filled"
color="gray"
size="lg"
style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 1000, borderRadius: '50%' }}
style={{ position: "absolute", top: "1rem", right: "1rem", zIndex: 1000, borderRadius: "50%" }}
onClick={onClose}
>
<CloseIcon />
@@ -1028,26 +1093,31 @@ const EmbedPdfViewerContent = ({
<Box
ref={pdfContainerRef}
style={{
position: 'relative',
position: "relative",
flex: 1,
overflow: 'hidden',
overflow: "hidden",
minHeight: 0,
minWidth: 0,
marginRight: `${totalRightMargin}rem`,
transition: 'margin-right 0.3s ease'
}}>
transition: "margin-right 0.3s ease",
}}
>
<LocalEmbedPDF
key={currentFileId || 'no-file'}
key={currentFileId || "no-file"}
pdfRenderMode={pdfRenderMode}
file={effectiveFile.file}
url={effectiveFile.url}
fileName={
previewFile ? previewFile.name :
(currentFile && isStirlingFile(currentFile) ? currentFile.name :
(effectiveFile?.file instanceof File ? effectiveFile.file.name : undefined))
previewFile
? previewFile.name
: currentFile && isStirlingFile(currentFile)
? currentFile.name
: effectiveFile?.file instanceof File
? effectiveFile.file.name
: undefined
}
enableAnnotations={shouldEnableAnnotations}
isSignMode={selectedTool === 'sign'}
isSignMode={selectedTool === "sign"}
showBakedAnnotations={isAnnotationsVisible}
enableRedaction={shouldEnableRedaction}
enableFormFill={shouldEnableFormFill}
@@ -1065,21 +1135,13 @@ const EmbedPdfViewerContent = ({
}}
/>
{/* Floating save bar for form-filled PDFs (like Chrome/Firefox PDF viewers) */}
<FormSaveBar
file={currentFile ?? null}
isFormFillToolActive={isFormFillToolActive}
onApply={handleFormApply}
/>
<FormSaveBar file={currentFile ?? null} isFormFillToolActive={isFormFillToolActive} onApply={handleFormApply} />
<StampPlacementOverlay
containerRef={pdfContainerRef}
isActive={isPlacementOverlayActive}
signatureConfig={signatureConfig}
/>
<RulerOverlay
containerRef={pdfContainerRef}
isActive={isRulerActive}
pageMeasureScales={pageMeasureScales}
/>
<RulerOverlay containerRef={pdfContainerRef} isActive={isRulerActive} pageMeasureScales={pageMeasureScales} />
</Box>
</>
)}
@@ -1100,15 +1162,11 @@ const EmbedPdfViewerContent = ({
}}
>
<div style={{ pointerEvents: "auto" }}>
<PdfViewerToolbar
currentPage={scrollState.currentPage}
totalPages={scrollState.totalPages}
/>
<PdfViewerToolbar currentPage={scrollState.currentPage} totalPages={scrollState.totalPages} />
</div>
</div>
)}
{/* Thumbnail Sidebar */}
<ThumbnailSidebar
visible={isThumbnailSidebarVisible}
@@ -1140,15 +1198,12 @@ const EmbedPdfViewerContent = ({
onApplyLayers={handleLayerApply}
onLayersDetected={setHasLayers}
/>
</Box>
);
};
const EmbedPdfViewer = (props: EmbedPdfViewerProps) => {
return (
<EmbedPdfViewerContent {...props} />
);
return <EmbedPdfViewerContent {...props} />;
};
export default EmbedPdfViewer;