mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-17 11:45:05 +02:00
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:
@@ -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 =
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user