mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/v2/add text (#4951)
Refactor sign to separate out add text and add image functions. Implement add text as standalone tool
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { createStampTool } from '@app/tools/stamp/createStampTool';
|
||||
|
||||
// AddText is text-only annotation (no drawing, no images, no save-to-library)
|
||||
const AddText = createStampTool({
|
||||
toolId: 'addText',
|
||||
translationScope: 'addText',
|
||||
allowedSignatureSources: ['text'],
|
||||
defaultSignatureSource: 'text',
|
||||
defaultSignatureType: 'text',
|
||||
});
|
||||
|
||||
export default AddText;
|
||||
@@ -1,208 +1,12 @@
|
||||
import { useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
|
||||
import { useSignParameters } from "@app/hooks/tools/sign/useSignParameters";
|
||||
import { useSignOperation } from "@app/hooks/tools/sign/useSignOperation";
|
||||
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "@app/types/tool";
|
||||
import SignSettings from "@app/components/tools/sign/SignSettings";
|
||||
import { useNavigation } from "@app/contexts/NavigationContext";
|
||||
import { useSignature } from "@app/contexts/SignatureContext";
|
||||
import { useFileContext } from "@app/contexts/FileContext";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { flattenSignatures } from "@app/utils/signatureFlattening";
|
||||
import { createStampTool } from '@app/tools/stamp/createStampTool';
|
||||
|
||||
const Sign = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setWorkbench } = useNavigation();
|
||||
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, signatureApiRef, getImageData, setSignaturesApplied } = useSignature();
|
||||
const { consumeFiles, selectors } = useFileContext();
|
||||
const { exportActions, getScrollState, activeFileIndex, setActiveFileIndex } = useViewer();
|
||||
const { setHasUnsavedChanges, unregisterUnsavedChangesChecker } = useNavigation();
|
||||
|
||||
// Track which signature mode was active for reactivation after save
|
||||
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
||||
|
||||
// Single handler that activates placement mode
|
||||
const handleSignaturePlacement = useCallback(() => {
|
||||
activateSignaturePlacementMode();
|
||||
}, [activateSignaturePlacementMode]);
|
||||
|
||||
// Memoized callbacks for SignSettings to prevent infinite loops
|
||||
const handleActivateDrawMode = useCallback(() => {
|
||||
activeModeRef.current = 'draw';
|
||||
activateDrawMode();
|
||||
}, [activateDrawMode]);
|
||||
|
||||
const handleActivateSignaturePlacement = useCallback(() => {
|
||||
activeModeRef.current = 'placement';
|
||||
handleSignaturePlacement();
|
||||
}, [handleSignaturePlacement]);
|
||||
|
||||
const handleDeactivateSignature = useCallback(() => {
|
||||
activeModeRef.current = null;
|
||||
deactivateDrawMode();
|
||||
}, [deactivateDrawMode]);
|
||||
|
||||
const base = useBaseTool(
|
||||
'sign',
|
||||
useSignParameters,
|
||||
useSignOperation,
|
||||
props
|
||||
);
|
||||
|
||||
const hasOpenedViewer = useRef(false);
|
||||
|
||||
// Open viewer when files are selected (only once)
|
||||
useEffect(() => {
|
||||
if (base.selectedFiles.length > 0 && !hasOpenedViewer.current) {
|
||||
setWorkbench('viewer');
|
||||
hasOpenedViewer.current = true;
|
||||
}
|
||||
}, [base.selectedFiles.length, setWorkbench]);
|
||||
|
||||
|
||||
|
||||
// Sync signature configuration with context
|
||||
useEffect(() => {
|
||||
setSignatureConfig(base.params.parameters);
|
||||
}, [base.params.parameters, setSignatureConfig]);
|
||||
|
||||
// Save signed files to the system - apply signatures using EmbedPDF and replace original
|
||||
const handleSaveToSystem = useCallback(async () => {
|
||||
try {
|
||||
// Unregister unsaved changes checker to prevent warning during apply
|
||||
unregisterUnsavedChangesChecker();
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Get the original file from FileContext using activeFileIndex
|
||||
// The viewer displays files from FileContext, not from base.selectedFiles
|
||||
const allFiles = selectors.getFiles();
|
||||
const fileIndex = activeFileIndex < allFiles.length ? activeFileIndex : 0;
|
||||
const originalFile = allFiles[fileIndex];
|
||||
|
||||
if (!originalFile) {
|
||||
console.error('No file available to replace');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the signature flattening utility
|
||||
const flattenResult = await flattenSignatures({
|
||||
signatureApiRef,
|
||||
getImageData,
|
||||
exportActions,
|
||||
selectors,
|
||||
originalFile,
|
||||
getScrollState,
|
||||
activeFileIndex
|
||||
});
|
||||
|
||||
if (flattenResult) {
|
||||
// Now consume the files - this triggers the viewer reload
|
||||
await consumeFiles(
|
||||
flattenResult.inputFileIds,
|
||||
[flattenResult.outputStirlingFile],
|
||||
[flattenResult.outputStub]
|
||||
);
|
||||
|
||||
// According to FileReducer.processFileSwap, new files are inserted at the beginning
|
||||
// So the new file will be at index 0
|
||||
setActiveFileIndex(0);
|
||||
|
||||
// Mark signatures as applied
|
||||
setSignaturesApplied(true);
|
||||
|
||||
// Deactivate signature placement mode after everything completes
|
||||
handleDeactivateSignature();
|
||||
|
||||
const hasSignatureReady = (() => {
|
||||
const params = base.params.parameters;
|
||||
switch (params.signatureType) {
|
||||
case 'canvas':
|
||||
case 'image':
|
||||
return Boolean(params.signatureData);
|
||||
case 'text':
|
||||
return Boolean(params.signerName && params.signerName.trim() !== '');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (hasSignatureReady) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// TODO: Ideally, we should trigger handleActivateSignaturePlacement when the viewer is ready.
|
||||
// However, due to current architectural constraints, we use a 150ms delay to allow the viewer to reload.
|
||||
// This value was empirically determined to be sufficient for most environments, but should be revisited.
|
||||
window.setTimeout(() => {
|
||||
handleActivateSignaturePlacement();
|
||||
}, 150);
|
||||
} else {
|
||||
handleActivateSignaturePlacement();
|
||||
}
|
||||
}
|
||||
|
||||
// File has been consumed - viewer should reload automatically via key prop
|
||||
} else {
|
||||
console.error('Signature flattening failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving signed document:', error);
|
||||
}
|
||||
}, [exportActions, base.selectedFiles, base.params.parameters, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, handleActivateSignaturePlacement, setHasUnsavedChanges, unregisterUnsavedChangesChecker, activeFileIndex, setActiveFileIndex]);
|
||||
|
||||
const getSteps = () => {
|
||||
const steps = [];
|
||||
|
||||
// Step 1: Signature Configuration - Only visible when file is loaded
|
||||
if (base.selectedFiles.length > 0) {
|
||||
steps.push({
|
||||
title: t('sign.steps.configure', 'Configure Signature'),
|
||||
isCollapsed: false,
|
||||
onCollapsedClick: undefined,
|
||||
content: (
|
||||
<SignSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
onActivateDrawMode={handleActivateDrawMode}
|
||||
onActivateSignaturePlacement={handleActivateSignaturePlacement}
|
||||
onDeactivateSignature={handleDeactivateSignature}
|
||||
onUpdateDrawSettings={updateDrawSettings}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onSave={handleSaveToSystem}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
};
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.operation.files.length > 0,
|
||||
},
|
||||
steps: getSteps(),
|
||||
review: {
|
||||
isVisible: false, // Hide review section - save moved to configure section
|
||||
operation: base.operation,
|
||||
title: t('sign.results.title', 'Signature Results'),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: () => {},
|
||||
},
|
||||
forceStepNumbers: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Add the required static methods for automation
|
||||
Sign.tool = () => useSignOperation;
|
||||
Sign.getDefaultParameters = () => ({
|
||||
signatureType: 'canvas',
|
||||
reason: 'Document signing',
|
||||
location: 'Digital',
|
||||
signerName: '',
|
||||
const Sign = createStampTool({
|
||||
toolId: 'sign',
|
||||
translationScope: 'sign',
|
||||
allowedSignatureSources: ['canvas', 'image', 'text', 'saved'],
|
||||
defaultSignatureSource: 'canvas',
|
||||
defaultSignatureType: 'canvas',
|
||||
enableApplyAction: true,
|
||||
});
|
||||
|
||||
export default Sign as ToolComponent;
|
||||
export default Sign;
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
|
||||
import SignSettings, { SignatureSource } from '@app/components/tools/sign/SignSettings';
|
||||
import { DEFAULT_PARAMETERS, useSignParameters, SignParameters } from '@app/hooks/tools/sign/useSignParameters';
|
||||
import { useSignOperation } from '@app/hooks/tools/sign/useSignOperation';
|
||||
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
|
||||
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { useNavigation } from '@app/contexts/NavigationContext';
|
||||
import { useSignature } from '@app/contexts/SignatureContext';
|
||||
import { useFileContext } from '@app/contexts/FileContext';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { flattenSignatures } from '@app/utils/signatureFlattening';
|
||||
|
||||
export type StampToolConfig = {
|
||||
toolId: ToolId;
|
||||
translationScope?: string;
|
||||
allowedSignatureSources?: SignatureSource[];
|
||||
defaultSignatureSource?: SignatureSource;
|
||||
defaultSignatureType?: SignParameters['signatureType'];
|
||||
enableApplyAction?: boolean;
|
||||
};
|
||||
|
||||
const STAMP_TOOL_DEFAULT_SOURCES: SignatureSource[] = ['canvas', 'image', 'text', 'saved'];
|
||||
|
||||
export const createStampTool = (config: StampToolConfig) => {
|
||||
const {
|
||||
toolId,
|
||||
translationScope = toolId,
|
||||
allowedSignatureSources = STAMP_TOOL_DEFAULT_SOURCES,
|
||||
defaultSignatureSource,
|
||||
defaultSignatureType,
|
||||
enableApplyAction = false,
|
||||
} = config;
|
||||
|
||||
const StampTool = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const translateTool = useCallback(
|
||||
(key: string, defaultValue: string) => t(`${translationScope}.${key}`, defaultValue),
|
||||
[t, translationScope]
|
||||
);
|
||||
const { setWorkbench, setHasUnsavedChanges, unregisterUnsavedChangesChecker } = useNavigation();
|
||||
const {
|
||||
setSignatureConfig,
|
||||
activateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
deactivateDrawMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
signatureApiRef,
|
||||
getImageData,
|
||||
setSignaturesApplied,
|
||||
} = useSignature();
|
||||
const { consumeFiles, selectors } = useFileContext();
|
||||
const { exportActions, getScrollState, activeFileIndex, setActiveFileIndex } = useViewer();
|
||||
const base = useBaseTool(
|
||||
toolId,
|
||||
useSignParameters,
|
||||
useSignOperation,
|
||||
props
|
||||
);
|
||||
|
||||
const allowedSignatureTypes = allowedSignatureSources.filter(
|
||||
(source): source is SignParameters['signatureType'] => source !== 'saved'
|
||||
);
|
||||
const enforcedSignatureType =
|
||||
defaultSignatureType ?? allowedSignatureTypes[0] ?? DEFAULT_PARAMETERS.signatureType;
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowedSignatureTypes.includes(base.params.parameters.signatureType)) {
|
||||
base.params.updateParameter('signatureType', enforcedSignatureType);
|
||||
}
|
||||
}, [allowedSignatureTypes, base.params.parameters.signatureType, base.params.updateParameter, enforcedSignatureType]);
|
||||
|
||||
const hasOpenedViewer = useRef(false);
|
||||
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
||||
|
||||
const handleSignaturePlacement = useCallback(() => {
|
||||
activateSignaturePlacementMode();
|
||||
}, [activateSignaturePlacementMode]);
|
||||
|
||||
const handleActivateDrawMode = useCallback(() => {
|
||||
activeModeRef.current = 'draw';
|
||||
activateDrawMode();
|
||||
}, [activateDrawMode]);
|
||||
|
||||
const handleActivateSignaturePlacement = useCallback(() => {
|
||||
activeModeRef.current = 'placement';
|
||||
handleSignaturePlacement();
|
||||
}, [handleSignaturePlacement]);
|
||||
|
||||
const handleDeactivateSignature = useCallback(() => {
|
||||
activeModeRef.current = null;
|
||||
deactivateDrawMode();
|
||||
}, [deactivateDrawMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (base.selectedFiles.length > 0 && !hasOpenedViewer.current) {
|
||||
setWorkbench('viewer');
|
||||
hasOpenedViewer.current = true;
|
||||
}
|
||||
}, [base.selectedFiles.length, setWorkbench]);
|
||||
|
||||
useEffect(() => {
|
||||
setSignatureConfig(base.params.parameters);
|
||||
}, [base.params.parameters, setSignatureConfig]);
|
||||
|
||||
const handleSaveToSystem = useCallback(async () => {
|
||||
try {
|
||||
unregisterUnsavedChangesChecker();
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
const allFiles = selectors.getFiles();
|
||||
const fileIndex = activeFileIndex < allFiles.length ? activeFileIndex : 0;
|
||||
const originalFile = allFiles[fileIndex];
|
||||
|
||||
if (!originalFile) {
|
||||
console.error('No file available to replace');
|
||||
return;
|
||||
}
|
||||
|
||||
const flattenResult = await flattenSignatures({
|
||||
signatureApiRef,
|
||||
getImageData,
|
||||
exportActions,
|
||||
selectors,
|
||||
originalFile,
|
||||
getScrollState,
|
||||
activeFileIndex,
|
||||
});
|
||||
|
||||
if (flattenResult) {
|
||||
await consumeFiles(
|
||||
flattenResult.inputFileIds,
|
||||
[flattenResult.outputStirlingFile],
|
||||
[flattenResult.outputStub]
|
||||
);
|
||||
|
||||
setActiveFileIndex(0);
|
||||
setSignaturesApplied(true);
|
||||
handleDeactivateSignature();
|
||||
|
||||
const hasSignatureReady = (() => {
|
||||
const params = base.params.parameters;
|
||||
switch (params.signatureType) {
|
||||
case 'canvas':
|
||||
case 'image':
|
||||
return Boolean(params.signatureData);
|
||||
case 'text':
|
||||
return Boolean(params.signerName && params.signerName.trim() !== '');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (hasSignatureReady) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.setTimeout(() => {
|
||||
handleActivateSignaturePlacement();
|
||||
}, 150);
|
||||
} else {
|
||||
handleActivateSignaturePlacement();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Signature flattening failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving signed document:', error);
|
||||
}
|
||||
}, [
|
||||
exportActions,
|
||||
base.selectedFiles,
|
||||
base.params.parameters,
|
||||
selectors,
|
||||
consumeFiles,
|
||||
signatureApiRef,
|
||||
getImageData,
|
||||
setWorkbench,
|
||||
activateDrawMode,
|
||||
setSignaturesApplied,
|
||||
getScrollState,
|
||||
handleDeactivateSignature,
|
||||
handleActivateSignaturePlacement,
|
||||
setHasUnsavedChanges,
|
||||
unregisterUnsavedChangesChecker,
|
||||
activeFileIndex,
|
||||
setActiveFileIndex,
|
||||
]);
|
||||
|
||||
const getSteps = () => {
|
||||
const steps = [];
|
||||
|
||||
if (base.selectedFiles.length > 0) {
|
||||
steps.push({
|
||||
title: translateTool('steps.configure', 'Configure Stamp'),
|
||||
isCollapsed: false,
|
||||
onCollapsedClick: undefined,
|
||||
content: (
|
||||
<SignSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
onActivateDrawMode={handleActivateDrawMode}
|
||||
onActivateSignaturePlacement={handleActivateSignaturePlacement}
|
||||
onDeactivateSignature={handleDeactivateSignature}
|
||||
onUpdateDrawSettings={updateDrawSettings}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onSave={enableApplyAction ? handleSaveToSystem : undefined}
|
||||
translationScope={translationScope}
|
||||
allowedSignatureSources={allowedSignatureSources}
|
||||
defaultSignatureSource={defaultSignatureSource}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
};
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.operation.files.length > 0,
|
||||
},
|
||||
steps: getSteps(),
|
||||
review: {
|
||||
isVisible: false,
|
||||
operation: base.operation,
|
||||
title: translateTool('results.title', 'Stamp Results'),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: () => {},
|
||||
},
|
||||
forceStepNumbers: true,
|
||||
});
|
||||
};
|
||||
|
||||
const StampToolComponent = StampTool as ToolComponent;
|
||||
StampToolComponent.tool = () => useSignOperation;
|
||||
StampToolComponent.getDefaultParameters = () => ({
|
||||
...DEFAULT_PARAMETERS,
|
||||
signatureType: config.defaultSignatureType ?? DEFAULT_PARAMETERS.signatureType,
|
||||
});
|
||||
|
||||
return StampToolComponent;
|
||||
};
|
||||
Reference in New Issue
Block a user