From 1e3eea199acb15c46fb1c3d0a31ed702efa250f0 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Sun, 2 Nov 2025 23:48:53 +0000 Subject: [PATCH] add warning message to show redaction is irreversible --- .../src/core/components/shared/Warning.tsx | 37 ++++ .../redact/ManualRedactionWorkbenchView.tsx | 168 +++++++++--------- frontend/src/core/styles/theme.css | 8 + frontend/src/core/tools/Redact.tsx | 2 +- 4 files changed, 133 insertions(+), 82 deletions(-) create mode 100644 frontend/src/core/components/shared/Warning.tsx diff --git a/frontend/src/core/components/shared/Warning.tsx b/frontend/src/core/components/shared/Warning.tsx new file mode 100644 index 000000000..1d03771e1 --- /dev/null +++ b/frontend/src/core/components/shared/Warning.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +interface WarningProps { + text: React.ReactNode; + width?: number | string; + height?: number | string; + className?: string; + style?: React.CSSProperties; +} + +const Warning: React.FC = ({ text, width = '100%', height, className, style }) => { + return ( +
+ ⚠️ +
{text}
+
+ ); +}; + +export default Warning; + + diff --git a/frontend/src/core/components/tools/redact/ManualRedactionWorkbenchView.tsx b/frontend/src/core/components/tools/redact/ManualRedactionWorkbenchView.tsx index 18ad8694b..ceaaaea46 100644 --- a/frontend/src/core/components/tools/redact/ManualRedactionWorkbenchView.tsx +++ b/frontend/src/core/components/tools/redact/ManualRedactionWorkbenchView.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { createPluginRegistration } from '@embedpdf/core'; import { EmbedPDF } from '@embedpdf/core/react'; import { usePdfiumEngine } from '@embedpdf/engines/react'; @@ -18,12 +19,11 @@ import { ExportPluginPackage } from '@embedpdf/plugin-export/react'; import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { RedactionPluginPackage, RedactionLayer, useRedaction } from '@embedpdf/plugin-redaction/react'; import type { SelectionMenuProps } from '@embedpdf/plugin-redaction/react'; -import { Stack, Group, Text, Button, Alert, Loader } from '@mantine/core'; +import { Stack, Group, Text, Button, Loader, Alert } from '@mantine/core'; +import Warning from '@app/components/shared/Warning'; import { useTranslation } from 'react-i18next'; import CropFreeRoundedIcon from '@mui/icons-material/CropFreeRounded'; import TextFieldsRoundedIcon from '@mui/icons-material/TextFieldsRounded'; -import UndoRoundedIcon from '@mui/icons-material/UndoRounded'; -import RedoRoundedIcon from '@mui/icons-material/RedoRounded'; import ToolLoadingFallback from '@app/components/tools/ToolLoadingFallback'; import { alert } from '@app/components/toast'; import { useRightRailButtons, type RightRailButtonWithAction } from '@app/hooks/useRightRailButtons'; @@ -88,11 +88,12 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp const historyApiRef = useRef | null>(null); const [isReady, setIsReady] = useState(false); const [isApplying, setIsApplying] = useState(false); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); + // Removed undo/redo controls; plugin state still manages pending internally const [activeType, setActiveType] = useState(null); const [pdfUrl, setPdfUrl] = useState(null); const [objectUrl, setObjectUrl] = useState(null); + const desiredModeRef = useRef<'area' | 'text' | null>(null); + const correctingModeRef = useRef(false); const selectedFile = data?.file ?? null; const redactionPluginPackage = RedactionPluginPackage; const RedactionLayerComponent = RedactionLayer; @@ -214,11 +215,26 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp try { const api = redactionApiRef.current; api?.onStateChange?.((state: any) => { - // heuristics: if there are any pending or previous operations, enable undo - const pending = Number(state?.pendingCount ?? 0); - setCanUndo(pending > 0 || Boolean(state?.canUndo)); - setCanRedo(Boolean(state?.canRedo)); setActiveType(state?.activeType ?? null); + + // Prevent unexpected plugin mode flips (e.g., switching to text after area drag) + const desired = desiredModeRef.current; + const isAreaDesired = desired === 'area'; + const isTextDesired = desired === 'text'; + const isAreaActive = state?.activeType === 'marqueeRedact' || state?.activeType === 'area'; + const isTextActive = state?.activeType === 'redactSelection' || state?.activeType === 'text'; + if (!correctingModeRef.current) { + if (isAreaDesired && !isAreaActive) { + correctingModeRef.current = true; + // best-effort attempts to re-activate area mode + enableAreaRedaction(); + setTimeout(() => { correctingModeRef.current = false; }, 0); + } else if (isTextDesired && !isTextActive) { + correctingModeRef.current = true; + enableTextRedaction(); + setTimeout(() => { correctingModeRef.current = false; }, 0); + } + } }); } catch {} }); @@ -279,6 +295,7 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp const api = redactionApiRef.current; // Ensure selection plugin is not intercepting as text selection try { selectionApiRef.current?.setMode?.('none'); } catch {} + desiredModeRef.current = 'area'; // Prefer official capability if (api?.toggleMarqueeRedact) { try { api.toggleMarqueeRedact(); setActiveType('marqueeRedact'); return; } catch {} @@ -300,6 +317,7 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp const api = redactionApiRef.current; // Ensure selection plugin is in text mode when redacting text try { selectionApiRef.current?.setMode?.('text'); } catch {} + desiredModeRef.current = 'text'; if (api?.toggleRedactSelection) { try { api.toggleRedactSelection(); setActiveType('redactSelection'); return; } catch {} } @@ -314,46 +332,7 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp console.warn('[manual-redaction] No compatible text redaction activation method found'); }, [hasRedactionSupport, invokeRedactionMethod]); - const handleUndo = useCallback(() => { - if (!hasRedactionSupport) return; - // Prefer redaction-aware undo - if (invokeRedactionMethod(['undo', 'stepBack', 'undoLast'])) { - return; - } - // Fallback: remove the most recent pending mark if available - try { - const state = (redactionApiRef.current?.getState?.() as any) || {}; - const pendingMap = state.pending || {}; - const pages = Object.keys(pendingMap).map(n => parseInt(n, 10)).sort((a,b) => b-a); - for (const page of pages) { - const items = pendingMap[page]; - const last = Array.isArray(items) ? items[items.length - 1] : null; - if (last) { - redactionApiRef.current?.removePending?.(page, last.id); - return; - } - } - } catch {} - const historyApi = historyApiRef.current; - if (historyApi && typeof historyApi.undo === 'function') { - historyApi.undo(); - return; - } - console.warn('[manual-redaction] Undo not available'); - }, [hasRedactionSupport, invokeRedactionMethod]); - - const handleRedo = useCallback(() => { - if (!hasRedactionSupport) return; - if (invokeRedactionMethod(['redo', 'stepForward', 'redoLast'])) { - return; - } - const historyApi = historyApiRef.current; - if (historyApi && typeof historyApi.redo === 'function') { - historyApi.redo(); - return; - } - console.warn('[manual-redaction] Redo not available'); - }, [hasRedactionSupport, invokeRedactionMethod]); + // Undo/Redo removed from UI; users can remove individual marks via the inline menu const exportRedactedBlob = useCallback(async (): Promise => { if (!hasRedactionSupport) { @@ -494,27 +473,7 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp className: activeType === 'redactSelection' ? 'right-rail-icon--active' : undefined, onClick: enableTextRedaction, }, - { - id: 'manual-redaction-undo', - icon: , - tooltip: t('redact.manual.buttons.undo', 'Undo last change'), - ariaLabel: t('redact.manual.buttons.undo', 'Undo last change'), - section: 'top', - order: 2, - disabled: !isReady || !hasRedactionSupport || !canUndo, - onClick: handleUndo, - }, - { - id: 'manual-redaction-redo', - icon: , - tooltip: t('redact.manual.buttons.redo', 'Redo change'), - ariaLabel: t('redact.manual.buttons.redo', 'Redo change'), - section: 'top', - order: 3, - disabled: !isReady || !hasRedactionSupport || !canRedo, - onClick: handleRedo, - }, - ]), [enableAreaRedaction, enableTextRedaction, handleUndo, handleRedo, hasRedactionSupport, isReady, t, canUndo, canRedo]); + ]), [enableAreaRedaction, enableTextRedaction, hasRedactionSupport, isReady, t, activeType]); useRightRailButtons(rightRailButtons); @@ -564,7 +523,7 @@ const ManualRedactionWorkbenchView = ({ data }: ManualRedactionWorkbenchViewProp - ); + + const { ref: _ignoredRef, ...restWrapper } = (menuWrapperProps as any) || {}; + + return ( + <> +
+ {isVisible && screenRect ? createPortal(panel, document.body) : null} + + ); } diff --git a/frontend/src/core/styles/theme.css b/frontend/src/core/styles/theme.css index c69fcd3d3..9b4bd5676 100644 --- a/frontend/src/core/styles/theme.css +++ b/frontend/src/core/styles/theme.css @@ -81,6 +81,10 @@ --color-yellow-800: #854d0e; --color-yellow-900: #713f12; + /* Warning component custom variables */ + --warning-yellow-bg: #fefce8; /* requested: fefce8 */ + --warning-yellow-border: #fef9c2; /* requested: fef9c2 */ + --color-red-50: #fef2f2; --color-red-100: #fee2e2; --color-red-200: #fecaca; @@ -367,6 +371,10 @@ --color-yellow-800: #fef08a; --color-yellow-900: #fef9c3; + /* Warning component variables (dark mode - keep readable) */ + --warning-yellow-bg: #3b2a10; + --warning-yellow-border: #4a3514; + /* Dark theme semantic colors */ --bg-surface: #2A2F36; --bg-raised: #1F2329; diff --git a/frontend/src/core/tools/Redact.tsx b/frontend/src/core/tools/Redact.tsx index a93a093cf..92ce6caf1 100644 --- a/frontend/src/core/tools/Redact.tsx +++ b/frontend/src/core/tools/Redact.tsx @@ -215,7 +215,7 @@ const Redact = (props: BaseToolProps) => { )}