mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-23 12:06:14 +00:00
Initial set up
This commit is contained in:
parent
3b87ca0c3c
commit
a70472b172
3378
frontend/package-lock.json
generated
3378
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,21 +6,23 @@
|
|||||||
"proxy": "http://localhost:8080",
|
"proxy": "http://localhost:8080",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||||
"@embedpdf/core": "^1.1.1",
|
"@embedpdf/core": "^1.2.1",
|
||||||
"@embedpdf/engines": "^1.1.1",
|
"@embedpdf/engines": "^1.2.1",
|
||||||
"@embedpdf/plugin-interaction-manager": "^1.1.1",
|
"@embedpdf/plugin-annotation": "^1.2.1",
|
||||||
"@embedpdf/plugin-loader": "^1.1.1",
|
"@embedpdf/plugin-history": "^1.2.1",
|
||||||
"@embedpdf/plugin-pan": "^1.1.1",
|
"@embedpdf/plugin-interaction-manager": "^1.2.1",
|
||||||
"@embedpdf/plugin-render": "^1.1.1",
|
"@embedpdf/plugin-loader": "^1.2.1",
|
||||||
"@embedpdf/plugin-rotate": "^1.1.1",
|
"@embedpdf/plugin-pan": "^1.2.1",
|
||||||
"@embedpdf/plugin-scroll": "^1.1.1",
|
"@embedpdf/plugin-render": "^1.2.1",
|
||||||
"@embedpdf/plugin-search": "^1.1.1",
|
"@embedpdf/plugin-rotate": "^1.2.1",
|
||||||
"@embedpdf/plugin-selection": "^1.1.1",
|
"@embedpdf/plugin-scroll": "^1.2.1",
|
||||||
"@embedpdf/plugin-spread": "^1.1.1",
|
"@embedpdf/plugin-search": "^1.2.1",
|
||||||
"@embedpdf/plugin-thumbnail": "^1.1.1",
|
"@embedpdf/plugin-selection": "^1.2.1",
|
||||||
"@embedpdf/plugin-tiling": "^1.1.1",
|
"@embedpdf/plugin-spread": "^1.2.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.1.1",
|
"@embedpdf/plugin-thumbnail": "^1.2.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.1.1",
|
"@embedpdf/plugin-tiling": "^1.2.1",
|
||||||
|
"@embedpdf/plugin-viewport": "^1.2.1",
|
||||||
|
"@embedpdf/plugin-zoom": "^1.2.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
|
@ -14,6 +14,7 @@ import "./styles/cookieconsent.css";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
import { ViewerProvider } from "./contexts/ViewerContext";
|
||||||
|
import { SignatureProvider } from "./contexts/SignatureContext";
|
||||||
|
|
||||||
// Import file ID debugging helpers (development only)
|
// Import file ID debugging helpers (development only)
|
||||||
import "./utils/fileIdSafety";
|
import "./utils/fileIdSafety";
|
||||||
@ -45,9 +46,11 @@ export default function App() {
|
|||||||
<ToolWorkflowProvider>
|
<ToolWorkflowProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<ViewerProvider>
|
<ViewerProvider>
|
||||||
<RightRailProvider>
|
<SignatureProvider>
|
||||||
<HomePage />
|
<RightRailProvider>
|
||||||
</RightRailProvider>
|
<HomePage />
|
||||||
|
</RightRailProvider>
|
||||||
|
</SignatureProvider>
|
||||||
</ViewerProvider>
|
</ViewerProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</ToolWorkflowProvider>
|
</ToolWorkflowProvider>
|
||||||
|
263
frontend/src/components/tools/sign/SignSettings.tsx
Normal file
263
frontend/src/components/tools/sign/SignSettings.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert } from '@mantine/core';
|
||||||
|
import ButtonSelector from "../../shared/ButtonSelector";
|
||||||
|
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
||||||
|
|
||||||
|
interface SignSettingsProps {
|
||||||
|
parameters: SignParameters;
|
||||||
|
onParameterChange: <K extends keyof SignParameters>(key: K, value: SignParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
onActivateDrawMode?: () => void;
|
||||||
|
onActivateSignaturePlacement?: () => void;
|
||||||
|
onDeactivateSignature?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature }: SignSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [signatureImage, setSignatureImage] = useState<File | null>(null);
|
||||||
|
|
||||||
|
// Drawing functions for signature canvas
|
||||||
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
setIsDrawing(true);
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawing || !canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
if (!isDrawing || disabled) return;
|
||||||
|
|
||||||
|
setIsDrawing(false);
|
||||||
|
|
||||||
|
// Save canvas as signature data
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const dataURL = canvasRef.current.toDataURL('image/png');
|
||||||
|
console.log('Saving canvas signature data:', dataURL.substring(0, 50) + '...');
|
||||||
|
onParameterChange('signatureData', dataURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCanvas = () => {
|
||||||
|
if (!canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
onParameterChange('signatureData', undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle signature image upload
|
||||||
|
const handleSignatureImageChange = (file: File | null) => {
|
||||||
|
console.log('Image file selected:', file);
|
||||||
|
if (file && !disabled) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
console.log('Image loaded, saving to signatureData, length:', (e.target.result as string).length);
|
||||||
|
onParameterChange('signatureData', e.target.result as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
setSignatureImage(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize canvas
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (canvasRef.current && parameters.signatureType === 'draw') {
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.strokeStyle = '#000000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [parameters.signatureType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Signature Type Selection */}
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{t('sign.type.title', 'Signature Type')}
|
||||||
|
</Text>
|
||||||
|
<ButtonSelector
|
||||||
|
value={parameters.signatureType}
|
||||||
|
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw')}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'draw',
|
||||||
|
label: t('sign.type.draw', 'Draw'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'image',
|
||||||
|
label: t('sign.type.image', 'Image'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'text',
|
||||||
|
label: t('sign.type.text', 'Text'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signature Creation based on type */}
|
||||||
|
{parameters.signatureType === 'draw' && (
|
||||||
|
<Paper withBorder p="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500}>{t('sign.draw.title', 'Draw your signature')}</Text>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="compact-sm"
|
||||||
|
onClick={clearCanvas}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{t('sign.draw.clear', 'Clear')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={400}
|
||||||
|
height={150}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: disabled ? 'default' : 'crosshair',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
}}
|
||||||
|
onMouseDown={startDrawing}
|
||||||
|
onMouseMove={draw}
|
||||||
|
onMouseUp={stopDrawing}
|
||||||
|
onMouseLeave={stopDrawing}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('sign.draw.hint', 'Click and drag to draw your signature')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parameters.signatureType === 'image' && (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<FileInput
|
||||||
|
label={t('sign.image.label', 'Upload signature image')}
|
||||||
|
placeholder={t('sign.image.placeholder', 'Select image file')}
|
||||||
|
accept="image/*"
|
||||||
|
value={signatureImage}
|
||||||
|
onChange={(file) => {
|
||||||
|
console.log('FileInput onChange triggered with file:', file);
|
||||||
|
handleSignatureImageChange(file);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parameters.signatureType === 'text' && (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInput
|
||||||
|
label={t('sign.text.name', 'Signer Name')}
|
||||||
|
placeholder={t('sign.text.placeholder', 'Enter your full name')}
|
||||||
|
value={parameters.signerName || ''}
|
||||||
|
onChange={(e) => onParameterChange('signerName', e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Instructions for placing signature */}
|
||||||
|
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||||
|
<Text size="sm">
|
||||||
|
{parameters.signatureType === 'draw' && t('sign.instructions.draw', 'Draw your signature above, then click "Draw Directly on PDF" to draw live, or "Place Canvas Signature" to place your drawn signature.')}
|
||||||
|
{parameters.signatureType === 'image' && t('sign.instructions.image', 'Upload your signature image above, then click "Activate Image Placement" to place it on the PDF.')}
|
||||||
|
{parameters.signatureType === 'text' && t('sign.instructions.text', 'Enter your name above, then click "Activate Text Signature" to place it on the PDF.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group mt="sm" gap="sm">
|
||||||
|
{/* Universal activation button */}
|
||||||
|
{((parameters.signatureType === 'draw' && parameters.signatureData) ||
|
||||||
|
(parameters.signatureType === 'image' && parameters.signatureData) ||
|
||||||
|
(parameters.signatureType === 'text' && parameters.signerName)) && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onActivateSignaturePlacement) {
|
||||||
|
onActivateSignaturePlacement();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{t('sign.activate', 'Activate Signature Placement')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Draw directly mode for draw type */}
|
||||||
|
{parameters.signatureType === 'draw' && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (onActivateDrawMode) {
|
||||||
|
onActivateDrawMode();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{t('sign.activate.draw', 'Draw Directly on PDF')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Universal deactivate button */}
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
if (onDeactivateSignature) {
|
||||||
|
onDeactivateSignature();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{t('sign.deactivate', 'Stop Placing Signatures')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignSettings;
|
@ -9,6 +9,8 @@ import { useViewer } from "../../contexts/ViewerContext";
|
|||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||||
|
import { useNavigationState } from '../../contexts/NavigationContext';
|
||||||
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
@ -33,6 +35,13 @@ const EmbedPdfViewerContent = ({
|
|||||||
const zoomState = getZoomState();
|
const zoomState = getZoomState();
|
||||||
const spreadState = getSpreadState();
|
const spreadState = getSpreadState();
|
||||||
|
|
||||||
|
// Check if we're in signature mode
|
||||||
|
const { selectedTool } = useNavigationState();
|
||||||
|
const isSignatureMode = selectedTool === 'sign';
|
||||||
|
|
||||||
|
// Get signature context
|
||||||
|
const { signatureApiRef } = useSignature();
|
||||||
|
|
||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
@ -178,6 +187,12 @@ const EmbedPdfViewerContent = ({
|
|||||||
<LocalEmbedPDF
|
<LocalEmbedPDF
|
||||||
file={effectiveFile.file}
|
file={effectiveFile.file}
|
||||||
url={effectiveFile.url}
|
url={effectiveFile.url}
|
||||||
|
enableSignature={isSignatureMode}
|
||||||
|
signatureApiRef={signatureApiRef as React.RefObject<any>}
|
||||||
|
onSignatureAdded={(annotation) => {
|
||||||
|
console.log('Signature added:', annotation);
|
||||||
|
// Future: Handle signature completion
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
@ -18,6 +18,11 @@ import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
|||||||
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
||||||
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
||||||
import { Rotation } from '@embedpdf/models';
|
import { Rotation } from '@embedpdf/models';
|
||||||
|
|
||||||
|
// Import annotation plugins
|
||||||
|
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
|
||||||
|
import { AnnotationLayer, AnnotationPluginPackage, useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
|
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||||
import { CustomSearchLayer } from './CustomSearchLayer';
|
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||||
import ToolLoadingFallback from '../tools/ToolLoadingFallback';
|
import ToolLoadingFallback from '../tools/ToolLoadingFallback';
|
||||||
@ -29,13 +34,17 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
|
|||||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||||
|
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
|
enableSignature?: boolean;
|
||||||
|
onSignatureAdded?: (annotation: any) => void;
|
||||||
|
signatureApiRef?: React.RefObject<SignatureAPI>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef }: LocalEmbedPDFProps) {
|
||||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// Convert File to URL if needed
|
// Convert File to URL if needed
|
||||||
@ -78,6 +87,17 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
// Register selection plugin (depends on InteractionManager)
|
// Register selection plugin (depends on InteractionManager)
|
||||||
createPluginRegistration(SelectionPluginPackage),
|
createPluginRegistration(SelectionPluginPackage),
|
||||||
|
|
||||||
|
// Register history plugin for undo/redo (recommended for annotations)
|
||||||
|
...(enableSignature ? [createPluginRegistration(HistoryPluginPackage)] : []),
|
||||||
|
|
||||||
|
// Register annotation plugin (depends on InteractionManager, Selection, History)
|
||||||
|
...(enableSignature ? [createPluginRegistration(AnnotationPluginPackage, {
|
||||||
|
annotationAuthor: 'Digital Signature',
|
||||||
|
autoCommit: true,
|
||||||
|
deactivateToolAfterCreate: false,
|
||||||
|
selectAfterCreate: true,
|
||||||
|
})] : []),
|
||||||
|
|
||||||
// Register pan plugin (depends on Viewport, InteractionManager)
|
// Register pan plugin (depends on Viewport, InteractionManager)
|
||||||
createPluginRegistration(PanPluginPackage, {
|
createPluginRegistration(PanPluginPackage, {
|
||||||
defaultMode: 'mobile', // Try mobile mode which might be more permissive
|
defaultMode: 'mobile', // Try mobile mode which might be more permissive
|
||||||
@ -161,7 +181,52 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0
|
minWidth: 0
|
||||||
}}>
|
}}>
|
||||||
<EmbedPDF engine={engine} plugins={plugins}>
|
<EmbedPDF
|
||||||
|
engine={engine}
|
||||||
|
plugins={plugins}
|
||||||
|
onInitialized={enableSignature ? async (registry) => {
|
||||||
|
const annotationPlugin = registry.getPlugin('annotation');
|
||||||
|
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||||
|
|
||||||
|
const annotationApi = annotationPlugin.provides();
|
||||||
|
if (!annotationApi) return;
|
||||||
|
|
||||||
|
// Add custom signature stamp tool for image signatures
|
||||||
|
annotationApi.addTool({
|
||||||
|
id: 'signatureStamp',
|
||||||
|
name: 'Digital Signature',
|
||||||
|
interaction: { exclusive: false, cursor: 'copy' },
|
||||||
|
matchScore: () => 0,
|
||||||
|
defaults: {
|
||||||
|
type: PdfAnnotationSubtype.STAMP,
|
||||||
|
// Image will be set dynamically when signature is created
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom ink signature tool for drawn signatures
|
||||||
|
annotationApi.addTool({
|
||||||
|
id: 'signatureInk',
|
||||||
|
name: 'Signature Draw',
|
||||||
|
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||||
|
matchScore: () => 0,
|
||||||
|
defaults: {
|
||||||
|
type: PdfAnnotationSubtype.INK,
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 1.0,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for annotation events to notify parent component
|
||||||
|
if (onSignatureAdded) {
|
||||||
|
annotationApi.onAnnotationEvent((event: any) => {
|
||||||
|
if (event.type === 'create' && event.committed) {
|
||||||
|
onSignatureAdded(event.annotation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} : undefined}
|
||||||
|
>
|
||||||
<ZoomAPIBridge />
|
<ZoomAPIBridge />
|
||||||
<ScrollAPIBridge />
|
<ScrollAPIBridge />
|
||||||
<SelectionAPIBridge />
|
<SelectionAPIBridge />
|
||||||
@ -170,6 +235,7 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
<SearchAPIBridge />
|
<SearchAPIBridge />
|
||||||
<ThumbnailAPIBridge />
|
<ThumbnailAPIBridge />
|
||||||
<RotateAPIBridge />
|
<RotateAPIBridge />
|
||||||
|
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||||
<GlobalPointerProvider>
|
<GlobalPointerProvider>
|
||||||
<Viewport
|
<Viewport
|
||||||
style={{
|
style={{
|
||||||
@ -213,6 +279,18 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
|
|
||||||
{/* Selection layer for text interaction */}
|
{/* Selection layer for text interaction */}
|
||||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
|
{/* Annotation layer for signatures (only when enabled) */}
|
||||||
|
{enableSignature && (
|
||||||
|
<AnnotationLayer
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
scale={scale}
|
||||||
|
pageWidth={width}
|
||||||
|
pageHeight={height}
|
||||||
|
rotation={rotation || 0}
|
||||||
|
selectionOutlineColor="#007ACC"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PagePointerProvider>
|
</PagePointerProvider>
|
||||||
</Rotate>
|
</Rotate>
|
||||||
|
315
frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx
Normal file
315
frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { createPluginRegistration } from '@embedpdf/core';
|
||||||
|
import { EmbedPDF } from '@embedpdf/core/react';
|
||||||
|
import { usePdfiumEngine } from '@embedpdf/engines/react';
|
||||||
|
|
||||||
|
// Import the essential plugins
|
||||||
|
import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react';
|
||||||
|
import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react';
|
||||||
|
import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react';
|
||||||
|
import { RenderPluginPackage } from '@embedpdf/plugin-render/react';
|
||||||
|
import { ZoomPluginPackage } from '@embedpdf/plugin-zoom/react';
|
||||||
|
import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react';
|
||||||
|
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react';
|
||||||
|
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
||||||
|
import { PanPluginPackage } from '@embedpdf/plugin-pan/react';
|
||||||
|
import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
|
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
||||||
|
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
||||||
|
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
||||||
|
import { Rotation } from '@embedpdf/models';
|
||||||
|
|
||||||
|
// Import annotation plugins
|
||||||
|
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
|
||||||
|
import { AnnotationLayer, AnnotationPluginPackage, useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
|
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||||
|
|
||||||
|
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||||
|
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||||
|
import ToolLoadingFallback from '../tools/ToolLoadingFallback';
|
||||||
|
import { Center, Stack, Text } from '@mantine/core';
|
||||||
|
import { ScrollAPIBridge } from './ScrollAPIBridge';
|
||||||
|
import { SelectionAPIBridge } from './SelectionAPIBridge';
|
||||||
|
import { PanAPIBridge } from './PanAPIBridge';
|
||||||
|
import { SpreadAPIBridge } from './SpreadAPIBridge';
|
||||||
|
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||||
|
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||||
|
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||||
|
|
||||||
|
interface LocalEmbedPDFWithAnnotationsProps {
|
||||||
|
file?: File | Blob;
|
||||||
|
url?: string | null;
|
||||||
|
onAnnotationChange?: (annotations: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalEmbedPDFWithAnnotations({
|
||||||
|
file,
|
||||||
|
url,
|
||||||
|
onAnnotationChange
|
||||||
|
}: LocalEmbedPDFWithAnnotationsProps) {
|
||||||
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Convert File to URL if needed
|
||||||
|
useEffect(() => {
|
||||||
|
if (file) {
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
setPdfUrl(objectUrl);
|
||||||
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
|
} else if (url) {
|
||||||
|
setPdfUrl(url);
|
||||||
|
}
|
||||||
|
}, [file, url]);
|
||||||
|
|
||||||
|
// Create plugins configuration with annotation support
|
||||||
|
const plugins = useMemo(() => {
|
||||||
|
if (!pdfUrl) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
createPluginRegistration(LoaderPluginPackage, {
|
||||||
|
loadingOptions: {
|
||||||
|
type: 'url',
|
||||||
|
pdfFile: {
|
||||||
|
id: 'stirling-pdf-signing-viewer',
|
||||||
|
url: pdfUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createPluginRegistration(ViewportPluginPackage, {
|
||||||
|
viewportGap: 10,
|
||||||
|
}),
|
||||||
|
createPluginRegistration(ScrollPluginPackage, {
|
||||||
|
strategy: ScrollStrategy.Vertical,
|
||||||
|
initialPage: 0,
|
||||||
|
}),
|
||||||
|
createPluginRegistration(RenderPluginPackage),
|
||||||
|
|
||||||
|
// Register interaction manager (required for annotations)
|
||||||
|
createPluginRegistration(InteractionManagerPluginPackage),
|
||||||
|
|
||||||
|
// Register selection plugin (depends on InteractionManager)
|
||||||
|
createPluginRegistration(SelectionPluginPackage),
|
||||||
|
|
||||||
|
// Register history plugin for undo/redo (recommended for annotations)
|
||||||
|
createPluginRegistration(HistoryPluginPackage),
|
||||||
|
|
||||||
|
// Register annotation plugin (depends on InteractionManager, Selection, History)
|
||||||
|
createPluginRegistration(AnnotationPluginPackage, {
|
||||||
|
annotationAuthor: 'Digital Signature',
|
||||||
|
autoCommit: true,
|
||||||
|
deactivateToolAfterCreate: false,
|
||||||
|
selectAfterCreate: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Register pan plugin
|
||||||
|
createPluginRegistration(PanPluginPackage, {
|
||||||
|
defaultMode: 'mobile',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Register zoom plugin
|
||||||
|
createPluginRegistration(ZoomPluginPackage, {
|
||||||
|
defaultZoomLevel: 1.4,
|
||||||
|
minZoom: 0.2,
|
||||||
|
maxZoom: 3.0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Register tiling plugin
|
||||||
|
createPluginRegistration(TilingPluginPackage, {
|
||||||
|
tileSize: 768,
|
||||||
|
overlapPx: 5,
|
||||||
|
extraRings: 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Register spread plugin
|
||||||
|
createPluginRegistration(SpreadPluginPackage, {
|
||||||
|
defaultSpreadMode: SpreadMode.None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Register search plugin
|
||||||
|
createPluginRegistration(SearchPluginPackage),
|
||||||
|
|
||||||
|
// Register thumbnail plugin
|
||||||
|
createPluginRegistration(ThumbnailPluginPackage),
|
||||||
|
|
||||||
|
// Register rotate plugin
|
||||||
|
createPluginRegistration(RotatePluginPackage, {
|
||||||
|
defaultRotation: Rotation.Degree0,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}, [pdfUrl]);
|
||||||
|
|
||||||
|
// Initialize the engine
|
||||||
|
const { engine, isLoading, error } = usePdfiumEngine();
|
||||||
|
|
||||||
|
// Early return if no file or URL provided
|
||||||
|
if (!file && !url) {
|
||||||
|
return (
|
||||||
|
<Center h="100%" w="100%">
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<div style={{ fontSize: '24px' }}>📄</div>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
No PDF provided
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !engine || !pdfUrl) {
|
||||||
|
return <ToolLoadingFallback toolName="PDF Engine" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Center h="100%" w="100%">
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<div style={{ fontSize: '24px' }}>❌</div>
|
||||||
|
<Text c="red" size="sm" style={{ textAlign: 'center' }}>
|
||||||
|
Error loading PDF engine: {error.message}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0
|
||||||
|
}}>
|
||||||
|
<EmbedPDF
|
||||||
|
engine={engine}
|
||||||
|
plugins={plugins}
|
||||||
|
onInitialized={async (registry) => {
|
||||||
|
const annotationPlugin = registry.getPlugin('annotation');
|
||||||
|
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||||
|
|
||||||
|
const annotationApi = annotationPlugin.provides();
|
||||||
|
if (!annotationApi) return;
|
||||||
|
|
||||||
|
// Add custom signature stamp tool
|
||||||
|
annotationApi.addTool({
|
||||||
|
id: 'signatureStamp',
|
||||||
|
name: 'Digital Signature',
|
||||||
|
interaction: { exclusive: false, cursor: 'copy' },
|
||||||
|
matchScore: () => 0,
|
||||||
|
defaults: {
|
||||||
|
type: PdfAnnotationSubtype.STAMP,
|
||||||
|
// Will be set dynamically when user creates signature
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom ink signature tool
|
||||||
|
annotationApi.addTool({
|
||||||
|
id: 'signatureInk',
|
||||||
|
name: 'Signature Draw',
|
||||||
|
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||||
|
matchScore: () => 0,
|
||||||
|
defaults: {
|
||||||
|
type: PdfAnnotationSubtype.INK,
|
||||||
|
color: '#000000',
|
||||||
|
opacity: 1.0,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for annotation events to notify parent
|
||||||
|
if (onAnnotationChange) {
|
||||||
|
annotationApi.onAnnotationEvent((event: any) => {
|
||||||
|
if (event.committed) {
|
||||||
|
// Get all annotations and notify parent
|
||||||
|
// This is a simplified approach - in reality you'd need to get all annotations
|
||||||
|
onAnnotationChange([event.annotation]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ZoomAPIBridge />
|
||||||
|
<ScrollAPIBridge />
|
||||||
|
<SelectionAPIBridge />
|
||||||
|
<PanAPIBridge />
|
||||||
|
<SpreadAPIBridge />
|
||||||
|
<SearchAPIBridge />
|
||||||
|
<ThumbnailAPIBridge />
|
||||||
|
<RotateAPIBridge />
|
||||||
|
<GlobalPointerProvider>
|
||||||
|
<Viewport
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
contain: 'strict',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Scroller
|
||||||
|
renderPage={({ width, height, pageIndex, scale, rotation }: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
pageIndex: number;
|
||||||
|
scale: number;
|
||||||
|
rotation?: number;
|
||||||
|
}) => (
|
||||||
|
<Rotate pageSize={{ width, height }}>
|
||||||
|
<PagePointerProvider {...{
|
||||||
|
pageWidth: width,
|
||||||
|
pageHeight: height,
|
||||||
|
pageIndex,
|
||||||
|
scale,
|
||||||
|
rotation: rotation || 0
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
position: 'relative',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
msUserSelect: 'none'
|
||||||
|
}}
|
||||||
|
draggable={false}
|
||||||
|
onDragStart={(e) => e.preventDefault()}
|
||||||
|
onDrop={(e) => e.preventDefault()}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{/* High-resolution tile layer */}
|
||||||
|
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
|
{/* Search highlight layer */}
|
||||||
|
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
|
{/* Selection layer for text interaction */}
|
||||||
|
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
|
{/* Annotation layer for signatures */}
|
||||||
|
<AnnotationLayer
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
scale={scale}
|
||||||
|
pageWidth={width}
|
||||||
|
pageHeight={height}
|
||||||
|
rotation={rotation || 0}
|
||||||
|
selectionOutlineColor="#007ACC"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PagePointerProvider>
|
||||||
|
</Rotate>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Viewport>
|
||||||
|
</GlobalPointerProvider>
|
||||||
|
</EmbedPDF>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
174
frontend/src/components/viewer/SignatureAPIBridge.tsx
Normal file
174
frontend/src/components/viewer/SignatureAPIBridge.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import React, { useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||||
|
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
|
import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models';
|
||||||
|
import { SignParameters } from '../../hooks/tools/sign/useSignParameters';
|
||||||
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
|
|
||||||
|
export interface SignatureAPI {
|
||||||
|
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
||||||
|
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => void;
|
||||||
|
activateDrawMode: () => void;
|
||||||
|
activateSignaturePlacementMode: () => void;
|
||||||
|
deactivateTools: () => void;
|
||||||
|
applySignatureFromParameters: (params: SignParameters) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignatureAPIBridgeProps {}
|
||||||
|
|
||||||
|
export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>((props, ref) => {
|
||||||
|
const { provides: annotationApi } = useAnnotationCapability();
|
||||||
|
const { signatureConfig } = useSignature();
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
||||||
|
if (!annotationApi) return;
|
||||||
|
|
||||||
|
// Create image stamp annotation
|
||||||
|
annotationApi.createAnnotation(pageIndex, {
|
||||||
|
type: PdfAnnotationSubtype.STAMP,
|
||||||
|
rect: {
|
||||||
|
origin: { x, y },
|
||||||
|
size: { width, height }
|
||||||
|
},
|
||||||
|
author: 'Digital Signature',
|
||||||
|
subject: 'Digital Signature',
|
||||||
|
pageIndex: pageIndex,
|
||||||
|
id: uuidV4(),
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => {
|
||||||
|
if (!annotationApi) return;
|
||||||
|
|
||||||
|
// Create text annotation for signature
|
||||||
|
annotationApi.createAnnotation(pageIndex, {
|
||||||
|
type: PdfAnnotationSubtype.FREETEXT,
|
||||||
|
rect: {
|
||||||
|
origin: { x, y },
|
||||||
|
size: { width: 200, height: 50 }
|
||||||
|
},
|
||||||
|
contents: text,
|
||||||
|
author: 'Digital Signature',
|
||||||
|
fontSize: 16,
|
||||||
|
fontColor: '#000000',
|
||||||
|
fontFamily: PdfStandardFont.Helvetica,
|
||||||
|
textAlign: PdfTextAlignment.Left,
|
||||||
|
verticalAlign: PdfVerticalAlignment.Top,
|
||||||
|
opacity: 1,
|
||||||
|
pageIndex: pageIndex,
|
||||||
|
id: uuidV4(),
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
activateDrawMode: () => {
|
||||||
|
if (!annotationApi) return;
|
||||||
|
// Activate the built-in ink tool for drawing
|
||||||
|
annotationApi.setActiveTool('ink');
|
||||||
|
},
|
||||||
|
|
||||||
|
activateSignaturePlacementMode: () => {
|
||||||
|
console.log('SignatureAPIBridge.activateSignaturePlacementMode called');
|
||||||
|
console.log('annotationApi:', !!annotationApi, 'signatureConfig:', !!signatureConfig);
|
||||||
|
if (!annotationApi || !signatureConfig) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Signature type:', signatureConfig.signatureType);
|
||||||
|
if (signatureConfig.signatureType === 'text' && signatureConfig.signerName) {
|
||||||
|
console.log('Activating freetext tool');
|
||||||
|
// Use freetext tool for text signatures
|
||||||
|
annotationApi.setActiveTool('freetext');
|
||||||
|
const activeTool = annotationApi.getActiveTool();
|
||||||
|
console.log('Freetext tool activated:', activeTool);
|
||||||
|
if (activeTool && activeTool.id === 'freetext') {
|
||||||
|
annotationApi.setToolDefaults('freetext', {
|
||||||
|
contents: signatureConfig.signerName,
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: PdfStandardFont.Helvetica,
|
||||||
|
fontColor: '#000000',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (signatureConfig.signatureData) {
|
||||||
|
console.log('Activating stamp tool');
|
||||||
|
// Use stamp tool for image/canvas signatures
|
||||||
|
annotationApi.setActiveTool('stamp');
|
||||||
|
const activeTool = annotationApi.getActiveTool();
|
||||||
|
console.log('Stamp tool activated:', activeTool);
|
||||||
|
if (activeTool && activeTool.id === 'stamp') {
|
||||||
|
annotationApi.setToolDefaults('stamp', {
|
||||||
|
imageSrc: signatureConfig.signatureData,
|
||||||
|
subject: `Digital Signature - ${signatureConfig.reason || 'Document signing'}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error activating signature tool:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
deactivateTools: () => {
|
||||||
|
if (!annotationApi) return;
|
||||||
|
annotationApi.setActiveTool(null);
|
||||||
|
},
|
||||||
|
|
||||||
|
applySignatureFromParameters: (params: SignParameters) => {
|
||||||
|
if (!annotationApi || !params.signaturePosition) return;
|
||||||
|
|
||||||
|
const { x, y, width, height, page } = params.signaturePosition;
|
||||||
|
|
||||||
|
switch (params.signatureType) {
|
||||||
|
case 'image':
|
||||||
|
if (params.signatureData) {
|
||||||
|
annotationApi.createAnnotation(page, {
|
||||||
|
type: PdfAnnotationSubtype.STAMP,
|
||||||
|
rect: {
|
||||||
|
origin: { x, y },
|
||||||
|
size: { width, height }
|
||||||
|
},
|
||||||
|
author: 'Digital Signature',
|
||||||
|
subject: `Digital Signature - ${params.reason || 'Document signing'}`,
|
||||||
|
pageIndex: page,
|
||||||
|
id: uuidV4(),
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
if (params.signerName) {
|
||||||
|
annotationApi.createAnnotation(page, {
|
||||||
|
type: PdfAnnotationSubtype.FREETEXT,
|
||||||
|
rect: {
|
||||||
|
origin: { x, y },
|
||||||
|
size: { width, height }
|
||||||
|
},
|
||||||
|
contents: params.signerName,
|
||||||
|
author: 'Digital Signature',
|
||||||
|
fontSize: 16,
|
||||||
|
fontColor: '#000000',
|
||||||
|
fontFamily: PdfStandardFont.Helvetica,
|
||||||
|
textAlign: PdfTextAlignment.Left,
|
||||||
|
verticalAlign: PdfVerticalAlignment.Top,
|
||||||
|
opacity: 1,
|
||||||
|
pageIndex: page,
|
||||||
|
id: uuidV4(),
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'draw':
|
||||||
|
// For draw mode, we activate the tool and let user draw
|
||||||
|
annotationApi.setActiveTool('ink');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}), [annotationApi, signatureConfig]);
|
||||||
|
|
||||||
|
|
||||||
|
return null; // This is a bridge component with no UI
|
||||||
|
});
|
||||||
|
|
||||||
|
SignatureAPIBridge.displayName = 'SignatureAPIBridge';
|
123
frontend/src/contexts/SignatureContext.tsx
Normal file
123
frontend/src/contexts/SignatureContext.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
|
||||||
|
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
||||||
|
|
||||||
|
// Signature state interface
|
||||||
|
interface SignatureState {
|
||||||
|
// Current signature configuration from the tool
|
||||||
|
signatureConfig: SignParameters | null;
|
||||||
|
// Whether we're in signature placement mode
|
||||||
|
isPlacementMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature actions interface
|
||||||
|
interface SignatureActions {
|
||||||
|
setSignatureConfig: (config: SignParameters | null) => void;
|
||||||
|
setPlacementMode: (enabled: boolean) => void;
|
||||||
|
activateDrawMode: () => void;
|
||||||
|
deactivateDrawMode: () => void;
|
||||||
|
activateSignaturePlacementMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined context interface
|
||||||
|
interface SignatureContextValue extends SignatureState, SignatureActions {
|
||||||
|
signatureApiRef: React.RefObject<SignatureAPI | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context
|
||||||
|
const SignatureContext = createContext<SignatureContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
const initialState: SignatureState = {
|
||||||
|
signatureConfig: null,
|
||||||
|
isPlacementMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [state, setState] = useState<SignatureState>(initialState);
|
||||||
|
const signatureApiRef = useRef<SignatureAPI>(null);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const setSignatureConfig = useCallback((config: SignParameters | null) => {
|
||||||
|
console.log('SignatureContext: setSignatureConfig called with:', config);
|
||||||
|
setState(prev => {
|
||||||
|
console.log('SignatureContext: Previous state:', prev);
|
||||||
|
const newState = {
|
||||||
|
...prev,
|
||||||
|
signatureConfig: config,
|
||||||
|
};
|
||||||
|
console.log('SignatureContext: New state:', newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setPlacementMode = useCallback((enabled: boolean) => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isPlacementMode: enabled,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activateDrawMode = useCallback(() => {
|
||||||
|
if (signatureApiRef.current) {
|
||||||
|
signatureApiRef.current.activateDrawMode();
|
||||||
|
setPlacementMode(true);
|
||||||
|
}
|
||||||
|
}, [setPlacementMode]);
|
||||||
|
|
||||||
|
const deactivateDrawMode = useCallback(() => {
|
||||||
|
if (signatureApiRef.current) {
|
||||||
|
signatureApiRef.current.deactivateTools();
|
||||||
|
setPlacementMode(false);
|
||||||
|
}
|
||||||
|
}, [setPlacementMode]);
|
||||||
|
|
||||||
|
const activateSignaturePlacementMode = useCallback(() => {
|
||||||
|
console.log('SignatureContext.activateSignaturePlacementMode called');
|
||||||
|
if (signatureApiRef.current) {
|
||||||
|
console.log('Calling signatureApiRef.current.activateSignaturePlacementMode()');
|
||||||
|
signatureApiRef.current.activateSignaturePlacementMode();
|
||||||
|
setPlacementMode(true);
|
||||||
|
} else {
|
||||||
|
console.log('signatureApiRef.current is null');
|
||||||
|
}
|
||||||
|
}, [state.signatureConfig, setPlacementMode]);
|
||||||
|
|
||||||
|
|
||||||
|
// No auto-activation - all modes use manual buttons
|
||||||
|
|
||||||
|
const contextValue: SignatureContextValue = {
|
||||||
|
...state,
|
||||||
|
signatureApiRef,
|
||||||
|
setSignatureConfig,
|
||||||
|
setPlacementMode,
|
||||||
|
activateDrawMode,
|
||||||
|
deactivateDrawMode,
|
||||||
|
activateSignaturePlacementMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SignatureContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SignatureContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to use signature context
|
||||||
|
export const useSignature = (): SignatureContextValue => {
|
||||||
|
const context = useContext(SignatureContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useSignature must be used within a SignatureProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook for components that need to check if signature mode is active
|
||||||
|
export const useSignatureMode = () => {
|
||||||
|
const context = useContext(SignatureContext);
|
||||||
|
return {
|
||||||
|
isSignatureModeActive: context?.isPlacementMode || false,
|
||||||
|
hasSignatureConfig: context?.signatureConfig !== null,
|
||||||
|
};
|
||||||
|
};
|
@ -22,6 +22,7 @@ import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
|||||||
import Flatten from "../tools/Flatten";
|
import Flatten from "../tools/Flatten";
|
||||||
import Rotate from "../tools/Rotate";
|
import Rotate from "../tools/Rotate";
|
||||||
import ChangeMetadata from "../tools/ChangeMetadata";
|
import ChangeMetadata from "../tools/ChangeMetadata";
|
||||||
|
import Sign from "../tools/Sign";
|
||||||
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
||||||
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
||||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||||
@ -41,6 +42,7 @@ import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperati
|
|||||||
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
||||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
|
import { signOperationConfig } from "../hooks/tools/sign/useSignOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
@ -62,6 +64,7 @@ import MergeSettings from '../components/tools/merge/MergeSettings';
|
|||||||
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
||||||
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
|
import SignSettings from "../components/tools/sign/SignSettings";
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
@ -165,10 +168,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
sign: {
|
sign: {
|
||||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.sign.title", "Sign"),
|
name: t("home.sign.title", "Sign"),
|
||||||
component: null,
|
component: Sign,
|
||||||
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
|
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.SIGNING,
|
subcategoryId: SubcategoryId.SIGNING,
|
||||||
|
operationConfig: signOperationConfig,
|
||||||
|
settingsComponent: SignSettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Document Security
|
// Document Security
|
||||||
|
59
frontend/src/hooks/tools/sign/useSignOperation.ts
Normal file
59
frontend/src/hooks/tools/sign/useSignOperation.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation, ToolOperationHook, ToolType } from '../shared/useToolOperation';
|
||||||
|
import { SignParameters, DEFAULT_PARAMETERS } from './useSignParameters';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
|
||||||
|
// Static configuration that can be used by both the hook and automation executor
|
||||||
|
export const buildSignFormData = (params: SignParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', file);
|
||||||
|
|
||||||
|
// Add signature data if available
|
||||||
|
if (params.signatureData) {
|
||||||
|
formData.append('signatureData', params.signatureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add signature position and size
|
||||||
|
if (params.signaturePosition) {
|
||||||
|
formData.append('x', params.signaturePosition.x.toString());
|
||||||
|
formData.append('y', params.signaturePosition.y.toString());
|
||||||
|
formData.append('width', params.signaturePosition.width.toString());
|
||||||
|
formData.append('height', params.signaturePosition.height.toString());
|
||||||
|
formData.append('page', params.signaturePosition.page.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add signature type
|
||||||
|
formData.append('signatureType', params.signatureType || 'draw');
|
||||||
|
|
||||||
|
// Add other parameters
|
||||||
|
if (params.reason) {
|
||||||
|
formData.append('reason', params.reason);
|
||||||
|
}
|
||||||
|
if (params.location) {
|
||||||
|
formData.append('location', params.location);
|
||||||
|
}
|
||||||
|
if (params.signerName) {
|
||||||
|
formData.append('signerName', params.signerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const signOperationConfig = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
buildFormData: buildSignFormData,
|
||||||
|
operationType: 'sign',
|
||||||
|
endpoint: '/api/v1/security/add-signature',
|
||||||
|
filePrefix: 'signed_',
|
||||||
|
defaultParameters: DEFAULT_PARAMETERS,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useSignOperation = (): ToolOperationHook<SignParameters> => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<SignParameters>({
|
||||||
|
...signOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('sign.error.failed', 'An error occurred while signing the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
58
frontend/src/hooks/tools/sign/useSignParameters.ts
Normal file
58
frontend/src/hooks/tools/sign/useSignParameters.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useBaseParameters } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface SignaturePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignParameters {
|
||||||
|
signatureType: 'image' | 'text' | 'draw';
|
||||||
|
signatureData?: string; // Base64 encoded image or text content
|
||||||
|
signaturePosition?: SignaturePosition;
|
||||||
|
reason?: string;
|
||||||
|
location?: string;
|
||||||
|
signerName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PARAMETERS: SignParameters = {
|
||||||
|
signatureType: 'draw',
|
||||||
|
reason: 'Document signing',
|
||||||
|
location: 'Digital',
|
||||||
|
signerName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSignParameters = (parameters: SignParameters): boolean => {
|
||||||
|
// Basic validation
|
||||||
|
if (!parameters.signatureType) return false;
|
||||||
|
|
||||||
|
// If signature position is set, validate it
|
||||||
|
if (parameters.signaturePosition) {
|
||||||
|
const pos = parameters.signaturePosition;
|
||||||
|
if (pos.x < 0 || pos.y < 0 || pos.width <= 0 || pos.height <= 0 || pos.page < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For image signatures, require signature data
|
||||||
|
if (parameters.signatureType === 'image' && !parameters.signatureData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For text signatures, require signer name
|
||||||
|
if (parameters.signatureType === 'text' && !parameters.signerName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSignParameters = () => {
|
||||||
|
return useBaseParameters<SignParameters>({
|
||||||
|
defaultParameters: DEFAULT_PARAMETERS,
|
||||||
|
endpointName: 'add-signature',
|
||||||
|
validateFn: validateSignParameters,
|
||||||
|
});
|
||||||
|
};
|
107
frontend/src/tools/Sign.tsx
Normal file
107
frontend/src/tools/Sign.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import { useSignParameters } from "../hooks/tools/sign/useSignParameters";
|
||||||
|
import { useSignOperation } from "../hooks/tools/sign/useSignOperation";
|
||||||
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
import SignSettings from "../components/tools/sign/SignSettings";
|
||||||
|
import { useNavigation } from "../contexts/NavigationContext";
|
||||||
|
import { useSignature } from "../contexts/SignatureContext";
|
||||||
|
|
||||||
|
const Sign = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setWorkbench } = useNavigation();
|
||||||
|
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode } = useSignature();
|
||||||
|
|
||||||
|
// Manual sync function
|
||||||
|
const syncSignatureConfig = () => {
|
||||||
|
setSignatureConfig(base.params.parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single handler that syncs first
|
||||||
|
const handleSignaturePlacement = () => {
|
||||||
|
syncSignatureConfig();
|
||||||
|
setTimeout(() => {
|
||||||
|
activateSignaturePlacementMode();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const base = useBaseTool(
|
||||||
|
'sign',
|
||||||
|
useSignParameters,
|
||||||
|
useSignOperation,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open viewer when files are selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (base.selectedFiles.length > 0) {
|
||||||
|
setWorkbench('viewer');
|
||||||
|
}
|
||||||
|
}, [base.selectedFiles.length, setWorkbench]);
|
||||||
|
|
||||||
|
// Sync signature configuration with context
|
||||||
|
useEffect(() => {
|
||||||
|
setSignatureConfig(base.params.parameters);
|
||||||
|
}, [base.params.parameters, setSignatureConfig]);
|
||||||
|
|
||||||
|
const getSteps = () => {
|
||||||
|
const steps = [];
|
||||||
|
|
||||||
|
// Step 1: Signature Configuration
|
||||||
|
if (base.selectedFiles.length > 0 || base.operation.files.length > 0) {
|
||||||
|
steps.push({
|
||||||
|
title: t('sign.steps.configure', 'Configure Signature'),
|
||||||
|
isCollapsed: base.operation.files.length > 0,
|
||||||
|
onCollapsedClick: base.operation.files.length > 0 ? base.handleSettingsReset : undefined,
|
||||||
|
content: (
|
||||||
|
<SignSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
onActivateDrawMode={activateDrawMode}
|
||||||
|
onActivateSignaturePlacement={handleSignaturePlacement}
|
||||||
|
onDeactivateSignature={deactivateDrawMode}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.operation.files.length > 0,
|
||||||
|
},
|
||||||
|
steps: getSteps(),
|
||||||
|
executeButton: {
|
||||||
|
text: t('sign.submit', 'Sign Document'),
|
||||||
|
isVisible: base.operation.files.length === 0,
|
||||||
|
loadingText: t('loading'),
|
||||||
|
onClick: base.handleExecute,
|
||||||
|
disabled: !base.params.validateParameters() || base.selectedFiles.length === 0 || !base.endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: base.operation.files.length > 0,
|
||||||
|
operation: base.operation,
|
||||||
|
title: t('sign.results.title', 'Signature Results'),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
forceStepNumbers: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the required static methods for automation
|
||||||
|
Sign.tool = () => useSignOperation;
|
||||||
|
Sign.getDefaultParameters = () => ({
|
||||||
|
signatureType: 'draw',
|
||||||
|
reason: 'Document signing',
|
||||||
|
location: 'Digital',
|
||||||
|
signerName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Sign as ToolComponent;
|
Loading…
x
Reference in New Issue
Block a user