diff --git a/frontend/src/core/components/AppLayout.tsx b/frontend/src/core/components/AppLayout.tsx
index 39de5dc650..9bcd31e6db 100644
--- a/frontend/src/core/components/AppLayout.tsx
+++ b/frontend/src/core/components/AppLayout.tsx
@@ -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}
+
>
);
}
diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx
index b182c9d3a8..1b965367d5 100644
--- a/frontend/src/core/components/pageEditor/PageEditor.tsx
+++ b/frontend/src/core/components/pageEditor/PageEditor.tsx
@@ -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 = ({
)}
- {
- await applyChanges();
- }}
- onExportAndContinue={async () => {
- await onExportAll();
- }}
- />
);
};
diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx
index 6e143ccd18..8e80b5d771 100644
--- a/frontend/src/core/components/shared/NavigationWarningModal.tsx
+++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx
@@ -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;
- onExportAndContinue?: () => Promise;
- /** Called when discarding - allows saving applied changes while discarding pending ones */
- onDiscardAndContinue?: () => Promise;
-}
-
-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
}>
{t("discardChanges", "Discard Changes")}
- {onApplyAndContinue && (
+ {hasApply && (
}>
{t("applyAndContinue", "Apply & Leave")}
)}
- {onExportAndContinue && (
+ {hasExport && (
}>
{t("exportAndContinue", "Export & Leave")}
@@ -108,12 +127,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis
}>
{t("discardChanges", "Discard Changes")}
- {onApplyAndContinue && (
+ {hasApply && (
}>
{t("applyAndContinue", "Apply & Leave")}
)}
- {onExportAndContinue && (
+ {hasExport && (
}>
{t("exportAndContinue", "Export & Leave")}
diff --git a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx
index 99cd6ad99a..119ec4e381 100644
--- a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx
+++ b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx
@@ -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(() => {
)}
- {/* Navigation Warning Modal */}
-
);
};
diff --git a/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx
index 4c0fa41ef5..dd75d926d6 100644
--- a/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx
+++ b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx
@@ -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(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 }}
/>
- }
- onClick={handleRedactClick}
- disabled={disabled || !isApiReady}
- fullWidth
- size="sm"
- >
- {isRedactActive && !isAnnotationMode ? t('redact.manual.active', 'Redaction Mode Active') : t('redact.manual.activate', 'Activate Redaction Tool')}
-
-
{/* Save Changes Button - applies pending redactions and saves to file */}
);
}
-
diff --git a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx
index 7f669ce1e1..7bc4960772 100644
--- a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx
+++ b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx
@@ -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,
},
{
diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
index 33a6a80f17..5d9885232b 100644
--- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
+++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx
@@ -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 && (
- {
- 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;
- }}
- />
- )}
);
};
diff --git a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx
index 1e20542a6d..290963f5b2 100644
--- a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx
+++ b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx
@@ -31,7 +31,7 @@ export function useViewerRightRailButtons(
const [isPanning, setIsPanning] = useState(() => 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) {
diff --git a/frontend/src/core/contexts/NavigationContext.tsx b/frontend/src/core/contexts/NavigationContext.tsx
index 7485821b34..17c8ca7b26 100644
--- a/frontend/src/core/contexts/NavigationContext.tsx
+++ b/frontend/src/core/contexts/NavigationContext.tsx
@@ -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;
+ onExportAndContinue?: () => Promise;
+ onDiscardAndContinue?: () => Promise;
+}
+
// 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;
}
// 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(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,
};
};
diff --git a/frontend/src/core/tools/Redact.tsx b/frontend/src/core/tools/Redact.tsx
index 86bf3f9ba3..b314b6aedc 100644
--- a/frontend/src/core/tools/Redact.tsx
+++ b/frontend/src/core/tools/Redact.tsx
@@ -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}
/>
),