add save button and allow user to change view while annotations tool is open

This commit is contained in:
EthanHealy01 2025-12-18 15:14:27 +00:00
parent f5b9f5a910
commit d036444af1
3 changed files with 103 additions and 26 deletions

View File

@ -56,9 +56,6 @@ const EmbedPdfViewerContent = ({
exportActions,
} = useViewer();
// Register viewer right-rail buttons
useViewerRightRailButtons();
const scrollState = getScrollState();
const rotationState = getRotationState();
@ -73,6 +70,11 @@ const EmbedPdfViewerContent = ({
// Get signature and annotation contexts
const { signatureApiRef, annotationApiRef, historyApiRef, signatureConfig, isPlacementMode } = useSignature();
// Track whether there are unsaved annotation changes in this viewer session.
// This is our source of truth for navigation guards; it is set when the
// annotation history changes, and cleared after we successfully apply changes.
const hasAnnotationChangesRef = useRef(false);
// Get current file from FileContext
const { selectors, state } = useFileState();
const { actions } = useFileActions();
@ -225,6 +227,31 @@ const EmbedPdfViewerContent = ({
};
}, [isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions]);
// Watch the annotation history API to detect when the document becomes "dirty".
// We treat any change that makes the history undoable as unsaved changes until
// the user explicitly applies them via applyChanges.
useEffect(() => {
const historyApi = historyApiRef.current;
if (!historyApi || !historyApi.subscribe) {
return;
}
const updateHasChanges = () => {
const canUndo = historyApi.canUndo?.() ?? false;
if (!hasAnnotationChangesRef.current && canUndo) {
hasAnnotationChangesRef.current = true;
setHasUnsavedChanges(true);
}
};
const unsubscribe = historyApi.subscribe(updateHasChanges);
return () => {
if (typeof unsubscribe === 'function') {
unsubscribe();
}
};
}, [historyApiRef, setHasUnsavedChanges]);
// Register checker for unsaved changes (annotations only for now)
useEffect(() => {
if (previewFile) {
@ -232,39 +259,28 @@ const EmbedPdfViewerContent = ({
}
const checkForChanges = () => {
// Check for annotation changes via history
const hasAnnotationChanges = historyApiRef.current?.canUndo() || false;
console.log('[Viewer] Checking for unsaved changes:', {
hasAnnotationChanges
});
const hasAnnotationChanges = hasAnnotationChangesRef.current;
return hasAnnotationChanges;
};
console.log('[Viewer] Registering unsaved changes checker');
registerUnsavedChangesChecker(checkForChanges);
return () => {
console.log('[Viewer] Unregistering unsaved changes checker');
unregisterUnsavedChangesChecker();
};
}, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
}, [previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
// Apply changes - save annotations to new file version
const applyChanges = useCallback(async () => {
if (!currentFile || activeFileIds.length === 0) return;
try {
console.log('[Viewer] Applying changes - exporting PDF with annotations');
// Step 1: Export PDF with annotations using EmbedPDF
const arrayBuffer = await exportActions.saveAsCopy();
if (!arrayBuffer) {
throw new Error('Failed to export PDF');
}
console.log('[Viewer] Exported PDF size:', arrayBuffer.byteLength);
// Step 2: Convert ArrayBuffer to File
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const filename = currentFile.name || 'document.pdf';
@ -279,12 +295,19 @@ const EmbedPdfViewerContent = ({
// Step 4: Consume files (replace in context)
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
// Mark annotations as saved so navigation away from the viewer is allowed.
hasAnnotationChangesRef.current = false;
setHasUnsavedChanges(false);
} catch (error) {
console.error('Apply changes failed:', error);
}
}, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
// Register viewer right-rail buttons (including optional Save for annotations)
useViewerRightRailButtons({
onSaveAnnotations: applyChanges,
});
const sidebarWidthRem = 15;
const totalRightMargin =
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0);

View File

@ -1,4 +1,4 @@
import { useMemo, useState, useEffect, useCallback } from 'react';
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import { ActionIcon, Popover } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useViewer } from '@app/contexts/ViewerContext';
@ -13,7 +13,15 @@ import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useNavigationState } from '@app/contexts/NavigationContext';
import { BASE_PATH, withBasePath } from '@app/constants/app';
export function useViewerRightRailButtons() {
interface ViewerRightRailButtonsOptions {
/**
* Optional handler to save annotation changes to a new PDF version.
* When provided, a Save button will be shown in the viewer right rail.
*/
onSaveAnnotations?: () => void | Promise<void>;
}
export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOptions) {
const { t, i18n } = useTranslation();
const viewer = useViewer();
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
@ -22,6 +30,14 @@ export function useViewerRightRailButtons() {
const { handleToolSelect } = useToolWorkflow();
const { selectedTool } = useNavigationState();
// Keep the latest save handler in a ref to avoid re-registering right-rail
// buttons on every render when the callback identity changes.
const saveAnnotationsRef = useRef<(() => void | Promise<void>) | undefined>(undefined);
useEffect(() => {
saveAnnotationsRef.current = options?.onSaveAnnotations;
}, [options?.onSaveAnnotations]);
const stripBasePath = useCallback((path: string) => {
if (BASE_PATH && path.startsWith(BASE_PATH)) {
return path.slice(BASE_PATH.length) || '/';
@ -55,9 +71,13 @@ export function useViewerRightRailButtons() {
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
const printLabel = t('rightRail.print', 'Print PDF');
const annotationsLabel = t('rightRail.annotations', 'Annotations');
const saveChangesLabel = t('rightRail.saveChanges', 'Save Changes');
const viewerButtons = useMemo<RightRailButtonWithAction[]>(() => {
return [
const exportState = viewer.getExportState();
const canExport = Boolean(exportState?.canExport);
const buttons: RightRailButtonWithAction[] = [
{
id: 'viewer-search',
tooltip: searchLabel,
@ -214,9 +234,47 @@ export function useViewerRightRailButtons() {
render: ({ disabled }) => (
<ViewerAnnotationControls currentView="viewer" disabled={disabled} />
)
}
},
];
}, [t, i18n.language, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel, printLabel, tooltipPosition, annotationsLabel, isAnnotationsActive, handleToolSelect]);
// Optional: Save button for annotations (always registered when this hook is used
// with a save handler; uses a ref to avoid infinite re-registration loops).
buttons.push({
id: 'viewer-save-annotations',
icon: <LocalIcon icon="save" width="1.5rem" height="1.5rem" />,
tooltip: saveChangesLabel,
ariaLabel: saveChangesLabel,
section: 'top' as const,
order: 59,
disabled: !canExport,
visible: true,
onClick: () => {
const handler = saveAnnotationsRef.current;
if (handler) {
void handler();
}
},
});
return buttons;
}, [
t,
i18n.language,
viewer,
isPanning,
searchLabel,
panLabel,
rotateLeftLabel,
rotateRightLabel,
sidebarLabel,
bookmarkLabel,
printLabel,
tooltipPosition,
annotationsLabel,
saveChangesLabel,
isAnnotationsActive,
handleToolSelect,
]);
useRightRailButtons(viewerButtons);
}

View File

@ -38,7 +38,7 @@ const isKnownAnnotationTool = (toolId: string | undefined | null): toolId is Ann
const Annotate = (_props: BaseToolProps) => {
const { t } = useTranslation();
const { setToolAndWorkbench, selectedTool, workbench } = useNavigation();
const { selectedTool, workbench } = useNavigation();
const { selectedFiles } = useFileSelection();
const {
signatureApiRef,
@ -143,10 +143,6 @@ const Annotate = (_props: BaseToolProps) => {
setTextAlignment,
} = styleActions;
useEffect(() => {
setToolAndWorkbench('annotate', 'viewer');
}, [setToolAndWorkbench]);
useEffect(() => {
const isAnnotateActive = workbench === 'viewer' && selectedTool === 'annotate';
if (wasAnnotateActiveRef.current && !isAnnotateActive) {