From 5485b06735636b86f8128f62febfa0829df43620 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 3 Dec 2025 14:25:21 +0000 Subject: [PATCH 1/8] Added bones for manual redaction, quickly saving so I can work on the text editor --- frontend/package-lock.json | 20 ++ frontend/package.json | 1 + frontend/src/core/components/AppProviders.tsx | 17 +- .../rightRail/ViewerAnnotationControls.tsx | 103 ++++++++- .../tools/redact/ManualRedactionControls.tsx | 201 ++++++++++++++++++ .../tools/redact/RedactModeSelector.tsx | 5 +- .../core/components/viewer/EmbedPdfViewer.tsx | 41 +++- .../core/components/viewer/LocalEmbedPDF.tsx | 35 ++- .../components/viewer/RedactionAPIBridge.tsx | 50 +++++ .../viewer/RedactionPendingTracker.tsx | 59 +++++ .../viewer/RedactionSelectionMenu.tsx | 80 +++++++ .../src/core/contexts/NavigationContext.tsx | 48 +++-- .../src/core/contexts/RedactionContext.tsx | 185 ++++++++++++++++ .../hooks/tools/redact/useRedactOperation.ts | 10 +- .../hooks/tools/redact/useRedactParameters.ts | 9 +- frontend/src/core/tools/Redact.tsx | 62 +++++- 16 files changed, 861 insertions(+), 65 deletions(-) create mode 100644 frontend/src/core/components/tools/redact/ManualRedactionControls.tsx create mode 100644 frontend/src/core/components/viewer/RedactionAPIBridge.tsx create mode 100644 frontend/src/core/components/viewer/RedactionPendingTracker.tsx create mode 100644 frontend/src/core/components/viewer/RedactionSelectionMenu.tsx create mode 100644 frontend/src/core/contexts/RedactionContext.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e1f885e6..2db56bbfd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "@embedpdf/plugin-loader": "^1.5.0", "@embedpdf/plugin-pan": "^1.5.0", "@embedpdf/plugin-print": "^1.5.0", + "@embedpdf/plugin-redaction": "^1.5.0", "@embedpdf/plugin-render": "^1.5.0", "@embedpdf/plugin-rotate": "^1.5.0", "@embedpdf/plugin-scroll": "^1.5.0", @@ -759,6 +760,25 @@ "vue": ">=3.2.0" } }, + "node_modules/@embedpdf/plugin-redaction": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-1.5.0.tgz", + "integrity": "sha512-txiukr5UKAGvJzl6dVBmmIT1v3r/t4e2qYm1hqU2faGgNCa2dwk79x9mDBlvWwxlJXCDFuFE+7Ps9/nU6qmU2w==", + "license": "MIT", + "dependencies": { + "@embedpdf/models": "1.5.0", + "@embedpdf/utils": "1.5.0" + }, + "peerDependencies": { + "@embedpdf/core": "1.5.0", + "@embedpdf/plugin-interaction-manager": "1.5.0", + "@embedpdf/plugin-selection": "1.5.0", + "preact": "^10.26.4", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "vue": ">=3.2.0" + } + }, "node_modules/@embedpdf/plugin-render": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5489f6b46..25ae7ac8a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@embedpdf/plugin-loader": "^1.5.0", "@embedpdf/plugin-pan": "^1.5.0", "@embedpdf/plugin-print": "^1.5.0", + "@embedpdf/plugin-redaction": "^1.5.0", "@embedpdf/plugin-render": "^1.5.0", "@embedpdf/plugin-rotate": "^1.5.0", "@embedpdf/plugin-scroll": "^1.5.0", diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 7e47d00e0..0ad287559 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -12,6 +12,7 @@ import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from import { RightRailProvider } from "@app/contexts/RightRailContext"; import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; +import { RedactionProvider } from "@app/contexts/RedactionContext"; import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext"; import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext"; import { PageEditorProvider } from "@app/contexts/PageEditorContext"; @@ -93,13 +94,15 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - {children} - - - + + + + + {children} + + + + diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index cd95b90b9..822c3c83d 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -10,9 +10,10 @@ import { useFileState, useFileContext } from '@app/contexts/FileContext'; import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils'; import { createProcessedFile } from '@app/contexts/file/fileActions'; import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; -import { useNavigationState } from '@app/contexts/NavigationContext'; +import { useNavigationState, useNavigationGuard, useNavigationActions } from '@app/contexts/NavigationContext'; import { useSidebarContext } from '@app/contexts/SidebarContext'; import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; +import { useRedactionMode, useRedaction } from '@app/contexts/RedactionContext'; interface ViewerAnnotationControlsProps { currentView: string; @@ -38,9 +39,16 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const { actions: fileActions } = useFileContext(); const activeFiles = selectors.getFiles(); - // Check if we're in sign mode + // Check if we're in sign mode or redaction mode const { selectedTool } = useNavigationState(); + const { actions: navActions } = useNavigationActions(); const isSignMode = selectedTool === 'sign'; + const isRedactMode = selectedTool === 'redact'; + + // Get redaction pending state and navigation guard + const { pendingCount: redactionPendingCount, isRedacting } = useRedactionMode(); + const { requestNavigation } = useNavigationGuard(); + const { setRedactionMode, activateTextSelection } = useRedaction(); // Turn off annotation mode when switching away from viewer useEffect(() => { @@ -54,8 +62,60 @@ export default function ViewerAnnotationControls({ currentView, disabled = false return null; } + // Handle redaction mode toggle + const handleRedactionToggle = () => { + if (isRedactMode) { + // If already in redact mode, toggle annotation mode off and show redaction layer + if (viewerContext?.isAnnotationMode) { + viewerContext.setAnnotationMode(false); + // Deactivate any active annotation tools + if (signatureApiRef?.current) { + try { + signatureApiRef.current.deactivateTools(); + } catch (error) { + console.log('Unable to deactivate annotation tools:', error); + } + } + // Activate redaction tool + setTimeout(() => { + activateTextSelection(); + }, 100); + } else { + // Exit redaction mode - go back to default view + navActions.handleToolSelect('allTools'); + setRedactionMode(false); + } + } else { + // Enter redaction mode - select redact tool with manual mode + navActions.handleToolSelect('redact'); + setRedactionMode(true); + // Activate text selection mode after a short delay + setTimeout(() => { + activateTextSelection(); + }, 200); + } + }; + return ( <> + {/* Redaction Mode Toggle */} + + + + + + {/* Annotation Visibility Toggle */} @@ -140,14 +200,35 @@ export default function ViewerAnnotationControls({ currentView, disabled = false radius="md" className="right-rail-icon" onClick={() => { - viewerContext?.toggleAnnotationMode(); - // Activate ink drawing tool when entering annotation mode - if (signatureApiRef?.current && currentView === 'viewer') { - try { - signatureApiRef.current.activateDrawMode(); - signatureApiRef.current.updateDrawSettings(selectedColor, 2); - } catch (error) { - console.log('Signature API not ready:', error); + const activateDrawMode = () => { + // Use setTimeout to ensure this runs after any state updates from applyChanges + setTimeout(() => { + viewerContext?.setAnnotationMode(true); + // Activate ink drawing tool when entering annotation mode + if (signatureApiRef?.current && currentView === 'viewer') { + try { + signatureApiRef.current.activateDrawMode(); + signatureApiRef.current.updateDrawSettings(selectedColor, 2); + } catch (error) { + console.log('Signature API not ready:', error); + } + } + }, 150); + }; + + // If in redaction mode with pending redactions, show warning modal + if (isRedactMode && redactionPendingCount > 0) { + requestNavigation(activateDrawMode); + } else { + // Direct activation - no need for delay + viewerContext?.toggleAnnotationMode(); + if (signatureApiRef?.current && currentView === 'viewer') { + try { + signatureApiRef.current.activateDrawMode(); + signatureApiRef.current.updateDrawSettings(selectedColor, 2); + } catch (error) { + console.log('Signature API not ready:', error); + } } } }} diff --git a/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx new file mode 100644 index 000000000..f944c34e3 --- /dev/null +++ b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx @@ -0,0 +1,201 @@ +import { useTranslation } from 'react-i18next'; +import { useEffect, useRef } from 'react'; +import { Button, Stack, Text, Badge, Group, Divider } from '@mantine/core'; +import HighlightAltIcon from '@mui/icons-material/HighlightAlt'; +import CropFreeIcon from '@mui/icons-material/CropFree'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { useRedaction, useRedactionMode } from '@app/contexts/RedactionContext'; +import { useViewer } from '@app/contexts/ViewerContext'; +import { useSignature } from '@app/contexts/SignatureContext'; + +interface ManualRedactionControlsProps { + disabled?: boolean; +} + +/** + * ManualRedactionControls provides UI for manual PDF redaction in the tool panel. + * Displays controls for marking text/areas for redaction and applying them. + * Uses our RedactionContext which bridges to the EmbedPDF API. + */ +export default function ManualRedactionControls({ disabled = false }: ManualRedactionControlsProps) { + const { t } = useTranslation(); + + // Use our RedactionContext which bridges to EmbedPDF + const { activateTextSelection, activateMarquee, commitAllPending, redactionApiRef } = useRedaction(); + const { pendingCount, activeType, isRedacting } = useRedactionMode(); + + // Get viewer context to manage annotation mode + const { isAnnotationMode, setAnnotationMode } = useViewer(); + + // Get signature context to deactivate annotation tools when switching to redaction + const { signatureApiRef } = useSignature(); + + // Check which tool is active based on activeType + const isSelectionActive = activeType === 'redactSelection'; + const isMarqueeActive = activeType === 'marqueeRedact'; + + // Track if we've auto-activated + const hasAutoActivated = useRef(false); + + // Auto-activate selection mode when the API becomes available + // This ensures at least one tool is selected when entering manual redaction mode + useEffect(() => { + if (redactionApiRef.current && !disabled && !isRedacting && !hasAutoActivated.current) { + hasAutoActivated.current = true; + // Small delay to ensure EmbedPDF is fully ready + const timer = setTimeout(() => { + // Deactivate annotation mode to show redaction layer + setAnnotationMode(false); + activateTextSelection(); + }, 100); + return () => clearTimeout(timer); + } + }, [redactionApiRef.current, disabled, isRedacting, activateTextSelection, setAnnotationMode]); + + // Reset auto-activation flag when disabled changes + useEffect(() => { + if (disabled) { + hasAutoActivated.current = false; + } + }, [disabled]); + + const handleSelectionClick = () => { + // 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); + } + } + } + + if (isSelectionActive && !isAnnotationMode) { + // If already active and not coming from annotation mode, switch to marquee + activateMarquee(); + } else { + activateTextSelection(); + } + }; + + const handleMarqueeClick = () => { + // 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); + } + } + } + + if (isMarqueeActive && !isAnnotationMode) { + // If already active and not coming from annotation mode, switch to selection + activateTextSelection(); + } else { + activateMarquee(); + } + }; + + const handleApplyAll = () => { + commitAllPending(); + }; + + // Check if API is available + const isApiReady = redactionApiRef.current !== null; + + return ( + <> + + + + {t('redact.manual.title', 'Redaction Tools')} + + + + {t('redact.manual.instructions', 'Select text or draw areas on the PDF to mark content for redaction.')} + + + + {/* Mark Text Selection Tool */} + + + {/* Mark Area (Marquee) Tool */} + + + + + + {/* Pending Count and Apply Button */} + + + + {t('redact.manual.pendingLabel', 'Pending:')} + + 0 ? 'red' : 'gray'} + variant="filled" + size="lg" + > + {pendingCount} + + + + + + + {pendingCount === 0 && ( + + {t('redact.manual.noMarks', 'No redaction marks. Use the tools above to mark content for redaction.')} + + )} + + + ); +} + diff --git a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx index 47f15da66..f4a071af5 100644 --- a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx +++ b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx @@ -6,9 +6,10 @@ interface RedactModeSelectorProps { mode: RedactMode; onModeChange: (mode: RedactMode) => void; disabled?: boolean; + hasFiles?: boolean; } -export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) { +export default function RedactModeSelector({ mode, onModeChange, disabled, hasFiles = false }: RedactModeSelectorProps) { const { t } = useTranslation(); return ( @@ -24,7 +25,7 @@ export default function RedactModeSelector({ mode, onModeChange, disabled }: Red { value: 'manual' as const, label: t('redact.modeSelector.manual', 'Manual'), - disabled: true, // Keep manual mode disabled until implemented + disabled: !hasFiles, // Enable manual mode when files are present }, ]} disabled={disabled} diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index 9ca8bac0a..97838c33c 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -11,6 +11,8 @@ import { ThumbnailSidebar } from '@app/components/viewer/ThumbnailSidebar'; import { BookmarkSidebar } from '@app/components/viewer/BookmarkSidebar'; import { useNavigationGuard, useNavigationState } from '@app/contexts/NavigationContext'; import { useSignature } from '@app/contexts/SignatureContext'; +import { useRedaction } from '@app/contexts/RedactionContext'; +import type { RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker'; import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers'; import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; import { isStirlingFile } from '@app/types/fileContext'; @@ -70,6 +72,12 @@ const EmbedPdfViewerContent = ({ // Get signature context const { signatureApiRef, historyApiRef, signatureConfig, isPlacementMode } = useSignature(); + // Get redaction context + const { isRedactionMode } = useRedaction(); + + // Ref for redaction pending tracker API + const redactionTrackerRef = useRef(null); + // Get current file from FileContext const { selectors, state } = useFileState(); const { actions } = useFileActions(); @@ -80,13 +88,20 @@ const EmbedPdfViewerContent = ({ // Navigation guard for unsaved changes const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard(); - // Check if we're in signature mode OR viewer annotation mode + // Check if we're in signature mode OR viewer annotation mode OR redaction mode const { selectedTool } = useNavigationState(); // Tools that use the stamp/signature placement system with hover preview const isSignatureMode = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage'; + // Check if we're in manual redaction mode + const isManualRedactMode = selectedTool === 'redact'; // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations - const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible; + // When in manual redaction mode, annotation mode takes priority if active (user clicked draw tool) + const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || (isAnnotationsVisible && !isManualRedactMode); + + // Enable redaction when the redact tool is selected and annotation mode is NOT active + // This allows switching between redaction and annotation tools while redact is the selected tool + const shouldEnableRedaction = (isManualRedactMode || isRedactionMode) && !isAnnotationMode; const isPlacementOverlayActive = Boolean( isSignatureMode && shouldEnableAnnotations && isPlacementMode && signatureConfig ); @@ -218,11 +233,14 @@ const EmbedPdfViewerContent = ({ const checkForChanges = () => { // Check for annotation changes via history const hasAnnotationChanges = historyApiRef.current?.canUndo() || false; + // Check for pending redactions + const hasPendingRedactions = (redactionTrackerRef.current?.getPendingCount() ?? 0) > 0; console.log('[Viewer] Checking for unsaved changes:', { - hasAnnotationChanges + hasAnnotationChanges, + hasPendingRedactions }); - return hasAnnotationChanges; + return hasAnnotationChanges || hasPendingRedactions; }; console.log('[Viewer] Registering unsaved changes checker'); @@ -234,12 +252,20 @@ const EmbedPdfViewerContent = ({ }; }, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]); - // Apply changes - save annotations to new file version + // Apply changes - save annotations and redactions to new file version const applyChanges = useCallback(async () => { if (!currentFile || activeFileIds.length === 0) return; try { - console.log('[Viewer] Applying changes - exporting PDF with annotations'); + console.log('[Viewer] Applying changes - exporting PDF with annotations/redactions'); + + // Step 0: Commit any pending redactions before export + if (redactionTrackerRef.current?.getPendingCount() ?? 0 > 0) { + console.log('[Viewer] Committing pending redactions before export'); + redactionTrackerRef.current?.commitAllPending(); + // Give a small delay for the commit to process + await new Promise(resolve => setTimeout(resolve, 100)); + } // Step 1: Export PDF with annotations using EmbedPDF const arrayBuffer = await exportActions.saveAsCopy(); @@ -322,8 +348,11 @@ const EmbedPdfViewerContent = ({ file={effectiveFile.file} url={effectiveFile.url} enableAnnotations={shouldEnableAnnotations} + enableRedaction={shouldEnableRedaction} + isManualRedactionMode={isManualRedactMode} signatureApiRef={signatureApiRef as React.RefObject} historyApiRef={historyApiRef as React.RefObject} + redactionTrackerRef={redactionTrackerRef as React.RefObject} onSignatureAdded={() => { // Handle signature added - for debugging, enable console logs as needed // Future: Handle signature completion diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index 3e5fa5f1c..0a982ab42 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -21,11 +21,10 @@ import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react'; import { ExportPluginPackage } from '@embedpdf/plugin-export/react'; import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark'; import { PrintPluginPackage } from '@embedpdf/plugin-print/react'; - -// Import annotation plugins import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react'; import { PdfAnnotationSubtype } from '@embedpdf/models'; +import { RedactionPluginPackage, RedactionLayer } from '@embedpdf/plugin-redaction/react'; import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer'; import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge'; import ToolLoadingFallback from '@app/components/tools/ToolLoadingFallback'; @@ -46,17 +45,24 @@ import { PrintAPIBridge } from '@app/components/viewer/PrintAPIBridge'; import { isPdfFile } from '@app/utils/fileUtils'; import { useTranslation } from 'react-i18next'; import { LinkLayer } from '@app/components/viewer/LinkLayer'; +import { RedactionSelectionMenu } from '@app/components/viewer/RedactionSelectionMenu'; +import { RedactionPendingTracker, RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker'; +import { RedactionAPIBridge } from '@app/components/viewer/RedactionAPIBridge'; interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; enableAnnotations?: boolean; + enableRedaction?: boolean; + /** When true, RedactionAPIBridge is rendered even if enableRedaction is false (for switching between annotation/redaction) */ + isManualRedactionMode?: boolean; onSignatureAdded?: (annotation: any) => void; signatureApiRef?: React.RefObject; historyApiRef?: React.RefObject; + redactionTrackerRef?: React.RefObject; } -export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { +export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableRedaction = false, isManualRedactionMode = false, onSignatureAdded, signatureApiRef, historyApiRef, redactionTrackerRef }: LocalEmbedPDFProps) { const { t } = useTranslation(); const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); @@ -121,6 +127,11 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur selectAfterCreate: true, }), + // Register redaction plugin (depends on InteractionManager, Selection) + createPluginRegistration(RedactionPluginPackage, { + drawBlackBoxes: true, // Draw black boxes over redacted content + }), + // Register pan plugin (depends on Viewport, InteractionManager) createPluginRegistration(PanPluginPackage, { defaultMode: 'mobile', // Try mobile mode which might be more permissive @@ -305,8 +316,12 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur - {enableAnnotations && } - {enableAnnotations && } + {/* Always render SignatureAPIBridge so annotation tools (draw) can be activated even when starting in redaction mode */} + + {(enableAnnotations || enableRedaction || isManualRedactionMode) && } + {/* Always render RedactionAPIBridge when in manual redaction mode so buttons can switch from annotation mode */} + {(enableRedaction || isManualRedactionMode) && } + {(enableRedaction || isManualRedactionMode) && } @@ -373,6 +388,16 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur selectionOutlineColor="#007ACC" /> )} + + {/* Redaction layer for marking areas to redact (only when enabled) */} + {enableRedaction && ( + } + /> + )} diff --git a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx new file mode 100644 index 000000000..f930d1281 --- /dev/null +++ b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx @@ -0,0 +1,50 @@ +import { useEffect, useImperativeHandle } from 'react'; +import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; +import { useRedaction, RedactionAPI } from '@app/contexts/RedactionContext'; + +/** + * RedactionAPIBridge connects the EmbedPDF redaction plugin to our RedactionContext. + * It must be rendered inside the EmbedPDF context to access the plugin API. + * + * It does two things: + * 1. Syncs EmbedPDF state (pendingCount, activeType, isRedacting) to our context + * 2. Exposes the EmbedPDF API through our context's ref so outside components can call it + */ +export function RedactionAPIBridge() { + const { state, provides } = useEmbedPdfRedaction(); + const { + redactionApiRef, + setPendingCount, + setActiveType, + setIsRedacting, + setRedactionsApplied + } = useRedaction(); + + // Sync EmbedPDF state to our context + useEffect(() => { + if (state) { + setPendingCount(state.pendingCount ?? 0); + setActiveType(state.activeType ?? null); + setIsRedacting(state.isRedacting ?? false); + } + }, [state?.pendingCount, state?.activeType, state?.isRedacting, setPendingCount, setActiveType, setIsRedacting]); + + // Expose the EmbedPDF API through our context's ref + useImperativeHandle(redactionApiRef, () => ({ + toggleRedactSelection: () => { + provides?.toggleRedactSelection(); + }, + toggleMarqueeRedact: () => { + provides?.toggleMarqueeRedact(); + }, + commitAllPending: () => { + provides?.commitAllPending(); + setRedactionsApplied(true); + }, + getActiveType: () => state?.activeType ?? null, + getPendingCount: () => state?.pendingCount ?? 0, + }), [provides, state, setRedactionsApplied]); + + return null; +} + diff --git a/frontend/src/core/components/viewer/RedactionPendingTracker.tsx b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx new file mode 100644 index 000000000..8efbde6f5 --- /dev/null +++ b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react'; +import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; +import { useNavigationGuard } from '@app/contexts/NavigationContext'; + +export interface RedactionPendingTrackerAPI { + commitAllPending: () => void; + getPendingCount: () => number; +} + +/** + * RedactionPendingTracker monitors pending redactions and integrates with + * the navigation guard to warn users about unsaved changes. + * Must be rendered inside the EmbedPDF context. + */ +export const RedactionPendingTracker = forwardRef( + function RedactionPendingTracker(_, ref) { + const { state, provides } = useEmbedPdfRedaction(); + const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, setHasUnsavedChanges } = useNavigationGuard(); + + const pendingCountRef = useRef(0); + + // Expose API through ref + useImperativeHandle(ref, () => ({ + commitAllPending: () => { + if (provides?.commitAllPending) { + provides.commitAllPending(); + } + }, + getPendingCount: () => pendingCountRef.current, + }), [provides]); + + // Update ref when pending count changes + useEffect(() => { + pendingCountRef.current = state?.pendingCount ?? 0; + + // Also update the hasUnsavedChanges state + if (pendingCountRef.current > 0) { + setHasUnsavedChanges(true); + } + }, [state?.pendingCount, setHasUnsavedChanges]); + + // Register checker for pending redactions + useEffect(() => { + const checkForPendingRedactions = () => { + const hasPending = pendingCountRef.current > 0; + return hasPending; + }; + + registerUnsavedChangesChecker(checkForPendingRedactions); + + return () => { + unregisterUnsavedChangesChecker(); + }; + }, [registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]); + + return null; + } +); + diff --git a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx new file mode 100644 index 000000000..5ef127b09 --- /dev/null +++ b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx @@ -0,0 +1,80 @@ +import { useRedaction as useEmbedPdfRedaction, SelectionMenuProps } from '@embedpdf/plugin-redaction/react'; +import { ActionIcon, Tooltip, Button, Group } from '@mantine/core'; +import DeleteIcon from '@mui/icons-material/Delete'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + +/** + * Custom menu component that appears when a pending redaction mark is selected. + * Allows users to remove or apply individual pending marks. + */ +export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: SelectionMenuProps) { + const { provides } = useEmbedPdfRedaction(); + + if (!selected || !item) return null; + + const handleRemove = () => { + if (provides?.removePending) { + provides.removePending(item.page, item.id); + } + }; + + const handleApply = () => { + if (provides?.commitPending) { + provides.commitPending(item.page, item.id); + } + }; + + return ( +
+
+ + + + + + + + + + + +
+
+ ); +} + diff --git a/frontend/src/core/contexts/NavigationContext.tsx b/frontend/src/core/contexts/NavigationContext.tsx index 500a6db5e..33645e3e5 100644 --- a/frontend/src/core/contexts/NavigationContext.tsx +++ b/frontend/src/core/contexts/NavigationContext.tsx @@ -230,25 +230,37 @@ export const NavigationProvider: React.FC<{ }, []), handleToolSelect: useCallback((toolId: string) => { - if (toolId === 'allTools') { - dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); - return; + const performToolSelect = () => { + if (toolId === 'allTools') { + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); + return; + } + + if (toolId === 'read' || toolId === 'view-pdf') { + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } }); + return; + } + + // Look up the tool in the registry to get its proper workbench + const tool = isValidToolId(toolId)? toolRegistry[toolId] : null; + const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench(); + + // Validate toolId and convert to ToolId type + const validToolId = isValidToolId(toolId) ? toolId : null; + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } }); + }; + + // Check for unsaved changes using registered checker or state + const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges; + + // If switching away from current tool and have unsaved changes, show warning + if (hasUnsavedChanges && state.selectedTool && state.selectedTool !== toolId) { + dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performToolSelect } }); + dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); + } else { + performToolSelect(); } - - if (toolId === 'read' || toolId === 'view-pdf') { - dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } }); - return; - } - - // Look up the tool in the registry to get its proper workbench - - const tool = isValidToolId(toolId)? toolRegistry[toolId] : null; - const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench(); - - // Validate toolId and convert to ToolId type - const validToolId = isValidToolId(toolId) ? toolId : null; - dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } }); - }, [toolRegistry]) + }, [toolRegistry, state.hasUnsavedChanges, state.selectedTool]) }; const stateValue: NavigationContextStateValue = { diff --git a/frontend/src/core/contexts/RedactionContext.tsx b/frontend/src/core/contexts/RedactionContext.tsx new file mode 100644 index 000000000..1d6bb6eda --- /dev/null +++ b/frontend/src/core/contexts/RedactionContext.tsx @@ -0,0 +1,185 @@ +import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react'; +import { RedactParameters } from '@app/hooks/tools/redact/useRedactParameters'; + +/** + * API interface that the EmbedPDF bridge will implement + */ +export interface RedactionAPI { + toggleRedactSelection: () => void; + toggleMarqueeRedact: () => void; + commitAllPending: () => void; + getActiveType: () => 'redactSelection' | 'marqueeRedact' | null; + getPendingCount: () => number; +} + +/** + * State interface for redaction operations + */ +interface RedactionState { + // Current redaction configuration from the tool + redactionConfig: RedactParameters | null; + // Whether we're in redaction mode (viewer should show redaction layer) + isRedactionMode: boolean; + // Whether redactions have been applied + redactionsApplied: boolean; + // Synced state from EmbedPDF + pendingCount: number; + activeType: 'redactSelection' | 'marqueeRedact' | null; + isRedacting: boolean; +} + +/** + * Actions interface for redaction operations + */ +interface RedactionActions { + setRedactionConfig: (config: RedactParameters | null) => void; + setRedactionMode: (enabled: boolean) => void; + setRedactionsApplied: (applied: boolean) => void; + // Synced state setters (called from inside EmbedPDF) + setPendingCount: (count: number) => void; + setActiveType: (type: 'redactSelection' | 'marqueeRedact' | null) => void; + setIsRedacting: (isRedacting: boolean) => void; + // Actions that call through to EmbedPDF API + activateTextSelection: () => void; + activateMarquee: () => void; + commitAllPending: () => void; +} + +/** + * Combined context interface + */ +interface RedactionContextValue extends RedactionState, RedactionActions { + // Ref that the bridge component will populate + redactionApiRef: React.MutableRefObject; +} + +// Create context +const RedactionContext = createContext(undefined); + +// Initial state +const initialState: RedactionState = { + redactionConfig: null, + isRedactionMode: false, + redactionsApplied: false, + pendingCount: 0, + activeType: null, + isRedacting: false, +}; + +/** + * Provider component for redaction functionality + * Bridges between the tool panel (outside EmbedPDF) and the viewer (inside EmbedPDF) + */ +export const RedactionProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [state, setState] = useState(initialState); + const redactionApiRef = useRef(null); + + // Actions for tool configuration + const setRedactionConfig = useCallback((config: RedactParameters | null) => { + setState(prev => ({ + ...prev, + redactionConfig: config, + })); + }, []); + + const setRedactionMode = useCallback((enabled: boolean) => { + setState(prev => ({ + ...prev, + isRedactionMode: enabled, + })); + }, []); + + const setRedactionsApplied = useCallback((applied: boolean) => { + setState(prev => ({ + ...prev, + redactionsApplied: applied, + })); + }, []); + + // Synced state setters (called from bridge inside EmbedPDF) + const setPendingCount = useCallback((count: number) => { + setState(prev => ({ + ...prev, + pendingCount: count, + })); + }, []); + + const setActiveType = useCallback((type: 'redactSelection' | 'marqueeRedact' | null) => { + setState(prev => ({ + ...prev, + activeType: type, + })); + }, []); + + const setIsRedacting = useCallback((isRedacting: boolean) => { + setState(prev => ({ + ...prev, + isRedacting, + })); + }, []); + + // Actions that call through to EmbedPDF API + const activateTextSelection = useCallback(() => { + if (redactionApiRef.current) { + redactionApiRef.current.toggleRedactSelection(); + } + }, []); + + const activateMarquee = useCallback(() => { + if (redactionApiRef.current) { + redactionApiRef.current.toggleMarqueeRedact(); + } + }, []); + + const commitAllPending = useCallback(() => { + if (redactionApiRef.current) { + redactionApiRef.current.commitAllPending(); + } + }, []); + + const contextValue: RedactionContextValue = { + ...state, + redactionApiRef, + setRedactionConfig, + setRedactionMode, + setRedactionsApplied, + setPendingCount, + setActiveType, + setIsRedacting, + activateTextSelection, + activateMarquee, + commitAllPending, + }; + + return ( + + {children} + + ); +}; + +/** + * Hook to use redaction context + */ +export const useRedaction = (): RedactionContextValue => { + const context = useContext(RedactionContext); + if (context === undefined) { + throw new Error('useRedaction must be used within a RedactionProvider'); + } + return context; +}; + +/** + * Hook for components that need to check if redaction mode is active + */ +export const useRedactionMode = () => { + const context = useContext(RedactionContext); + return { + isRedactionModeActive: context?.isRedactionMode || false, + hasRedactionConfig: context?.redactionConfig !== null, + pendingCount: context?.pendingCount || 0, + activeType: context?.activeType || null, + isRedacting: context?.isRedacting || false, + }; +}; + diff --git a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts index bf2a05121..fd3325247 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts @@ -16,10 +16,8 @@ export const buildRedactFormData = (parameters: RedactParameters, file: File): F formData.append("redactColor", parameters.redactColor.replace('#', '')); formData.append("customPadding", parameters.customPadding.toString()); formData.append("convertPDFToImage", parameters.convertPDFToImage.toString()); - } else { - // Manual mode parameters would go here when implemented - throw new Error('Manual redaction not yet implemented'); } + // Note: Manual mode is handled client-side via EmbedPDF, no formData needed return formData; }; @@ -32,10 +30,10 @@ export const redactOperationConfig = { endpoint: (parameters: RedactParameters) => { if (parameters.mode === 'automatic') { return '/api/v1/security/auto-redact'; - } else { - // Manual redaction endpoint would go here when implemented - throw new Error('Manual redaction not yet implemented'); } + // Manual redaction is handled client-side via EmbedPDF + // Return null to indicate no server endpoint is needed + return null; }, defaultParameters, } as const; diff --git a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts index f29f56f96..890f85721 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts @@ -34,15 +34,16 @@ export const useRedactParameters = (): RedactParametersHook => { if (params.mode === 'automatic') { return '/api/v1/security/auto-redact'; } - // Manual redaction endpoint would go here when implemented - throw new Error('Manual redaction not yet implemented'); + // Manual redaction is handled client-side via EmbedPDF + // Return null or a placeholder since we don't call an endpoint + return null; }, validateFn: (params) => { if (params.mode === 'automatic') { return params.wordsToRedact.length > 0 && params.wordsToRedact.some(word => word.trim().length > 0); } - // Manual mode validation would go here when implemented - return false; + // Manual mode is always valid since redaction is done in the viewer + return true; } }); }; diff --git a/frontend/src/core/tools/Redact.tsx b/frontend/src/core/tools/Redact.tsx index 27604da0b..2172b008a 100644 --- a/frontend/src/core/tools/Redact.tsx +++ b/frontend/src/core/tools/Redact.tsx @@ -1,14 +1,17 @@ import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; import RedactModeSelector from "@app/components/tools/redact/RedactModeSelector"; -import { useRedactParameters } from "@app/hooks/tools/redact/useRedactParameters"; +import { useRedactParameters, RedactMode } from "@app/hooks/tools/redact/useRedactParameters"; import { useRedactOperation } from "@app/hooks/tools/redact/useRedactOperation"; import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useRedactModeTips, useRedactWordsTips, useRedactAdvancedTips } from "@app/components/tooltips/useRedactTips"; import RedactAdvancedSettings from "@app/components/tools/redact/RedactAdvancedSettings"; import WordsToRedactInput from "@app/components/tools/redact/WordsToRedactInput"; +import ManualRedactionControls from "@app/components/tools/redact/ManualRedactionControls"; +import { useNavigationActions } from "@app/contexts/NavigationContext"; +import { useRedaction } from "@app/contexts/RedactionContext"; const Redact = (props: BaseToolProps) => { const { t } = useTranslation(); @@ -18,6 +21,11 @@ const Redact = (props: BaseToolProps) => { const [wordsCollapsed, setWordsCollapsed] = useState(false); const [advancedCollapsed, setAdvancedCollapsed] = useState(true); + // Navigation and redaction context + const { actions: navActions } = useNavigationActions(); + const { setRedactionConfig, setRedactionMode } = useRedaction(); + const hasOpenedViewer = useRef(false); + const base = useBaseTool( 'redact', useRedactParameters, @@ -30,9 +38,40 @@ const Redact = (props: BaseToolProps) => { const wordsTips = useRedactWordsTips(); const advancedTips = useRedactAdvancedTips(); + // Handle mode change - navigate to viewer when manual mode is selected + const handleModeChange = (mode: RedactMode) => { + base.params.updateParameter('mode', mode); + + if (mode === 'manual' && base.hasFiles) { + // Set redaction config and navigate to viewer + setRedactionConfig(base.params.parameters); + setRedactionMode(true); + navActions.setWorkbench('viewer'); + hasOpenedViewer.current = true; + } + }; + + // When files are added and in manual mode, navigate to viewer + useEffect(() => { + if (base.params.parameters.mode === 'manual' && base.hasFiles && !hasOpenedViewer.current) { + setRedactionConfig(base.params.parameters); + setRedactionMode(true); + navActions.setWorkbench('viewer'); + hasOpenedViewer.current = true; + } + }, [base.hasFiles, base.params.parameters, navActions, setRedactionConfig, setRedactionMode]); + + // Reset viewer flag when mode changes back to automatic + useEffect(() => { + if (base.params.parameters.mode === 'automatic') { + hasOpenedViewer.current = false; + setRedactionMode(false); + } + }, [base.params.parameters.mode, setRedactionMode]); + const isExecuteDisabled = () => { if (base.params.parameters.mode === 'manual') { - return true; // Manual mode not implemented yet + return true; // Manual mode uses viewer, not execute button } return !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled; }; @@ -54,8 +93,9 @@ const Redact = (props: BaseToolProps) => { content: ( base.params.updateParameter('mode', mode)} + onModeChange={handleModeChange} disabled={base.endpointLoading} + hasFiles={base.hasFiles} /> ), } @@ -88,12 +128,22 @@ const Redact = (props: BaseToolProps) => { }, ); } else if (base.params.parameters.mode === 'manual') { - // Manual mode steps would go here when implemented + // Manual mode - show redaction controls + steps.push({ + title: t("redact.manual.controlsTitle", "Manual Redaction Controls"), + isCollapsed: false, + onCollapsedClick: () => {}, + tooltip: [], + content: , + }); } return steps; }; + // Hide execute button in manual mode (redactions applied via controls) + const isManualMode = base.params.parameters.mode === 'manual'; + return createToolFlow({ files: { selectedFiles: base.selectedFiles, @@ -102,7 +152,7 @@ const Redact = (props: BaseToolProps) => { steps: buildSteps(), executeButton: { text: t("redact.submit", "Redact"), - isVisible: !base.hasResults, + isVisible: !base.hasResults && !isManualMode, loadingText: t("loading"), onClick: base.handleExecute, disabled: isExecuteDisabled(), From 659d1deba888e8c4a81dae72d0178a2d39835f84 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Dec 2025 03:56:06 +0000 Subject: [PATCH 2/8] finished --- .../public/locales/en-GB/translation.toml | 32 ++++ .../shared/NavigationWarningModal.tsx | 48 ++++-- .../rightRail/ViewerAnnotationControls.tsx | 81 ++++++++- .../tools/redact/ManualRedactionControls.tsx | 49 +++--- .../core/components/tooltips/useRedactTips.ts | 29 ++++ .../core/components/viewer/EmbedPdfViewer.tsx | 40 +++-- .../components/viewer/RedactionAPIBridge.tsx | 2 +- .../viewer/RedactionPendingTracker.tsx | 30 +--- .../viewer/RedactionSelectionMenu.tsx | 158 ++++++++++++++---- .../src/core/contexts/RedactionContext.tsx | 11 +- .../tools/redact/useRedactOperation.test.ts | 5 +- .../hooks/tools/redact/useRedactOperation.ts | 10 +- .../tools/redact/useRedactParameters.test.ts | 4 +- .../hooks/tools/redact/useRedactParameters.ts | 7 +- frontend/src/core/tools/Redact.tsx | 19 ++- 15 files changed, 401 insertions(+), 124 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a425ad3d5..41a455ac1 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -1,8 +1,11 @@ unsavedChanges = "You have unsaved changes to your PDF." +pendingRedactionsTitle = "Unapplied Redactions" +pendingRedactions = "You have unapplied redactions that will be lost." areYouSure = "Are you sure you want to leave?" unsavedChangesTitle = "Unsaved Changes" keepWorking = "Keep Working" discardChanges = "Discard & Leave" +discardRedactions = "Discard & Leave" applyAndContinue = "Save & Leave" exportAndContinue = "Export & Continue" cancel = "Cancel" @@ -3140,8 +3143,35 @@ text = "Only match complete words, not partial matches. 'John' won't match 'John title = "Convert to PDF-Image" text = "Converts the PDF to an image-based PDF after redaction. This ensures text behind redaction boxes is completely removed and unrecoverable." +[redact.tooltip.manual.header] +title = "Manual Redaction Controls" + +[redact.tooltip.manual.markText] +title = "Mark Text Tool" +text = "Select text directly on the PDF to mark it for redaction. Click and drag to highlight specific text that you want to redact." + +[redact.tooltip.manual.markArea] +title = "Mark Area Tool" +text = "Draw rectangular areas on the PDF to mark regions for redaction. Useful for redacting images, signatures, or irregular shapes." + +[redact.tooltip.manual.apply] +title = "Apply Redactions" +text = "After marking content, click 'Apply' to permanently redact all marked areas. The pending count shows how many redactions are ready to be applied." +bullet1 = "Mark as many areas as needed before applying" +bullet2 = "All pending redactions are applied at once" +bullet3 = "Redactions cannot be undone after applying" + [redact.manual] +title = "Redaction Tools" +instructions = "Select text or draw areas on the PDF to mark content for redaction." +markText = "Mark Text" +markArea = "Mark Area" +pendingLabel = "Pending:" +applyWarning = "⚠️ Permanent application, cannot be undone and the data underneath will be deleted" +apply = "Apply" +noMarks = "No redaction marks. Use the tools above to mark content for redaction." header = "Manual Redaction" +controlsTitle = "Manual Redaction Controls" textBasedRedaction = "Text-based Redaction" pageBasedRedaction = "Page-based Redaction" convertPDFToImageLabel = "Convert PDF to PDF-Image (Used to remove text behind the box)" @@ -3901,6 +3931,8 @@ toggleAnnotations = "Toggle Annotations Visibility" annotationMode = "Toggle Annotation Mode" print = "Print PDF" draw = "Draw" +redact = "Redact" +exitRedaction = "Exit Redaction Mode" save = "Save" saveChanges = "Save Changes" diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index faff07427..16cb5e890 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -1,9 +1,11 @@ import { Modal, Text, Button, Group, Stack } from "@mantine/core"; -import { useNavigationGuard } from "@app/contexts/NavigationContext"; +import { useNavigationGuard, useNavigationState } from "@app/contexts/NavigationContext"; import { useTranslation } from "react-i18next"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import { useRedactionMode } from "@app/contexts/RedactionContext"; +import FitText from "@app/components/shared/FitText"; interface NavigationWarningModalProps { onApplyAndContinue?: () => Promise; @@ -14,6 +16,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav const { t } = useTranslation(); const { showNavigationWarning, hasUnsavedChanges, cancelNavigation, confirmNavigation, setHasUnsavedChanges } = useNavigationGuard(); + const { selectedTool } = useNavigationState(); + const { pendingCount } = useRedactionMode(); + + // Check if we're in redact mode with pending redactions + const isRedactMode = selectedTool === 'redact'; + const hasPendingRedactions = pendingCount > 0; const handleKeepWorking = () => { cancelNavigation(); @@ -39,7 +47,7 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav setHasUnsavedChanges(false); confirmNavigation(); }; - const BUTTON_WIDTH = "10rem"; + const BUTTON_WIDTH = "12rem"; if (!hasUnsavedChanges) { return null; @@ -49,7 +57,9 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav - {t("unsavedChanges", "You have unsaved changes to your PDF.")} + {isRedactMode && hasPendingRedactions + ? t("pendingRedactions", "You have unapplied redactions that will be lost.") + : t("unsavedChanges", "You have unsaved changes to your PDF.")} {t("areYouSure", "Are you sure you want to leave?")} @@ -69,16 +81,24 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav {onApplyAndContinue && ( )} @@ -87,14 +107,22 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav {/* Mobile layout: centered stack of 4 buttons */} {onApplyAndContinue && ( )} diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index 822c3c83d..f071142a1 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -12,8 +12,10 @@ import { createProcessedFile } from '@app/contexts/file/fileActions'; import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; import { useNavigationState, useNavigationGuard, useNavigationActions } from '@app/contexts/NavigationContext'; import { useSidebarContext } from '@app/contexts/SidebarContext'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; import { useRedactionMode, useRedaction } from '@app/contexts/RedactionContext'; +import { defaultParameters, RedactParameters } from '@app/hooks/tools/redact/useRedactParameters'; interface ViewerAnnotationControlsProps { currentView: string; @@ -23,6 +25,7 @@ interface ViewerAnnotationControlsProps { export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) { const { t } = useTranslation(); const { sidebarRefs } = useSidebarContext(); + const { setLeftPanelView, setSidebarsVisible } = useToolWorkflow(); const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs); const [selectedColor, setSelectedColor] = useState('#000000'); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); @@ -32,7 +35,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const viewerContext = React.useContext(ViewerContext); // Signature context for accessing drawing API - const { signatureApiRef, isPlacementMode } = useSignature(); + const { signatureApiRef, historyApiRef, isPlacementMode } = useSignature(); // File state for save functionality const { state, selectors } = useFileState(); @@ -40,15 +43,15 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const activeFiles = selectors.getFiles(); // Check if we're in sign mode or redaction mode - const { selectedTool } = useNavigationState(); + const { selectedTool, workbench } = useNavigationState(); const { actions: navActions } = useNavigationActions(); const isSignMode = selectedTool === 'sign'; const isRedactMode = selectedTool === 'redact'; // Get redaction pending state and navigation guard - const { pendingCount: redactionPendingCount, isRedacting } = useRedactionMode(); + const { pendingCount: redactionPendingCount, isRedacting: _isRedacting } = useRedactionMode(); const { requestNavigation } = useNavigationGuard(); - const { setRedactionMode, activateTextSelection } = useRedaction(); + const { setRedactionMode, activateTextSelection, setRedactionConfig } = useRedaction(); // Turn off annotation mode when switching away from viewer useEffect(() => { @@ -62,11 +65,51 @@ export default function ViewerAnnotationControls({ currentView, disabled = false return null; } + // Persist annotations to file if there are unsaved changes + const saveAnnotationsIfNeeded = async () => { + if (!viewerContext?.exportActions?.saveAsCopy || currentView !== 'viewer') return; + const hasUnsavedAnnotations = historyApiRef?.current?.canUndo() || false; + if (!hasUnsavedAnnotations) return; + + try { + const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy(); + if (!pdfArrayBuffer) return; + + const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); + const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf'; + const newFile = new File([blob], originalFileName, { type: 'application/pdf' }); + + if (activeFiles.length > 0) { + const thumbnailResult = await generateThumbnailWithMetadata(newFile); + const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); + + const currentFileIds = state.files.ids; + if (currentFileIds.length > 0) { + const currentFileId = currentFileIds[0]; + const currentRecord = selectors.getStirlingFileStub(currentFileId); + if (!currentRecord) { + console.error('No file record found for:', currentFileId); + return; + } + + const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); + const outputStirlingFile = createStirlingFile(newFile, outputStub.id); + + await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); + } + } + } catch (error) { + console.error('Error auto-saving annotations before redaction:', error); + } + }; + // Handle redaction mode toggle - const handleRedactionToggle = () => { + const handleRedactionToggle = async () => { if (isRedactMode) { // If already in redact mode, toggle annotation mode off and show redaction layer if (viewerContext?.isAnnotationMode) { + await saveAnnotationsIfNeeded(); + viewerContext.setAnnotationMode(false); // Deactivate any active annotation tools if (signatureApiRef?.current) { @@ -81,13 +124,33 @@ export default function ViewerAnnotationControls({ currentView, disabled = false activateTextSelection(); }, 100); } else { - // Exit redaction mode - go back to default view - navActions.handleToolSelect('allTools'); + // Exit redaction mode - keep viewer workbench and show all tools in sidebar + navActions.setToolAndWorkbench(null, 'viewer'); + setLeftPanelView('toolPicker'); setRedactionMode(false); } } else { + await saveAnnotationsIfNeeded(); + // Enter redaction mode - select redact tool with manual mode - navActions.handleToolSelect('redact'); + // If we're already in the viewer, keep the viewer workbench and open the tool sidebar + if (workbench === 'viewer') { + // Set redaction config to manual mode when opening from viewer + const manualConfig: RedactParameters = { + ...defaultParameters, + mode: 'manual', + }; + setRedactionConfig(manualConfig); + + // Set tool and keep viewer workbench + navActions.setToolAndWorkbench('redact', 'viewer'); + + // Ensure sidebars are visible and open tool content + setSidebarsVisible(true); + setLeftPanelView('toolContent'); + } else { + navActions.handleToolSelect('redact'); + } setRedactionMode(true); // Activate text selection mode after a short delay setTimeout(() => { @@ -99,7 +162,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false return ( <> {/* Redaction Mode Toggle */} - + {/* Mark Text Selection Tool */} + + {pendingCount === 0 && ( diff --git a/frontend/src/core/components/tooltips/useRedactTips.ts b/frontend/src/core/components/tooltips/useRedactTips.ts index 7689a6a2a..e76cb032f 100644 --- a/frontend/src/core/components/tooltips/useRedactTips.ts +++ b/frontend/src/core/components/tooltips/useRedactTips.ts @@ -77,3 +77,32 @@ export const useRedactAdvancedTips = (): TooltipContent => { ] }; }; + +export const useRedactManualTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("redact.tooltip.manual.header.title", "Manual Redaction Controls") + }, + tips: [ + { + title: t("redact.tooltip.manual.markText.title", "Mark Text Tool"), + description: t("redact.tooltip.manual.markText.text", "Select text directly on the PDF to mark it for redaction. Click and drag to highlight specific text that you want to redact."), + }, + { + title: t("redact.tooltip.manual.markArea.title", "Mark Area Tool"), + description: t("redact.tooltip.manual.markArea.text", "Draw rectangular areas on the PDF to mark regions for redaction. Useful for redacting images, signatures, or irregular shapes."), + }, + { + title: t("redact.tooltip.manual.apply.title", "Apply Redactions"), + description: t("redact.tooltip.manual.apply.text", "After marking content, click 'Apply' to permanently redact all marked areas. The pending count shows how many redactions are ready to be applied."), + bullets: [ + t("redact.tooltip.manual.apply.bullet1", "Mark as many areas as needed before applying"), + t("redact.tooltip.manual.apply.bullet2", "All pending redactions are applied at once"), + t("redact.tooltip.manual.apply.bullet3", "Redactions cannot be undone after applying") + ] + } + ] + }; +}; diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index 97838c33c..cb14d28e1 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -73,7 +73,7 @@ const EmbedPdfViewerContent = ({ const { signatureApiRef, historyApiRef, signatureConfig, isPlacementMode } = useSignature(); // Get redaction context - const { isRedactionMode } = useRedaction(); + const { isRedactionMode, redactionsApplied, setRedactionsApplied } = useRedaction(); // Ref for redaction pending tracker API const redactionTrackerRef = useRef(null); @@ -224,23 +224,39 @@ const EmbedPdfViewerContent = ({ }; }, [isViewerHovered]); - // Register checker for unsaved changes (annotations only for now) + // Register checker for unsaved changes + // In redact mode: check for pending (unapplied) OR applied but not saved redactions + // In other modes: check annotation history useEffect(() => { if (previewFile) { return; } const checkForChanges = () => { - // Check for annotation changes via history - const hasAnnotationChanges = historyApiRef.current?.canUndo() || false; // Check for pending redactions const hasPendingRedactions = (redactionTrackerRef.current?.getPendingCount() ?? 0) > 0; + // Check for annotation history changes + const hasAnnotationChanges = historyApiRef.current?.canUndo() || false; - console.log('[Viewer] Checking for unsaved changes:', { - hasAnnotationChanges, - hasPendingRedactions - }); - return hasAnnotationChanges || hasPendingRedactions; + // Always consider applied redactions as unsaved until export + const hasAppliedRedactions = redactionsApplied; + + // When in redact mode, still include annotation changes (users may draw) + if (isManualRedactMode) { + console.log('[Viewer] Checking for unsaved changes (redact mode):', { + hasPendingRedactions, + hasAppliedRedactions, + hasAnnotationChanges, + }); + } else { + console.log('[Viewer] Checking for unsaved changes:', { + hasAnnotationChanges, + hasPendingRedactions, + hasAppliedRedactions, + }); + } + + return hasAnnotationChanges || hasPendingRedactions || hasAppliedRedactions; }; console.log('[Viewer] Registering unsaved changes checker'); @@ -250,7 +266,7 @@ const EmbedPdfViewerContent = ({ console.log('[Viewer] Unregistering unsaved changes checker'); unregisterUnsavedChangesChecker(); }; - }, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]); + }, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, isManualRedactMode, redactionsApplied]); // Apply changes - save annotations and redactions to new file version const applyChanges = useCallback(async () => { @@ -289,11 +305,13 @@ const EmbedPdfViewerContent = ({ // Step 4: Consume files (replace in context) await actions.consumeFiles(activeFileIds, stirlingFiles, stubs); + // Clear unsaved changes flags setHasUnsavedChanges(false); + setRedactionsApplied(false); } catch (error) { console.error('Apply changes failed:', error); } - }, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]); + }, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges, setRedactionsApplied]); const sidebarWidthRem = 15; const totalRightMargin = diff --git a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx index f930d1281..d79761df3 100644 --- a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx +++ b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx @@ -1,6 +1,6 @@ import { useEffect, useImperativeHandle } from 'react'; import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; -import { useRedaction, RedactionAPI } from '@app/contexts/RedactionContext'; +import { useRedaction } from '@app/contexts/RedactionContext'; /** * RedactionAPIBridge connects the EmbedPDF redaction plugin to our RedactionContext. diff --git a/frontend/src/core/components/viewer/RedactionPendingTracker.tsx b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx index 8efbde6f5..2865b49a6 100644 --- a/frontend/src/core/components/viewer/RedactionPendingTracker.tsx +++ b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react'; import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; -import { useNavigationGuard } from '@app/contexts/NavigationContext'; export interface RedactionPendingTrackerAPI { commitAllPending: () => void; @@ -8,14 +7,16 @@ export interface RedactionPendingTrackerAPI { } /** - * RedactionPendingTracker monitors pending redactions and integrates with - * the navigation guard to warn users about unsaved changes. + * RedactionPendingTracker monitors pending redactions and exposes an API + * for committing and checking pending redactions. * Must be rendered inside the EmbedPDF context. + * + * Note: The unsaved changes checker is registered by EmbedPdfViewer, not here, + * to avoid conflicts and allow the viewer to check both annotations and redactions. */ export const RedactionPendingTracker = forwardRef( function RedactionPendingTracker(_, ref) { const { state, provides } = useEmbedPdfRedaction(); - const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, setHasUnsavedChanges } = useNavigationGuard(); const pendingCountRef = useRef(0); @@ -32,26 +33,7 @@ export const RedactionPendingTracker = forwardRef( // Update ref when pending count changes useEffect(() => { pendingCountRef.current = state?.pendingCount ?? 0; - - // Also update the hasUnsavedChanges state - if (pendingCountRef.current > 0) { - setHasUnsavedChanges(true); - } - }, [state?.pendingCount, setHasUnsavedChanges]); - - // Register checker for pending redactions - useEffect(() => { - const checkForPendingRedactions = () => { - const hasPending = pendingCountRef.current > 0; - return hasPending; - }; - - registerUnsavedChangesChecker(checkForPendingRedactions); - - return () => { - unregisterUnsavedChangesChecker(); - }; - }, [registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]); + }, [state?.pendingCount]); return null; } diff --git a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx index 5ef127b09..8328615f1 100644 --- a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx +++ b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx @@ -1,64 +1,135 @@ import { useRedaction as useEmbedPdfRedaction, SelectionMenuProps } from '@embedpdf/plugin-redaction/react'; import { ActionIcon, Tooltip, Button, Group } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { createPortal } from 'react-dom'; +import { useEffect, useState, useRef, useCallback } from 'react'; import DeleteIcon from '@mui/icons-material/Delete'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; /** * Custom menu component that appears when a pending redaction mark is selected. * Allows users to remove or apply individual pending marks. + * Uses a portal to ensure it appears above all content, including next pages. */ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: SelectionMenuProps) { + const { t } = useTranslation(); const { provides } = useEmbedPdfRedaction(); + const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); + const wrapperRef = useRef(null); - if (!selected || !item) return null; - - const handleRemove = () => { - if (provides?.removePending) { + // Merge refs if menuWrapperProps has a ref + const setRef = useCallback((node: HTMLDivElement | null) => { + wrapperRef.current = node; + if (menuWrapperProps?.ref) { + const ref = menuWrapperProps.ref; + if (typeof ref === 'function') { + ref(node); + } else if (ref && 'current' in ref) { + (ref as React.MutableRefObject).current = node; + } + } + }, [menuWrapperProps]); + + const handleRemove = useCallback(() => { + if (provides?.removePending && item) { provides.removePending(item.page, item.id); } - }; + }, [provides, item]); - const handleApply = () => { - if (provides?.commitPending) { + const handleApply = useCallback(() => { + if (provides?.commitPending && item) { provides.commitPending(item.page, item.id); } - }; + }, [provides, item]); - return ( -
-
+ // Calculate position for portal based on wrapper element + useEffect(() => { + if (!selected || !item || !wrapperRef.current) { + setMenuPosition(null); + return; + } + + const updatePosition = () => { + const wrapper = wrapperRef.current; + if (!wrapper) { + setMenuPosition(null); + return; + } + + const rect = wrapper.getBoundingClientRect(); + // Position menu below the wrapper, centered + // Use getBoundingClientRect which gives viewport-relative coordinates + // Since we're using fixed positioning in the portal, we don't need to add scroll offsets + setMenuPosition({ + top: rect.bottom + 8, + left: rect.left + rect.width / 2, + }); + }; + + updatePosition(); + + // Update position on scroll/resize + window.addEventListener('scroll', updatePosition, true); + window.addEventListener('resize', updatePosition); + + return () => { + window.removeEventListener('scroll', updatePosition, true); + window.removeEventListener('resize', updatePosition); + }; + }, [selected, item]); + + // Early return AFTER all hooks have been called + if (!selected || !item) return null; + + const menuContent = menuPosition ? ( +
- +
-
+ ) : null; + + // Extract ref from menuWrapperProps to avoid conflicts + const { ref: _, ...wrapperPropsWithoutRef } = menuWrapperProps || {}; + + return ( + <> +
+ {typeof document !== 'undefined' && menuContent + ? createPortal(menuContent, document.body) + : null} + ); } diff --git a/frontend/src/core/contexts/RedactionContext.tsx b/frontend/src/core/contexts/RedactionContext.tsx index 1d6bb6eda..500c13cc7 100644 --- a/frontend/src/core/contexts/RedactionContext.tsx +++ b/frontend/src/core/contexts/RedactionContext.tsx @@ -1,5 +1,6 @@ -import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react'; +import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect } from 'react'; import { RedactParameters } from '@app/hooks/tools/redact/useRedactParameters'; +import { useNavigationGuard } from '@app/contexts/NavigationContext'; /** * API interface that the EmbedPDF bridge will implement @@ -73,6 +74,7 @@ const initialState: RedactionState = { export const RedactionProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [state, setState] = useState(initialState); const redactionApiRef = useRef(null); + const { setHasUnsavedChanges } = useNavigationGuard(); // Actions for tool configuration const setRedactionConfig = useCallback((config: RedactParameters | null) => { @@ -118,6 +120,13 @@ export const RedactionProvider: React.FC<{ children: ReactNode }> = ({ children })); }, []); + // Keep navigation guard aware of pending or applied redactions so we block navigation + useEffect(() => { + if (state.pendingCount > 0 || state.redactionsApplied) { + setHasUnsavedChanges(true); + } + }, [state.pendingCount, state.redactionsApplied, setHasUnsavedChanges]); + // Actions that call through to EmbedPDF API const activateTextSelection = useCallback(() => { if (redactionApiRef.current) { diff --git a/frontend/src/core/hooks/tools/redact/useRedactOperation.test.ts b/frontend/src/core/hooks/tools/redact/useRedactOperation.test.ts index 9e7460d85..a7a46bc3b 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactOperation.test.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactOperation.test.ts @@ -103,13 +103,14 @@ describe('buildRedactFormData', () => { expect(formData.get('convertPDFToImage')).toBe('false'); }); - test('should throw error for manual mode (not implemented)', () => { + test('should return empty form data for manual mode (handled client-side)', () => { const parameters: RedactParameters = { ...defaultParameters, mode: 'manual', }; - expect(() => buildRedactFormData(parameters, mockFile)).toThrow('Manual redaction not yet implemented'); + const formData = buildRedactFormData(parameters, mockFile); + expect(formData.get('fileInput')).toBeNull(); }); }); diff --git a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts index fd3325247..25192d082 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts @@ -6,9 +6,10 @@ import { RedactParameters, defaultParameters } from '@app/hooks/tools/redact/use // Static configuration that can be used by both the hook and automation executor export const buildRedactFormData = (parameters: RedactParameters, file: File): FormData => { const formData = new FormData(); - formData.append("fileInput", file); + // For automatic mode we hit the backend and need full payload if (parameters.mode === 'automatic') { + formData.append("fileInput", file); // Convert array to newline-separated string as expected by backend formData.append("listOfText", parameters.wordsToRedact.join('\n')); formData.append("useRegex", parameters.useRegex.toString()); @@ -16,8 +17,10 @@ export const buildRedactFormData = (parameters: RedactParameters, file: File): F formData.append("redactColor", parameters.redactColor.replace('#', '')); formData.append("customPadding", parameters.customPadding.toString()); formData.append("convertPDFToImage", parameters.convertPDFToImage.toString()); + } else { + // Manual redaction uses EmbedPDF in-viewer; we don't call the API. + // Return an empty formData to satisfy shared interfaces without throwing. } - // Note: Manual mode is handled client-side via EmbedPDF, no formData needed return formData; }; @@ -31,8 +34,7 @@ export const redactOperationConfig = { if (parameters.mode === 'automatic') { return '/api/v1/security/auto-redact'; } - // Manual redaction is handled client-side via EmbedPDF - // Return null to indicate no server endpoint is needed + // Manual redaction is handled by EmbedPDF in the viewer; no endpoint call. return null; }, defaultParameters, diff --git a/frontend/src/core/hooks/tools/redact/useRedactParameters.test.ts b/frontend/src/core/hooks/tools/redact/useRedactParameters.test.ts index 40e350010..c11d98de5 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactParameters.test.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactParameters.test.ts @@ -84,14 +84,14 @@ describe('useRedactParameters', () => { expect(result.current.getEndpointName()).toBe('/api/v1/security/auto-redact'); }); - test('should throw error for manual mode (not implemented)', () => { + test('should return null endpoint for manual mode (handled client-side)', () => { const { result } = renderHook(() => useRedactParameters()); act(() => { result.current.updateParameter('mode', 'manual'); }); - expect(() => result.current.getEndpointName()).toThrow('Manual redaction not yet implemented'); + expect(result.current.getEndpointName()).toBeNull(); }); }); diff --git a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts index 890f85721..b91cd2b72 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts @@ -34,16 +34,15 @@ export const useRedactParameters = (): RedactParametersHook => { if (params.mode === 'automatic') { return '/api/v1/security/auto-redact'; } - // Manual redaction is handled client-side via EmbedPDF - // Return null or a placeholder since we don't call an endpoint + // Manual redaction is handled in the viewer; no endpoint return null; }, validateFn: (params) => { if (params.mode === 'automatic') { return params.wordsToRedact.length > 0 && params.wordsToRedact.some(word => word.trim().length > 0); } - // Manual mode is always valid since redaction is done in the viewer - return true; + // Manual mode is not yet supported via this flow + return false; } }); }; diff --git a/frontend/src/core/tools/Redact.tsx b/frontend/src/core/tools/Redact.tsx index 2172b008a..92cf8fc69 100644 --- a/frontend/src/core/tools/Redact.tsx +++ b/frontend/src/core/tools/Redact.tsx @@ -6,11 +6,11 @@ import { useRedactParameters, RedactMode } from "@app/hooks/tools/redact/useReda import { useRedactOperation } from "@app/hooks/tools/redact/useRedactOperation"; import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; -import { useRedactModeTips, useRedactWordsTips, useRedactAdvancedTips } from "@app/components/tooltips/useRedactTips"; +import { useRedactModeTips, useRedactWordsTips, useRedactAdvancedTips, useRedactManualTips } from "@app/components/tooltips/useRedactTips"; import RedactAdvancedSettings from "@app/components/tools/redact/RedactAdvancedSettings"; import WordsToRedactInput from "@app/components/tools/redact/WordsToRedactInput"; import ManualRedactionControls from "@app/components/tools/redact/ManualRedactionControls"; -import { useNavigationActions } from "@app/contexts/NavigationContext"; +import { useNavigationActions, useNavigationState } from "@app/contexts/NavigationContext"; import { useRedaction } from "@app/contexts/RedactionContext"; const Redact = (props: BaseToolProps) => { @@ -23,7 +23,8 @@ const Redact = (props: BaseToolProps) => { // Navigation and redaction context const { actions: navActions } = useNavigationActions(); - const { setRedactionConfig, setRedactionMode } = useRedaction(); + const { setRedactionConfig, setRedactionMode, redactionConfig } = useRedaction(); + const { workbench } = useNavigationState(); const hasOpenedViewer = useRef(false); const base = useBaseTool( @@ -37,6 +38,16 @@ const Redact = (props: BaseToolProps) => { const modeTips = useRedactModeTips(); const wordsTips = useRedactWordsTips(); const advancedTips = useRedactAdvancedTips(); + const manualTips = useRedactManualTips(); + + // Auto-set manual mode if we're in the viewer and redaction config is set to manual + // This ensures when opening redact from viewer, it automatically selects manual mode + useEffect(() => { + if (workbench === 'viewer' && redactionConfig?.mode === 'manual' && base.params.parameters.mode !== 'manual') { + // Set immediately when conditions are met + base.params.updateParameter('mode', 'manual'); + } + }, [workbench, redactionConfig, base.params.parameters.mode, base.params.updateParameter]); // Handle mode change - navigate to viewer when manual mode is selected const handleModeChange = (mode: RedactMode) => { @@ -133,7 +144,7 @@ const Redact = (props: BaseToolProps) => { title: t("redact.manual.controlsTitle", "Manual Redaction Controls"), isCollapsed: false, onCollapsedClick: () => {}, - tooltip: [], + tooltip: manualTips, content: , }); } From 70ac8af978ac7613b377dee459cef53a7f406ec9 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Dec 2025 04:06:01 +0000 Subject: [PATCH 3/8] frontend validation --- frontend/package-lock.json | 57 ++++++------------- .../hooks/tools/redact/useRedactOperation.ts | 2 +- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fc742464..0f56ad4e4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -458,7 +458,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -502,7 +501,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -583,7 +581,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz", "integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/engines": "1.5.0", "@embedpdf/models": "1.5.0" @@ -683,7 +680,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz", "integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -700,7 +696,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz", "integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -718,7 +713,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz", "integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -791,7 +785,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", "integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -826,7 +819,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz", "integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -863,7 +855,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz", "integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -939,7 +930,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz", "integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1095,7 +1085,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1139,7 +1128,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2170,7 +2158,6 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz", "integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2221,7 +2208,6 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz", "integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2289,7 +2275,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -3222,7 +3207,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -3341,6 +3325,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "license": "MIT", + "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -4117,7 +4102,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4446,7 +4430,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4457,7 +4440,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4527,7 +4509,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -5241,6 +5222,7 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", "license": "MIT", + "peer": true, "dependencies": { "@vue/shared": "3.5.24" } @@ -5250,6 +5232,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/shared": "3.5.24" @@ -5260,6 +5243,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/runtime-core": "3.5.24", @@ -5272,6 +5256,7 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.24", "@vue/shared": "3.5.24" @@ -5298,7 +5283,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5706,6 +5690,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -5982,7 +5967,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7030,8 +7014,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7426,7 +7409,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7597,7 +7579,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7764,7 +7745,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/espree": { "version": "10.4.0", @@ -7829,6 +7811,7 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -8919,7 +8902,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -9396,6 +9378,7 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -9716,7 +9699,6 @@ "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.19", "@asamuzakjp/dom-selector": "^6.7.3", @@ -10303,7 +10285,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -11462,7 +11445,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11742,7 +11724,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -12125,7 +12106,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12135,7 +12115,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13647,6 +13626,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -13855,7 +13835,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14157,7 +14136,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14239,7 +14217,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -14444,7 +14421,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14615,7 +14591,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14629,7 +14604,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15241,7 +15215,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/zod": { "version": "3.25.76", diff --git a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts index 25192d082..fea3c53fd 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts @@ -35,7 +35,7 @@ export const redactOperationConfig = { return '/api/v1/security/auto-redact'; } // Manual redaction is handled by EmbedPDF in the viewer; no endpoint call. - return null; + return ""; }, defaultParameters, } as const; From 7864d1c4e29d7fcf31bfefd1c0db43a11c6c897a Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Dec 2025 14:19:59 +0000 Subject: [PATCH 4/8] undo silly AI stuff --- .../shared/NavigationWarningModal.tsx | 56 +++---------- .../rightRail/ViewerAnnotationControls.tsx | 84 +++++++------------ 2 files changed, 39 insertions(+), 101 deletions(-) diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index e87f2fa13..dff9dd957 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -1,27 +1,18 @@ import { Modal, Text, Button, Group, Stack } from "@mantine/core"; -import { useNavigationGuard, useNavigationState } from "@app/contexts/NavigationContext"; +import { useNavigationGuard } from "@app/contexts/NavigationContext"; import { useTranslation } from "react-i18next"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; -import { useRedactionMode } from "@app/contexts/RedactionContext"; -import FitText from "@app/components/shared/FitText"; interface NavigationWarningModalProps { onApplyAndContinue?: () => Promise; - onExportAndContinue?: () => Promise; } -const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => { +const NavigationWarningModal = ({ onApplyAndContinue }: NavigationWarningModalProps) => { const { t } = useTranslation(); const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } = useNavigationGuard(); - const { selectedTool } = useNavigationState(); - const { pendingCount } = useRedactionMode(); - - // Check if we're in redact mode with pending redactions - const isRedactMode = selectedTool === 'redact'; - const hasPendingRedactions = pendingCount > 0; const handleKeepWorking = () => { cancelNavigation(); @@ -40,13 +31,6 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav confirmNavigation(); }; - const _handleExportAndContinue = async () => { - if (onExportAndContinue) { - await onExportAndContinue(); - } - setHasUnsavedChanges(false); - confirmNavigation(); - }; const BUTTON_WIDTH = "12rem"; // Only show modal if there are unsaved changes AND there's an actual pending navigation @@ -59,9 +43,7 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav - {isRedactMode && hasPendingRedactions - ? t("pendingRedactions", "You have unapplied redactions that will be lost.") - : t("unsavedChanges", "You have unsaved changes to your PDF.")} + {t("unsavedChanges", "You have unsaved changes to your PDF.")} {t("areYouSure", "Are you sure you want to leave?")} @@ -83,24 +63,16 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav {onApplyAndContinue && ( )} @@ -109,22 +81,14 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav {/* Mobile layout: centered stack of 4 buttons */} {onApplyAndContinue && ( )} diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index f071142a1..74a7d2feb 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -7,9 +7,7 @@ import { ViewerContext } from '@app/contexts/ViewerContext'; import { useSignature } from '@app/contexts/SignatureContext'; import { ColorSwatchButton, ColorPicker } from '@app/components/annotation/shared/ColorPicker'; import { useFileState, useFileContext } from '@app/contexts/FileContext'; -import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils'; -import { createProcessedFile } from '@app/contexts/file/fileActions'; -import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; +import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers'; import { useNavigationState, useNavigationGuard, useNavigationActions } from '@app/contexts/NavigationContext'; import { useSidebarContext } from '@app/contexts/SidebarContext'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; @@ -50,8 +48,8 @@ export default function ViewerAnnotationControls({ currentView, disabled = false // Get redaction pending state and navigation guard const { pendingCount: redactionPendingCount, isRedacting: _isRedacting } = useRedactionMode(); - const { requestNavigation } = useNavigationGuard(); - const { setRedactionMode, activateTextSelection, setRedactionConfig } = useRedaction(); + const { requestNavigation, setHasUnsavedChanges } = useNavigationGuard(); + const { setRedactionMode, activateTextSelection, setRedactionConfig, setRedactionsApplied } = useRedaction(); // Turn off annotation mode when switching away from viewer useEffect(() => { @@ -67,37 +65,23 @@ export default function ViewerAnnotationControls({ currentView, disabled = false // Persist annotations to file if there are unsaved changes const saveAnnotationsIfNeeded = async () => { - if (!viewerContext?.exportActions?.saveAsCopy || currentView !== 'viewer') return; - const hasUnsavedAnnotations = historyApiRef?.current?.canUndo() || false; - if (!hasUnsavedAnnotations) return; + if (!viewerContext?.exportActions?.saveAsCopy || currentView !== 'viewer' || !historyApiRef?.current?.canUndo()) return; + if (activeFiles.length === 0 || state.files.ids.length === 0) return; try { - const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy(); - if (!pdfArrayBuffer) return; + const arrayBuffer = await viewerContext.exportActions.saveAsCopy(); + if (!arrayBuffer) return; - const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); - const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf'; - const newFile = new File([blob], originalFileName, { type: 'application/pdf' }); + const file = new File([new Blob([arrayBuffer])], activeFiles[0].name, { type: 'application/pdf' }); + const parentStub = selectors.getStirlingFileStub(state.files.ids[0]); + if (!parentStub) return; - if (activeFiles.length > 0) { - const thumbnailResult = await generateThumbnailWithMetadata(newFile); - const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); - - const currentFileIds = state.files.ids; - if (currentFileIds.length > 0) { - const currentFileId = currentFileIds[0]; - const currentRecord = selectors.getStirlingFileStub(currentFileId); - if (!currentRecord) { - console.error('No file record found for:', currentFileId); - return; - } - - const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); - const outputStirlingFile = createStirlingFile(newFile, outputStub.id); - - await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); - } - } + const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'redact'); + await fileActions.consumeFiles([state.files.ids[0]], stirlingFiles, stubs); + + // Clear unsaved changes flags after successful save + setHasUnsavedChanges(false); + setRedactionsApplied(false); } catch (error) { console.error('Error auto-saving annotations before redaction:', error); } @@ -321,30 +305,20 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf'; const newFile = new File([blob], originalFileName, { type: 'application/pdf' }); - // Replace the current file in context with the saved version (exact same logic as Sign tool) - if (activeFiles.length > 0) { - // Generate thumbnail and metadata for the saved file - const thumbnailResult = await generateThumbnailWithMetadata(newFile); - const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); - - // Get current file info - const currentFileIds = state.files.ids; - if (currentFileIds.length > 0) { - const currentFileId = currentFileIds[0]; - const currentRecord = selectors.getStirlingFileStub(currentFileId); - - if (!currentRecord) { - console.error('No file record found for:', currentFileId); - return; - } - - // Create output stub and file (exact same as Sign tool) - const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); - const outputStirlingFile = createStirlingFile(newFile, outputStub.id); - - // Replace the original file with the saved version - await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); + // Replace the current file in context with the saved version + if (activeFiles.length > 0 && state.files.ids.length > 0) { + const parentStub = selectors.getStirlingFileStub(state.files.ids[0]); + if (!parentStub) { + console.error('No file record found for:', state.files.ids[0]); + return; } + + const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([newFile], parentStub, 'multiTool'); + await fileActions.consumeFiles([state.files.ids[0]], stirlingFiles, stubs); + + // Clear unsaved changes flags after successful save + setHasUnsavedChanges(false); + setRedactionsApplied(false); } } } catch (error) { From 3d4d4481fa371cbf25b2764602b1f137f58702d0 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Dec 2025 14:30:59 +0000 Subject: [PATCH 5/8] undo silly AI stuff p2 --- .../rightRail/ViewerAnnotationControls.tsx | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index 74a7d2feb..00c9787fd 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -58,6 +58,18 @@ export default function ViewerAnnotationControls({ currentView, disabled = false } }, [currentView, viewerContext]); + // Activate draw mode when annotation mode becomes active + useEffect(() => { + if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') { + try { + signatureApiRef.current.activateDrawMode(); + signatureApiRef.current.updateDrawSettings(selectedColor, 2); + } catch (error) { + console.log('Signature API not ready:', error); + } + } + }, [viewerContext?.isAnnotationMode, currentView, selectedColor, signatureApiRef]); + // Don't show any annotation controls in sign mode if (isSignMode) { return null; @@ -247,36 +259,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false radius="md" className="right-rail-icon" onClick={() => { - const activateDrawMode = () => { - // Use setTimeout to ensure this runs after any state updates from applyChanges - setTimeout(() => { - viewerContext?.setAnnotationMode(true); - // Activate ink drawing tool when entering annotation mode - if (signatureApiRef?.current && currentView === 'viewer') { - try { - signatureApiRef.current.activateDrawMode(); - signatureApiRef.current.updateDrawSettings(selectedColor, 2); - } catch (error) { - console.log('Signature API not ready:', error); - } - } - }, 150); - }; - // If in redaction mode with pending redactions, show warning modal if (isRedactMode && redactionPendingCount > 0) { - requestNavigation(activateDrawMode); + requestNavigation(() => { + viewerContext?.setAnnotationMode(true); + }); } else { - // Direct activation - no need for delay + // Direct activation - useEffect will handle draw mode activation viewerContext?.toggleAnnotationMode(); - if (signatureApiRef?.current && currentView === 'viewer') { - try { - signatureApiRef.current.activateDrawMode(); - signatureApiRef.current.updateDrawSettings(selectedColor, 2); - } catch (error) { - console.log('Signature API not ready:', error); - } - } } }} disabled={disabled} From 53a404d44d3dfb2868938f36e40046346261c635 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Dec 2025 17:59:18 +0000 Subject: [PATCH 6/8] undo silly AI stuff p3 --- frontend/src/core/components/viewer/LocalEmbedPDF.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index 7e9cde250..7a92fbad9 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -55,7 +55,6 @@ interface LocalEmbedPDFProps { url?: string | null; enableAnnotations?: boolean; enableRedaction?: boolean; - /** When true, RedactionAPIBridge is rendered even if enableRedaction is false (for switching between annotation/redaction) */ isManualRedactionMode?: boolean; onSignatureAdded?: (annotation: any) => void; signatureApiRef?: React.RefObject; From 2f87ff2ae39bfb661f86b1f5e18c79ff43e35577 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 17 Dec 2025 10:13:46 +0000 Subject: [PATCH 7/8] frontend validation --- .../components/shared/rightRail/ViewerAnnotationControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index 3248ae9f1..ddda276c3 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -48,7 +48,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const isRedactMode = selectedTool === 'redact'; // Get redaction pending state and navigation guard - const { pendingCount: redactionPendingCount, isRedacting: _isRedacting, activeType } = useRedactionMode(); + const { pendingCount: redactionPendingCount, isRedacting: _isRedacting } = useRedactionMode(); const { requestNavigation, setHasUnsavedChanges } = useNavigationGuard(); const { setRedactionMode, activateTextSelection, setRedactionConfig, setRedactionsApplied, redactionApiRef, setActiveType } = useRedaction(); From a17d36a90b6f38b3e1135f8206a01889afdfca66 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 17 Dec 2025 16:24:44 +0000 Subject: [PATCH 8/8] frontend validation --- .../shared/NavigationWarningModal.tsx | 21 ++++++++++++++++++- .../hooks/tools/redact/useRedactParameters.ts | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index dff9dd957..ad061c1ca 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -7,9 +7,10 @@ import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; interface NavigationWarningModalProps { onApplyAndContinue?: () => Promise; + onExportAndContinue?: () => Promise; } -const NavigationWarningModal = ({ onApplyAndContinue }: NavigationWarningModalProps) => { +const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => { const { t } = useTranslation(); const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } = useNavigationGuard(); @@ -31,6 +32,14 @@ const NavigationWarningModal = ({ onApplyAndContinue }: NavigationWarningModalPr confirmNavigation(); }; + const handleExportAndContinue = async () => { + if (onExportAndContinue) { + await onExportAndContinue(); + } + setHasUnsavedChanges(false); + confirmNavigation(); + }; + const BUTTON_WIDTH = "12rem"; // Only show modal if there are unsaved changes AND there's an actual pending navigation @@ -75,6 +84,11 @@ const NavigationWarningModal = ({ onApplyAndContinue }: NavigationWarningModalPr {t("applyAndContinue", "Apply & Leave")} )} + {onExportAndContinue && ( + + )} @@ -91,6 +105,11 @@ const NavigationWarningModal = ({ onApplyAndContinue }: NavigationWarningModalPr {t("applyAndContinue", "Apply & Leave")} )} + {onExportAndContinue && ( + + )} diff --git a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts index b91cd2b72..89884d558 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts @@ -34,8 +34,8 @@ export const useRedactParameters = (): RedactParametersHook => { if (params.mode === 'automatic') { return '/api/v1/security/auto-redact'; } - // Manual redaction is handled in the viewer; no endpoint - return null; + // Manual redaction handled client-side + return ''; }, validateFn: (params) => { if (params.mode === 'automatic') {