mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
finished
This commit is contained in:
parent
5485b06735
commit
659d1deba8
@ -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"
|
||||
|
||||
|
||||
@ -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<void>;
|
||||
@ -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
|
||||
<Modal
|
||||
opened={showNavigationWarning}
|
||||
onClose={handleKeepWorking}
|
||||
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
||||
title={isRedactMode && hasPendingRedactions
|
||||
? t("pendingRedactionsTitle", "Unapplied Redactions")
|
||||
: t("unsavedChangesTitle", "Unsaved Changes")}
|
||||
centered
|
||||
size="auto"
|
||||
closeOnClickOutside={true}
|
||||
@ -58,7 +68,9 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav
|
||||
<Stack>
|
||||
<Stack ta="center" p="md">
|
||||
<Text size="md" fw="300">
|
||||
{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.")}
|
||||
</Text>
|
||||
<Text size="lg" fw="500" >
|
||||
{t("areYouSure", "Are you sure you want to leave?")}
|
||||
@ -69,16 +81,24 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav
|
||||
<Group justify="space-between" gap="xl" visibleFrom="md">
|
||||
<Group gap="sm">
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={handleKeepWorking} w={BUTTON_WIDTH} leftSection={<ArrowBackIcon fontSize="small" />}>
|
||||
{t("keepWorking", "Keep Working")}
|
||||
<FitText text={t("keepWorking", "Keep Working")} minimumFontScale={0.55} />
|
||||
</Button>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
<FitText
|
||||
text={isRedactMode && hasPendingRedactions
|
||||
? t("discardRedactions", "Discard & Leave")
|
||||
: t("discardChanges", "Discard & Leave")}
|
||||
minimumFontScale={0.55}
|
||||
/>
|
||||
</Button>
|
||||
{onApplyAndContinue && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
<FitText
|
||||
text={t("applyAndContinue", "Save & Leave")}
|
||||
minimumFontScale={0.55}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
@ -87,14 +107,22 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav
|
||||
{/* Mobile layout: centered stack of 4 buttons */}
|
||||
<Stack align="center" gap="sm" hiddenFrom="md">
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={handleKeepWorking} w={BUTTON_WIDTH} leftSection={<ArrowBackIcon fontSize="small" />}>
|
||||
{t("keepWorking", "Keep Working")}
|
||||
<FitText text={t("keepWorking", "Keep Working")} minimumFontScale={0.55} />
|
||||
</Button>
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
<FitText
|
||||
text={isRedactMode && hasPendingRedactions
|
||||
? t("discardRedactions", "Discard & Leave")
|
||||
: t("discardChanges", "Discard & Leave")}
|
||||
minimumFontScale={0.55}
|
||||
/>
|
||||
</Button>
|
||||
{onApplyAndContinue && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
<FitText
|
||||
text={t("applyAndContinue", "Save & Leave")}
|
||||
minimumFontScale={0.55}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@ -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 */}
|
||||
<Tooltip content={isRedactMode ? t('rightRail.exitRedaction', 'Exit Redaction Mode') : t('rightRail.redact', 'Redact')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
|
||||
<Tooltip content={isRedactMode && !viewerContext?.isAnnotationMode ? t('rightRail.exitRedaction', 'Exit Redaction Mode') : t('rightRail.redact', 'Redact')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
|
||||
<ActionIcon
|
||||
variant={isRedactMode && !viewerContext?.isAnnotationMode ? 'filled' : 'subtle'}
|
||||
color={isRedactMode && !viewerContext?.isAnnotationMode ? 'red' : undefined}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Button, Stack, Text, Badge, Group, Divider } from '@mantine/core';
|
||||
import { Button, Stack, Text, Badge, Group, Divider, Tooltip } from '@mantine/core';
|
||||
import HighlightAltIcon from '@mui/icons-material/HighlightAlt';
|
||||
import CropFreeIcon from '@mui/icons-material/CropFree';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
@ -125,14 +125,16 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
<Group gap="sm" grow wrap="nowrap">
|
||||
{/* Mark Text Selection Tool */}
|
||||
<Button
|
||||
variant={isSelectionActive && !isAnnotationMode ? 'filled' : 'light'}
|
||||
color={isSelectionActive && !isAnnotationMode ? 'red' : 'gray'}
|
||||
variant={isSelectionActive && !isAnnotationMode ? 'filled' : 'outline'}
|
||||
color={isSelectionActive && !isAnnotationMode ? 'blue' : 'gray'}
|
||||
leftSection={<HighlightAltIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleSelectionClick}
|
||||
disabled={disabled || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { minWidth: 0 },
|
||||
root: {
|
||||
minWidth: 0,
|
||||
},
|
||||
label: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
@ -141,14 +143,16 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
|
||||
{/* Mark Area (Marquee) Tool */}
|
||||
<Button
|
||||
variant={isMarqueeActive && !isAnnotationMode ? 'filled' : 'light'}
|
||||
color={isMarqueeActive && !isAnnotationMode ? 'red' : 'gray'}
|
||||
variant={isMarqueeActive && !isAnnotationMode ? 'filled' : 'outline'}
|
||||
color={isMarqueeActive && !isAnnotationMode ? 'blue' : 'gray'}
|
||||
leftSection={<CropFreeIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleMarqueeClick}
|
||||
disabled={disabled || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { minWidth: 0 },
|
||||
root: {
|
||||
minWidth: 0,
|
||||
},
|
||||
label: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
@ -173,20 +177,27 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
leftSection={<CheckCircleIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleApplyAll}
|
||||
<Tooltip
|
||||
label={t('redact.manual.applyWarning', '⚠️ Permanent application, cannot be undone and the data underneath will be deleted')}
|
||||
withArrow
|
||||
position="top"
|
||||
disabled={disabled || pendingCount === 0 || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { flexShrink: 0 },
|
||||
label: { whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
{t('redact.manual.apply', 'Apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
leftSection={<CheckCircleIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleApplyAll}
|
||||
disabled={disabled || pendingCount === 0 || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { flexShrink: 0 },
|
||||
label: { whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
{t('redact.manual.apply', 'Apply')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{pendingCount === 0 && (
|
||||
|
||||
@ -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")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@ -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<RedactionPendingTrackerAPI>(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 =
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<RedactionPendingTrackerAPI>(
|
||||
function RedactionPendingTracker(_, ref) {
|
||||
const { state, provides } = useEmbedPdfRedaction();
|
||||
const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
const pendingCountRef = useRef(0);
|
||||
|
||||
@ -32,26 +33,7 @@ export const RedactionPendingTracker = forwardRef<RedactionPendingTrackerAPI>(
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -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<HTMLDivElement>(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<HTMLDivElement | null>).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 (
|
||||
<div {...menuWrapperProps}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
marginTop: 8,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 12px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.25)',
|
||||
border: '1px solid var(--mantine-color-default-border)',
|
||||
// Fixed size to prevent browser zoom affecting layout
|
||||
fontSize: '14px',
|
||||
minWidth: '180px',
|
||||
}}
|
||||
>
|
||||
// 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 ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: `${menuPosition.top}px`,
|
||||
left: `${menuPosition.left}px`,
|
||||
transform: 'translateX(-50%)',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 10000, // Very high z-index to appear above everything
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 12px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.25)',
|
||||
border: '1px solid var(--mantine-color-default-border)',
|
||||
// Fixed size to prevent browser zoom affecting layout
|
||||
fontSize: '14px',
|
||||
minWidth: '180px',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" justify="center">
|
||||
<Tooltip label="Remove this mark">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
onClick={handleRemove}
|
||||
style={{ flexShrink: 0 }}
|
||||
styles={{
|
||||
root: {
|
||||
flexShrink: 0,
|
||||
backgroundColor: 'var(--bg-raised)',
|
||||
border: '1px solid var(--border-default)',
|
||||
color: 'var(--text-secondary)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--hover-bg)',
|
||||
borderColor: 'var(--border-strong)',
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon style={{ fontSize: 18 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Apply this redaction permanently">
|
||||
<Tooltip
|
||||
label={t('redact.manual.applyWarning', '⚠️ Permanent application, cannot be undone and the data underneath will be deleted')}
|
||||
withArrow
|
||||
position="top"
|
||||
>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
@ -74,7 +145,28 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// Extract ref from menuWrapperProps to avoid conflicts
|
||||
const { ref: _, ...wrapperPropsWithoutRef } = menuWrapperProps || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={setRef}
|
||||
{...wrapperPropsWithoutRef}
|
||||
style={{
|
||||
// Preserve the original positioning from menuWrapperProps
|
||||
...(wrapperPropsWithoutRef?.style || {}),
|
||||
// Override visibility to hide the wrapper (we only need its position)
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
{typeof document !== 'undefined' && menuContent
|
||||
? createPortal(menuContent, document.body)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<RedactionState>(initialState);
|
||||
const redactionApiRef = useRef<RedactionAPI | null>(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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -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: <ManualRedactionControls disabled={!base.hasFiles} />,
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user