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