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
@@ -86,6 +86,7 @@ interface RightRailButtonWithAction {
id: string; // Unique identifier
icon?: React.ReactNode; // Icon component (omit when using render)
tooltip?: React.ReactNode; // Hover tooltip / description
active?: boolean; // Optional active state for highlight
section?: 'top' | 'middle' | 'bottom'; // Section (default: 'top')
order?: number; // Sort order (default: 0)
disabled?: boolean; // Disabled state (default: false)
@@ -100,6 +101,7 @@ interface RightRailRenderContext {
allButtonsDisabled: boolean;
action?: () => void;
triggerAction: () => void;
active: boolean;
}
```
@@ -71,6 +71,10 @@
color: var(--right-rail-icon);
}
.right-rail-icon[data-active="true"] {
color: #fff;
}
.right-rail-icon[aria-disabled="true"],
.right-rail-icon[disabled] {
color: var(--right-rail-icon-disabled) !important;
@@ -1,12 +1,18 @@
import React from 'react';
import React, { useCallback } from 'react';
import { ActionIcon } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip';
import { ViewerContext } from '@app/contexts/ViewerContext';
import { useNavigationState } from '@app/contexts/NavigationContext';
import { useSignature } from '@app/contexts/SignatureContext';
import { useFileState, useFileContext } from '@app/contexts/FileContext';
import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers';
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;
@@ -16,47 +22,162 @@ 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);
// Viewer context for PDF controls - safely handle when not available
const viewerContext = React.useContext(ViewerContext);
// Check if we're in sign mode
// Signature context for accessing drawing API
const { historyApiRef, isPlacementMode } = useSignature();
// File state for save functionality
const { state, selectors } = useFileState();
const { actions: fileActions } = useFileContext();
const activeFiles = selectors.getFiles();
// 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 { isRedacting: _isRedacting } = useRedactionMode();
const { requestNavigation, setHasUnsavedChanges } = useNavigationGuard();
const { setRedactionMode, activateTextSelection, setRedactionConfig, setRedactionsApplied, redactionApiRef, setActiveType } = useRedaction();
// Check if we're in any annotation tool that should disable the toggle
const isInAnnotationTool = selectedTool === 'annotate' || selectedTool === 'sign' || selectedTool === 'addImage' || selectedTool === 'addText';
// Check if we're on annotate tool to highlight the button
const isAnnotateActive = selectedTool === 'annotate';
const annotationsHidden = viewerContext ? !viewerContext.isAnnotationsVisible : false;
// Don't show any annotation controls in sign mode
if (isSignMode) {
return null;
}
// Persist annotations to file if there are unsaved changes
const saveAnnotationsIfNeeded = async () => {
if (!viewerContext?.exportActions?.saveAsCopy || currentView !== 'viewer' || !historyApiRef?.current?.canUndo()) return;
if (activeFiles.length === 0 || state.files.ids.length === 0) return;
try {
const arrayBuffer = await viewerContext.exportActions.saveAsCopy();
if (!arrayBuffer) return;
const file = new File([new Blob([arrayBuffer])], activeFiles[0].name, { type: 'application/pdf' });
const parentStub = selectors.getStirlingFileStub(state.files.ids[0]);
if (!parentStub) return;
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'redact');
await fileActions.consumeFiles([state.files.ids[0]], stirlingFiles, stubs);
// Clear unsaved changes flags after successful save
setHasUnsavedChanges(false);
setRedactionsApplied(false);
} catch (error) {
console.error('Error auto-saving annotations before redaction:', error);
}
};
const exitRedactionMode = useCallback(() => {
navActions.setToolAndWorkbench(null, 'viewer');
setLeftPanelView('toolPicker');
setRedactionMode(false);
setActiveType(null);
}, [navActions, setLeftPanelView, setRedactionMode, setActiveType]);
// Handle redaction mode toggle
const handleRedactionToggle = async () => {
if (isRedactMode) {
// Exit redaction mode
exitRedactionMode();
} else {
// Check for unsaved annotation changes
const hasAnnotationChanges = historyApiRef?.current?.canUndo() ?? false;
const enterRedactionMode = async () => {
await saveAnnotationsIfNeeded();
// 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');
setRedactionMode(true);
// Activate text selection mode after a short delay
setTimeout(() => {
const currentType = redactionApiRef.current?.getActiveType?.();
if (currentType !== 'redactSelection') {
activateTextSelection();
}
}, 200);
};
if (hasAnnotationChanges) {
requestNavigation(enterRedactionMode);
} else {
await enterRedactionMode();
}
}
};
return (
<>
{/* Annotation Visibility Toggle */}
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
{/* 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={isAnnotateActive ? "filled" : "subtle"}
color="blue"
variant={isRedactMode ? 'filled' : 'subtle'}
color={isRedactMode ? 'blue' : undefined}
radius="md"
className="right-rail-icon"
onClick={() => {
viewerContext?.toggleAnnotationsVisibility();
}}
disabled={disabled || currentView !== 'viewer' || isInAnnotationTool}
onClick={handleRedactionToggle}
disabled={disabled || currentView !== 'viewer'}
>
<LocalIcon
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
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
variant={annotationsHidden ? "filled" : "subtle"}
color={annotationsHidden ? "blue" : undefined}
radius="md"
className="right-rail-icon"
onClick={() => {
viewerContext?.toggleAnnotationsVisibility();
}}
disabled={disabled || currentView !== 'viewer' || (isInAnnotationTool && !isAnnotateActive) || isPlacementMode}
data-active={annotationsHidden ? 'true' : undefined}
aria-pressed={annotationsHidden}
>
<LocalIcon
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "preview-off-rounded"}
width="1.5rem"
height="1.5rem"
/>
</ActionIcon>
</Tooltip>
</>
);
}