mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
Added bones for manual redaction, quickly saving so I can work on the text editor
This commit is contained in:
parent
bdb3c887f3
commit
5485b06735
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
<ViewerProvider>
|
||||
<PageEditorProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
<RedactionProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</RedactionProvider>
|
||||
</SignatureProvider>
|
||||
</PageEditorProvider>
|
||||
</ViewerProvider>
|
||||
|
||||
@ -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 */}
|
||||
<Tooltip content={isRedactMode ? 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}
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={handleRedactionToggle}
|
||||
disabled={disabled || currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon
|
||||
icon="scan-delete-rounded"
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Visibility Toggle */}
|
||||
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
|
||||
<ActionIcon
|
||||
@ -68,7 +128,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
|
||||
disabled={disabled || currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode}
|
||||
>
|
||||
<LocalIcon
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "preview-off-rounded"}
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
/>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('redact.manual.title', 'Redaction Tools')}
|
||||
</Text>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('redact.manual.instructions', 'Select text or draw areas on the PDF to mark content for redaction.')}
|
||||
</Text>
|
||||
|
||||
<Group gap="sm" grow wrap="nowrap">
|
||||
{/* Mark Text Selection Tool */}
|
||||
<Button
|
||||
variant={isSelectionActive && !isAnnotationMode ? 'filled' : 'light'}
|
||||
color={isSelectionActive && !isAnnotationMode ? 'red' : 'gray'}
|
||||
leftSection={<HighlightAltIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleSelectionClick}
|
||||
disabled={disabled || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { minWidth: 0 },
|
||||
label: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
{t('redact.manual.markText', 'Mark Text')}
|
||||
</Button>
|
||||
|
||||
{/* Mark Area (Marquee) Tool */}
|
||||
<Button
|
||||
variant={isMarqueeActive && !isAnnotationMode ? 'filled' : 'light'}
|
||||
color={isMarqueeActive && !isAnnotationMode ? 'red' : 'gray'}
|
||||
leftSection={<CropFreeIcon style={{ fontSize: 18, flexShrink: 0 }} />}
|
||||
onClick={handleMarqueeClick}
|
||||
disabled={disabled || !isApiReady}
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { minWidth: 0 },
|
||||
label: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
{t('redact.manual.markArea', 'Mark Area')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Pending Count and Apply Button */}
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text size="sm" c="dimmed" style={{ whiteSpace: 'nowrap' }}>
|
||||
{t('redact.manual.pendingLabel', 'Pending:')}
|
||||
</Text>
|
||||
<Badge
|
||||
color={pendingCount > 0 ? 'red' : 'gray'}
|
||||
variant="filled"
|
||||
size="lg"
|
||||
>
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<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>
|
||||
</Group>
|
||||
|
||||
{pendingCount === 0 && (
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{t('redact.manual.noMarks', 'No redaction marks. Use the tools above to mark content for redaction.')}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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<RedactionPendingTrackerAPI>(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<any>}
|
||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
||||
onSignatureAdded={() => {
|
||||
// Handle signature added - for debugging, enable console logs as needed
|
||||
// Future: Handle signature completion
|
||||
|
||||
@ -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<SignatureAPI>;
|
||||
historyApiRef?: React.RefObject<HistoryAPI>;
|
||||
redactionTrackerRef?: React.RefObject<RedactionPendingTrackerAPI>;
|
||||
}
|
||||
|
||||
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<string | null>(null);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
|
||||
@ -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
|
||||
<SearchAPIBridge />
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
{enableAnnotations && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||
{enableAnnotations && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
{/* Always render SignatureAPIBridge so annotation tools (draw) can be activated even when starting in redaction mode */}
|
||||
<SignatureAPIBridge ref={signatureApiRef} />
|
||||
{(enableAnnotations || enableRedaction || isManualRedactionMode) && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
{/* Always render RedactionAPIBridge when in manual redaction mode so buttons can switch from annotation mode */}
|
||||
{(enableRedaction || isManualRedactionMode) && <RedactionAPIBridge />}
|
||||
{(enableRedaction || isManualRedactionMode) && <RedactionPendingTracker ref={redactionTrackerRef} />}
|
||||
<ExportAPIBridge />
|
||||
<BookmarkAPIBridge />
|
||||
<PrintAPIBridge />
|
||||
@ -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 && (
|
||||
<RedactionLayer
|
||||
pageIndex={pageIndex}
|
||||
scale={scale}
|
||||
rotation={rotation}
|
||||
selectionMenu={(props) => <RedactionSelectionMenu {...props} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PagePointerProvider>
|
||||
</Rotate>
|
||||
|
||||
50
frontend/src/core/components/viewer/RedactionAPIBridge.tsx
Normal file
50
frontend/src/core/components/viewer/RedactionAPIBridge.tsx
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@ -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<RedactionPendingTrackerAPI>(
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<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',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" justify="center">
|
||||
<Tooltip label="Remove this mark">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="md"
|
||||
onClick={handleRemove}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<DeleteIcon style={{ fontSize: 18 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Apply this redaction permanently">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
size="xs"
|
||||
onClick={handleApply}
|
||||
leftSection={<CheckCircleIcon style={{ fontSize: 16 }} />}
|
||||
styles={{
|
||||
root: { flexShrink: 0, whiteSpace: 'nowrap' },
|
||||
}}
|
||||
>
|
||||
Apply (permanent)
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
185
frontend/src/core/contexts/RedactionContext.tsx
Normal file
185
frontend/src/core/contexts/RedactionContext.tsx
Normal file
@ -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<RedactionAPI | null>;
|
||||
}
|
||||
|
||||
// Create context
|
||||
const RedactionContext = createContext<RedactionContextValue | undefined>(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<RedactionState>(initialState);
|
||||
const redactionApiRef = useRef<RedactionAPI | null>(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 (
|
||||
<RedactionContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</RedactionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -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: (
|
||||
<RedactModeSelector
|
||||
mode={base.params.parameters.mode}
|
||||
onModeChange={(mode) => 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: <ManualRedactionControls disabled={!base.hasFiles} />,
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user