Feature/v2/redact (#5249)

# Description of Changes

- Add manual redaction and added it to the right rail in the viewer

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01
2026-01-05 15:12:14 +00:00
committed by GitHub
parent 991d158cb8
commit 0c96133544
34 changed files with 4210 additions and 1237 deletions
+64 -4
View File
@@ -12,6 +12,27 @@ import { useAnnotationStyleState } from '@app/tools/annotate/useAnnotationStyleS
import { useAnnotationSelection } from '@app/tools/annotate/useAnnotationSelection';
import { AnnotationPanel } from '@app/tools/annotate/AnnotationPanel';
// Tools that require drawing/interacting with the PDF and should disable pan mode
const DRAWING_TOOLS: AnnotationToolId[] = [
'highlight',
'underline',
'strikeout',
'squiggly',
'ink',
'inkHighlighter',
'text',
'note',
'square',
'circle',
'line',
'lineArrow',
'polyline',
'polygon',
'stamp',
'signatureStamp',
'signatureInk',
];
const KNOWN_ANNOTATION_TOOLS: AnnotationToolId[] = [
'select',
'highlight',
@@ -52,9 +73,12 @@ const Annotate = (_props: BaseToolProps) => {
setPlacementPreviewSize,
} = useSignature();
const viewerContext = useContext(ViewerContext);
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
const { getZoomState, registerImmediateZoomUpdate, applyChanges, activeFileIndex, panActions } = useViewer();
const [activeTool, setActiveTool] = useState<AnnotationToolId>('select');
// Track the previous file index to detect file switches
const prevFileIndexRef = useRef<number>(activeFileIndex);
const activeToolRef = useRef<AnnotationToolId>('select');
const wasAnnotateActiveRef = useRef<boolean>(false);
const [selectedTextDraft, setSelectedTextDraft] = useState<string>('');
@@ -143,9 +167,11 @@ const Annotate = (_props: BaseToolProps) => {
setTextAlignment,
} = styleActions;
const handleApplyChanges = useCallback(() => {
window.dispatchEvent(new CustomEvent('stirling-annotations-apply'));
}, []);
const handleApplyChanges = useCallback(async () => {
if (applyChanges) {
await applyChanges();
}
}, [applyChanges]);
useEffect(() => {
const isAnnotateActive = workbench === 'viewer' && selectedTool === 'annotate';
@@ -205,6 +231,35 @@ const Annotate = (_props: BaseToolProps) => {
annotationApiRef?.current?.activateAnnotationTool?.(activeTool, toolOptions);
}, [viewerContext?.isAnnotationMode, signatureApiRef, activeTool, buildToolOptions, stampImageData, stampImageSize]);
// Reset to 'select' mode when switching between files
// The new PDF gets a fresh EmbedPDF instance - forcing user to re-select tool ensures it works properly
useEffect(() => {
if (prevFileIndexRef.current !== activeFileIndex) {
prevFileIndexRef.current = activeFileIndex;
// Reset to select mode when switching files
// This forces the user to re-select their tool, which ensures proper activation on the new PDF
if (activeTool !== 'select') {
setActiveTool('select');
activeToolRef.current = 'select';
// Clean up placement mode if we were in stamp tool
setPlacementMode(false);
setSignatureConfig(null);
// Small delay to ensure the new EmbedPDF instance is ready, then activate select mode
const timer = setTimeout(() => {
if (annotationApiRef?.current) {
annotationApiRef.current.deselectAnnotation?.();
annotationApiRef.current.activateAnnotationTool?.('select');
}
}, 100);
return () => clearTimeout(timer);
}
}
}, [activeFileIndex, activeTool, setPlacementMode, setSignatureConfig]);
const activateAnnotationTool = (toolId: AnnotationToolId) => {
// If leaving stamp tool, clean up placement mode
if (activeTool === 'stamp' && toolId !== 'stamp') {
@@ -224,6 +279,11 @@ const Annotate = (_props: BaseToolProps) => {
setSelectedAnn(null);
setSelectedAnnId(null);
// Disable pan mode when activating drawing tools to avoid conflict
if (DRAWING_TOOLS.includes(toolId)) {
panActions.disablePan();
}
// Change the tool
setActiveTool(toolId);
const options =
+83 -9
View File
@@ -1,14 +1,18 @@
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 { 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, useNavigationState } from "@app/contexts/NavigationContext";
import { useRedaction } from "@app/contexts/RedactionContext";
import { useFileState } from "@app/contexts/file/fileHooks";
const Redact = (props: BaseToolProps) => {
const { t } = useTranslation();
@@ -18,6 +22,12 @@ 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, redactionConfig } = useRedaction();
const { workbench } = useNavigationState();
const hasOpenedViewer = useRef(false);
const base = useBaseTool(
'redact',
useRedactParameters,
@@ -25,14 +35,61 @@ const Redact = (props: BaseToolProps) => {
props
);
// Get total file count from context (any files in workbench, not just selected)
const { state: fileState } = useFileState();
const hasAnyFiles = fileState.files.ids.length > 0;
// Tooltips for each step
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
// Manual mode works with any files in workbench (not just selected files)
const handleModeChange = (mode: RedactMode) => {
base.params.updateParameter('mode', mode);
if (mode === 'manual' && hasAnyFiles) {
// 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
// Uses hasAnyFiles since manual mode works with any files in workbench
useEffect(() => {
if (base.params.parameters.mode === 'manual' && hasAnyFiles && !hasOpenedViewer.current) {
setRedactionConfig(base.params.parameters);
setRedactionMode(true);
navActions.setWorkbench('viewer');
hasOpenedViewer.current = true;
}
}, [hasAnyFiles, 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;
};
@@ -44,18 +101,24 @@ const Redact = (props: BaseToolProps) => {
// Build conditional steps based on redaction mode
const buildSteps = () => {
// Method step is always expandable (even without files selected)
// Only collapse on results or user preference
const methodStepCollapsed = base.hasResults ? true : methodCollapsed;
const steps = [
// Method selection step (always present)
// Method selection step (always present and always expandable)
{
title: t("redact.modeSelector.title", "Redaction Method"),
isCollapsed: getActualCollapsedState(methodCollapsed),
isCollapsed: methodStepCollapsed,
onCollapsedClick: () => base.settingsCollapsed ? base.handleSettingsReset() : setMethodCollapsed(!methodCollapsed),
tooltip: modeTips,
content: (
<RedactModeSelector
mode={base.params.parameters.mode}
onModeChange={(mode) => base.params.updateParameter('mode', mode)}
onModeChange={handleModeChange}
disabled={base.endpointLoading}
hasFilesSelected={base.hasFiles}
hasAnyFiles={hasAnyFiles}
/>
),
}
@@ -88,12 +151,23 @@ const Redact = (props: BaseToolProps) => {
},
);
} else if (base.params.parameters.mode === 'manual') {
// Manual mode steps would go here when implemented
// Manual mode - show redaction controls
// Uses hasAnyFiles since manual mode works with any files in workbench (viewer-powered)
steps.push({
title: t("redact.manual.controlsTitle", "Manual Redaction Controls"),
isCollapsed: false,
onCollapsedClick: () => {},
tooltip: manualTips,
content: <ManualRedactionControls disabled={!hasAnyFiles} />,
});
}
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 +176,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(),
@@ -1305,7 +1305,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
disabled={applyDisabled}
onClick={onApplyChanges}
>
{t('annotation.applyChanges', 'Apply Changes')}
{t('annotation.saveChanges', 'Save Changes')}
</Button>
<SuggestedToolsSection />