mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
Fix/redact bug (#6048)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useBanner } from '@app/contexts/BannerContext';
|
||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -26,6 +27,7 @@ export function AppLayout({ children }: AppLayoutProps) {
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<NavigationWarningModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import '@app/components/pageEditor/PageEditor.module.css';
|
||||
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
|
||||
import DragDropGrid from '@app/components/pageEditor/DragDropGrid';
|
||||
import SkeletonLoader from '@app/components/shared/SkeletonLoader';
|
||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||
import { FileId } from "@app/types/file";
|
||||
import { GRID_CONSTANTS } from '@app/components/pageEditor/constants';
|
||||
import { useInitialPageDocument } from '@app/components/pageEditor/hooks/useInitialPageDocument';
|
||||
@@ -39,7 +38,7 @@ const PageEditor = ({
|
||||
const { actions } = useFileActions();
|
||||
|
||||
// Navigation guard for unsaved changes
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
const { setHasUnsavedChanges, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard();
|
||||
const navigationState = useNavigationState();
|
||||
|
||||
// Get PageEditor coordination functions
|
||||
@@ -393,6 +392,19 @@ const PageEditor = ({
|
||||
updateCurrentPages,
|
||||
});
|
||||
|
||||
// Register navigation warning handlers for the global modal
|
||||
useEffect(() => {
|
||||
registerNavigationWarningHandlers({
|
||||
onApplyAndContinue: async () => {
|
||||
await applyChanges();
|
||||
},
|
||||
onExportAndContinue: async () => {
|
||||
await onExportAll();
|
||||
},
|
||||
});
|
||||
return () => unregisterNavigationWarningHandlers();
|
||||
}, [applyChanges, onExportAll, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]);
|
||||
|
||||
// Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument)
|
||||
const selectedPageCount = selectedPageIds.length;
|
||||
const activeFileIds = selectedFileIds;
|
||||
@@ -704,14 +716,6 @@ const PageEditor = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={async () => {
|
||||
await applyChanges();
|
||||
}}
|
||||
onExportAndContinue={async () => {
|
||||
await onExportAll();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { Modal, Text, Button, Group, Stack } from "@mantine/core";
|
||||
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -6,51 +7,69 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
||||
import { Z_INDEX_TOAST } from "@app/styles/zIndex";
|
||||
|
||||
interface NavigationWarningModalProps {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
onExportAndContinue?: () => Promise<void>;
|
||||
/** Called when discarding - allows saving applied changes while discarding pending ones */
|
||||
onDiscardAndContinue?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDiscardAndContinue }: NavigationWarningModalProps) => {
|
||||
const NavigationWarningModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
||||
useNavigationGuard();
|
||||
const {
|
||||
showNavigationWarning,
|
||||
hasUnsavedChanges,
|
||||
pendingNavigation,
|
||||
cancelNavigation,
|
||||
setHasUnsavedChanges,
|
||||
navigationWarningHandlersRef,
|
||||
} = useNavigationGuard();
|
||||
|
||||
// Store pendingNavigation in a ref so async handlers always have the latest,
|
||||
// not a stale closure captured before an await.
|
||||
const pendingNavigationRef = useRef(pendingNavigation);
|
||||
useEffect(() => {
|
||||
pendingNavigationRef.current = pendingNavigation;
|
||||
}, [pendingNavigation]);
|
||||
|
||||
const handleKeepWorking = () => {
|
||||
cancelNavigation();
|
||||
};
|
||||
|
||||
const handleDiscardChanges = async () => {
|
||||
// If a discard handler is provided, call it to save any already-applied changes, then discard the unsaved changes
|
||||
if (onDiscardAndContinue) {
|
||||
await onDiscardAndContinue();
|
||||
}
|
||||
const finishAndNavigate = () => {
|
||||
const nav = pendingNavigationRef.current;
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
cancelNavigation();
|
||||
if (nav) {
|
||||
nav();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscardChanges = async () => {
|
||||
const handlers = navigationWarningHandlersRef.current;
|
||||
if (handlers?.onDiscardAndContinue) {
|
||||
await handlers.onDiscardAndContinue();
|
||||
}
|
||||
finishAndNavigate();
|
||||
};
|
||||
|
||||
const handleApplyAndContinue = async () => {
|
||||
if (onApplyAndContinue) {
|
||||
await onApplyAndContinue();
|
||||
const handlers = navigationWarningHandlersRef.current;
|
||||
if (handlers?.onApplyAndContinue) {
|
||||
await handlers.onApplyAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
finishAndNavigate();
|
||||
};
|
||||
|
||||
const handleExportAndContinue = async () => {
|
||||
if (onExportAndContinue) {
|
||||
await onExportAndContinue();
|
||||
const handlers = navigationWarningHandlersRef.current;
|
||||
if (handlers?.onExportAndContinue) {
|
||||
await handlers.onExportAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
finishAndNavigate();
|
||||
};
|
||||
|
||||
// Read handler availability at render time for button visibility
|
||||
const handlers = navigationWarningHandlersRef.current;
|
||||
const hasApply = !!handlers?.onApplyAndContinue;
|
||||
const hasExport = !!handlers?.onExportAndContinue;
|
||||
|
||||
const BUTTON_WIDTH = "12rem";
|
||||
|
||||
// Only show modal if there are unsaved changes AND there's an actual pending navigation
|
||||
// This prevents the modal from showing due to spurious state updates
|
||||
if (!hasUnsavedChanges || !pendingNavigation) {
|
||||
return null;
|
||||
}
|
||||
@@ -87,12 +106,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
</Button>
|
||||
{onApplyAndContinue && (
|
||||
{hasApply && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
</Button>
|
||||
)}
|
||||
{onExportAndContinue && (
|
||||
{hasExport && (
|
||||
<Button variant="filled" onClick={handleExportAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("exportAndContinue", "Export & Leave")}
|
||||
</Button>
|
||||
@@ -108,12 +127,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
</Button>
|
||||
{onApplyAndContinue && (
|
||||
{hasApply && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
</Button>
|
||||
)}
|
||||
{onExportAndContinue && (
|
||||
{hasExport && (
|
||||
<Button variant="filled" onClick={handleExportAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("exportAndContinue", "Export & Leave")}
|
||||
</Button>
|
||||
|
||||
@@ -28,7 +28,7 @@ import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFileOutlined';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||
import { useNavigationGuard } from '@app/contexts/NavigationContext';
|
||||
|
||||
import { useFileContext } from '@app/contexts/FileContext';
|
||||
import {
|
||||
@@ -415,6 +415,15 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
} : null,
|
||||
});
|
||||
|
||||
// Register navigation warning handlers for the global modal
|
||||
const { registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard();
|
||||
useEffect(() => {
|
||||
registerNavigationWarningHandlers({
|
||||
onApplyAndContinue: onSaveToWorkbench,
|
||||
});
|
||||
return () => unregisterNavigationWarningHandlers();
|
||||
}, [onSaveToWorkbench, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedGroupIds(new Set());
|
||||
lastSelectedGroupIdRef.current = null;
|
||||
@@ -2385,10 +2394,6 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Navigation Warning Modal */}
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={onSaveToWorkbench}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Stack, Text, Divider, ColorInput } from '@mantine/core';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import { useRedaction, useRedactionMode } from '@app/contexts/RedactionContext';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useSignature } from '@app/contexts/SignatureContext';
|
||||
import { useNavigationGuard } from '@app/contexts/NavigationContext';
|
||||
|
||||
interface ManualRedactionControlsProps {
|
||||
disabled?: boolean;
|
||||
@@ -27,40 +27,45 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
// Get signature context to deactivate annotation tools when switching to redaction
|
||||
const { signatureApiRef } = useSignature();
|
||||
|
||||
// Check if redaction mode is active
|
||||
const isRedactActive = isRedacting;
|
||||
|
||||
// Track if we've auto-activated for the current bridge session
|
||||
const hasAutoActivated = useRef(false);
|
||||
// Check if user is navigating away (modal shown) — don't fight the save/leave process
|
||||
const { showNavigationWarning } = useNavigationGuard();
|
||||
|
||||
// Track the previous file index to detect file switches
|
||||
const prevFileIndexRef = useRef<number>(activeFileIndex);
|
||||
|
||||
// Auto-activate selection mode when the API bridge becomes ready
|
||||
// This ensures Mark Text is pre-selected when entering manual redaction mode
|
||||
// Guard: pause auto-reactivation during save/export to avoid interfering with EmbedPDF
|
||||
const isSavingRef = useRef(false);
|
||||
|
||||
// Keep redaction tool active at all times while this component is mounted.
|
||||
// If anything deactivates it (annotation tools, text selection, file switch, etc.)
|
||||
// this re-enables it automatically — no manual "Activate" button needed.
|
||||
useEffect(() => {
|
||||
if (isBridgeReady && !disabled && !hasAutoActivated.current) {
|
||||
hasAutoActivated.current = true;
|
||||
// Small delay to ensure EmbedPDF is fully ready
|
||||
const timer = setTimeout(() => {
|
||||
// Deactivate annotation mode to show redaction layer
|
||||
if (disabled || !isBridgeReady || isSavingRef.current || showNavigationWarning) return;
|
||||
|
||||
if (!isRedacting || isAnnotationMode) {
|
||||
// Kill annotation mode if it stole focus
|
||||
if (isAnnotationMode) {
|
||||
setAnnotationMode(false);
|
||||
// Pre-select the Redaction tool
|
||||
activateManualRedact();
|
||||
}, 150);
|
||||
if (signatureApiRef?.current) {
|
||||
try {
|
||||
signatureApiRef.current.deactivateTools();
|
||||
} catch (error) {
|
||||
console.log('Unable to deactivate annotation tools:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Small delay to avoid racing with EmbedPDF's own state updates
|
||||
const timer = setTimeout(() => {
|
||||
if (!isSavingRef.current) {
|
||||
activateManualRedact();
|
||||
}
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isBridgeReady, disabled, activateManualRedact, setAnnotationMode]);
|
||||
|
||||
// Reset auto-activation flag when disabled changes or bridge becomes not ready
|
||||
useEffect(() => {
|
||||
if (disabled || !isBridgeReady) {
|
||||
hasAutoActivated.current = false;
|
||||
}
|
||||
}, [disabled, isBridgeReady]);
|
||||
}, [isRedacting, isAnnotationMode, disabled, isBridgeReady, showNavigationWarning, setAnnotationMode, signatureApiRef, activateManualRedact]);
|
||||
|
||||
// Reset redaction tool when switching between files
|
||||
// The new PDF gets a fresh EmbedPDF instance - forcing user to re-select tool ensures it works properly
|
||||
// The new PDF gets a fresh EmbedPDF instance
|
||||
useEffect(() => {
|
||||
if (prevFileIndexRef.current !== activeFileIndex) {
|
||||
prevFileIndexRef.current = activeFileIndex;
|
||||
@@ -69,41 +74,24 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
if (activeType) {
|
||||
setActiveType(null);
|
||||
}
|
||||
|
||||
// Reset auto-activation flag so new file can auto-activate
|
||||
hasAutoActivated.current = false;
|
||||
}
|
||||
}, [activeFileIndex, activeType, setActiveType]);
|
||||
|
||||
const handleRedactClick = () => {
|
||||
// Deactivate annotation mode and tools to switch to redaction layer
|
||||
if (isAnnotationMode) {
|
||||
setAnnotationMode(false);
|
||||
// Deactivate any active annotation tools (like draw)
|
||||
if (signatureApiRef?.current) {
|
||||
try {
|
||||
signatureApiRef.current.deactivateTools();
|
||||
} catch (error) {
|
||||
console.log('Unable to deactivate annotation tools:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activateManualRedact();
|
||||
};
|
||||
|
||||
// Handle saving changes - this will apply pending redactions and save to file
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
if (applyChanges) {
|
||||
await applyChanges();
|
||||
isSavingRef.current = true;
|
||||
try {
|
||||
await applyChanges();
|
||||
} finally {
|
||||
isSavingRef.current = false;
|
||||
}
|
||||
}
|
||||
}, [applyChanges]);
|
||||
|
||||
// Check if there are unsaved changes to save (pending redactions OR applied redactions)
|
||||
// Save Changes button will apply pending redactions and then save everything
|
||||
const hasUnsavedChanges = pendingCount > 0 || redactionsApplied;
|
||||
|
||||
// Check if API is available - use isBridgeReady state instead of ref (refs don't trigger re-renders)
|
||||
const isApiReady = isBridgeReady;
|
||||
|
||||
return (
|
||||
@@ -128,18 +116,6 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
popoverProps={{ withinPortal: true }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant={isRedactActive && !isAnnotationMode ? 'filled' : 'outline'}
|
||||
color={isRedactActive && !isAnnotationMode ? 'blue' : 'gray'}
|
||||
leftSection={<AutoFixHighIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleRedactClick}
|
||||
disabled={disabled || !isApiReady}
|
||||
fullWidth
|
||||
size="sm"
|
||||
>
|
||||
{isRedactActive && !isAnnotationMode ? t('redact.manual.active', 'Redaction Mode Active') : t('redact.manual.activate', 'Activate Redaction Tool')}
|
||||
</Button>
|
||||
|
||||
{/* Save Changes Button - applies pending redactions and saves to file */}
|
||||
<Button
|
||||
fullWidth
|
||||
@@ -157,4 +133,3 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@ interface RedactModeSelectorProps {
|
||||
mode: RedactMode;
|
||||
onModeChange: (mode: RedactMode) => void;
|
||||
disabled?: boolean;
|
||||
hasFilesSelected?: boolean; // Files are selected in workbench
|
||||
hasAnyFiles?: boolean; // Any files exist in workbench (for manual mode)
|
||||
hasAnyFiles?: boolean;
|
||||
}
|
||||
|
||||
export default function RedactModeSelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
disabled,
|
||||
hasFilesSelected = false,
|
||||
hasAnyFiles = false
|
||||
export default function RedactModeSelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
disabled,
|
||||
hasAnyFiles = false
|
||||
}: RedactModeSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -28,9 +26,9 @@ export default function RedactModeSelector({
|
||||
{
|
||||
value: 'automatic' as const,
|
||||
label: t('redact.modeSelector.automatic', 'Automatic'),
|
||||
disabled: !hasFilesSelected, // Automatic requires files to be selected
|
||||
tooltip: !hasFilesSelected
|
||||
? t('redact.modeSelector.automaticDisabledTooltip', 'Select files in the file manager to redact multiple files at once')
|
||||
disabled: !hasAnyFiles, // Allow switching to automatic whenever files exist; selection can happen after
|
||||
tooltip: !hasAnyFiles
|
||||
? t('redact.modeSelector.automaticDisabledTooltip', 'Upload files to use automatic redaction')
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||
import { isStirlingFile, getFormFillFileId } from '@app/types/fileContext';
|
||||
import { useViewerRightRailButtons } from '@app/components/viewer/useViewerRightRailButtons';
|
||||
import { StampPlacementOverlay } from '@app/components/viewer/StampPlacementOverlay';
|
||||
@@ -196,7 +195,7 @@ const EmbedPdfViewerContent = ({
|
||||
const selectedFileIds = state.ui.selectedFileIds;
|
||||
|
||||
// Navigation guard for unsaved changes
|
||||
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard();
|
||||
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard();
|
||||
|
||||
const { selectedTool } = useNavigationState();
|
||||
|
||||
@@ -619,7 +618,7 @@ const EmbedPdfViewerContent = ({
|
||||
const parentStub = selectors.getStirlingFileStub(currentFileId);
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
|
||||
|
||||
// Store the page to restore after file replacement triggers re-render
|
||||
pendingScrollRestoreRef.current = pageToRestore;
|
||||
@@ -673,7 +672,7 @@ const EmbedPdfViewerContent = ({
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
// Create StirlingFiles and stubs for version history
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
|
||||
|
||||
// Store the page to restore after file replacement
|
||||
pendingScrollRestoreRef.current = pageToRestore;
|
||||
@@ -731,7 +730,7 @@ const EmbedPdfViewerContent = ({
|
||||
const parentStub = selectors.getStirlingFileStub(currentFileId);
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
|
||||
|
||||
pendingScrollRestoreRef.current = pageToRestore;
|
||||
scrollRestoreAttemptsRef.current = 0;
|
||||
@@ -786,7 +785,7 @@ const EmbedPdfViewerContent = ({
|
||||
const parentStub = selectors.getStirlingFileStub(currentFileId);
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, selectedTool ?? 'multiTool');
|
||||
|
||||
// Store view state to restore after file replacement
|
||||
pendingScrollRestoreRef.current = pageToRestore;
|
||||
@@ -807,6 +806,28 @@ const EmbedPdfViewerContent = ({
|
||||
}
|
||||
}, [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(() => {
|
||||
if (previewFile) return;
|
||||
|
||||
registerNavigationWarningHandlers({
|
||||
onApplyAndContinue: async () => {
|
||||
await applyChanges();
|
||||
},
|
||||
onDiscardAndContinue: async () => {
|
||||
await discardAndSaveApplied();
|
||||
const historyApi = historyApiRef.current;
|
||||
if (historyApi?.canUndo) {
|
||||
while (historyApi.canUndo()) {
|
||||
historyApi.undo?.();
|
||||
}
|
||||
}
|
||||
hasAnnotationChangesRef.current = false;
|
||||
},
|
||||
});
|
||||
return () => unregisterNavigationWarningHandlers();
|
||||
}, [previewFile, applyChanges, discardAndSaveApplied, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]);
|
||||
|
||||
// Restore scroll position after file replacement or tool switch
|
||||
// Uses polling with retries to ensure the scroll succeeds
|
||||
useEffect(() => {
|
||||
@@ -1122,20 +1143,6 @@ const EmbedPdfViewerContent = ({
|
||||
onLayersDetected={setHasLayers}
|
||||
/>
|
||||
|
||||
{/* Navigation Warning Modal */}
|
||||
{!previewFile && (
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={async () => {
|
||||
await applyChanges();
|
||||
}}
|
||||
onDiscardAndContinue={async () => {
|
||||
// Save applied redactions (if any) while discarding pending ones
|
||||
await discardAndSaveApplied();
|
||||
// Reset annotation changes ref so future show/hide doesn't re-prompt
|
||||
hasAnnotationChangesRef.current = false;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useViewerRightRailButtons(
|
||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
||||
const { handleToolSelect, handleBackToTools } = useToolWorkflow();
|
||||
const { handleToolSelect, handleToolSelectForced, handleBackToTools } = useToolWorkflow();
|
||||
const { selectedTool } = useNavigationState();
|
||||
const { requestNavigation } = useNavigationGuard();
|
||||
const { redactionsApplied, activeType: redactionActiveType } = useRedaction();
|
||||
@@ -373,7 +373,9 @@ export function useViewerRightRailButtons(
|
||||
window.history.pushState(null, '', targetPath);
|
||||
}
|
||||
setIsAnnotationsActive(true);
|
||||
handleToolSelect('annotate');
|
||||
// Use handleToolSelectForced to bypass the unsaved-changes guard —
|
||||
// the navigation warning modal already handled that check.
|
||||
handleToolSelectForced('annotate');
|
||||
};
|
||||
|
||||
if (hasRedactionChanges) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
|
||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useRef } from 'react';
|
||||
import { WorkbenchType, getDefaultWorkbench } from '@app/types/workbench';
|
||||
import { ToolId, isValidToolId } from '@app/types/toolId';
|
||||
import { useToolRegistry } from '@app/contexts/ToolRegistryContext';
|
||||
@@ -68,6 +68,13 @@ const initialState: NavigationContextState = {
|
||||
showNavigationWarning: false
|
||||
};
|
||||
|
||||
// Handlers that editors register for the navigation warning modal
|
||||
export interface NavigationWarningHandlers {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
onExportAndContinue?: () => Promise<void>;
|
||||
onDiscardAndContinue?: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Navigation context actions interface
|
||||
export interface NavigationContextActions {
|
||||
setWorkbench: (workbench: WorkbenchType) => void;
|
||||
@@ -82,6 +89,9 @@ export interface NavigationContextActions {
|
||||
cancelNavigation: () => void;
|
||||
clearToolSelection: () => void;
|
||||
handleToolSelect: (toolId: string) => void;
|
||||
registerNavigationWarningHandlers: (handlers: NavigationWarningHandlers) => void;
|
||||
unregisterNavigationWarningHandlers: () => void;
|
||||
navigationWarningHandlersRef: React.RefObject<NavigationWarningHandlers | null>;
|
||||
}
|
||||
|
||||
// Context state values
|
||||
@@ -109,6 +119,7 @@ export const NavigationProvider: React.FC<{
|
||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||
const { allTools: toolRegistry } = useToolRegistry();
|
||||
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
||||
const navigationWarningHandlersRef = useRef<NavigationWarningHandlers | null>(null);
|
||||
|
||||
// Memoize individual callbacks
|
||||
const setWorkbench = useCallback((workbench: WorkbenchType) => {
|
||||
@@ -191,6 +202,14 @@ export const NavigationProvider: React.FC<{
|
||||
unsavedChangesCheckerRef.current = null;
|
||||
}, []);
|
||||
|
||||
const registerNavigationWarningHandlers = useCallback((handlers: NavigationWarningHandlers) => {
|
||||
navigationWarningHandlersRef.current = handlers;
|
||||
}, []);
|
||||
|
||||
const unregisterNavigationWarningHandlers = useCallback(() => {
|
||||
navigationWarningHandlersRef.current = null;
|
||||
}, []);
|
||||
|
||||
const showNavigationWarning = useCallback((show: boolean) => {
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
||||
}, []);
|
||||
@@ -277,6 +296,9 @@ export const NavigationProvider: React.FC<{
|
||||
cancelNavigation,
|
||||
clearToolSelection,
|
||||
handleToolSelect,
|
||||
registerNavigationWarningHandlers,
|
||||
unregisterNavigationWarningHandlers,
|
||||
navigationWarningHandlersRef,
|
||||
}), [
|
||||
setWorkbench,
|
||||
setSelectedTool,
|
||||
@@ -290,6 +312,8 @@ export const NavigationProvider: React.FC<{
|
||||
cancelNavigation,
|
||||
clearToolSelection,
|
||||
handleToolSelect,
|
||||
registerNavigationWarningHandlers,
|
||||
unregisterNavigationWarningHandlers,
|
||||
]);
|
||||
|
||||
const stateValue: NavigationContextStateValue = {
|
||||
@@ -353,6 +377,9 @@ export const useNavigationGuard = () => {
|
||||
setHasUnsavedChanges: actions.setHasUnsavedChanges,
|
||||
setShowNavigationWarning: actions.showNavigationWarning,
|
||||
registerUnsavedChangesChecker: actions.registerUnsavedChangesChecker,
|
||||
unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker
|
||||
unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker,
|
||||
registerNavigationWarningHandlers: actions.registerNavigationWarningHandlers,
|
||||
unregisterNavigationWarningHandlers: actions.unregisterNavigationWarningHandlers,
|
||||
navigationWarningHandlersRef: actions.navigationWarningHandlersRef,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ const Redact = (props: BaseToolProps) => {
|
||||
|
||||
// Compute actual collapsed state based on results and user state
|
||||
const getActualCollapsedState = (userCollapsed: boolean) => {
|
||||
return (!base.hasFiles || base.hasResults) ? true : userCollapsed; // Force collapse when results are shown
|
||||
return base.hasResults ? true : userCollapsed; // Force collapse when results are shown
|
||||
};
|
||||
|
||||
// Build conditional steps based on redaction mode
|
||||
@@ -117,7 +117,6 @@ const Redact = (props: BaseToolProps) => {
|
||||
mode={base.params.parameters.mode}
|
||||
onModeChange={handleModeChange}
|
||||
disabled={base.endpointLoading}
|
||||
hasFilesSelected={base.hasFiles}
|
||||
hasAnyFiles={hasAnyFiles}
|
||||
/>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user