Restructure frontend code to allow for extensions (#4721)

# Description of Changes
Move frontend code into `core` folder and add infrastructure for
`proprietary` folder to include premium, non-OSS features
This commit is contained in:
James Brunton
2025-10-28 10:29:36 +00:00
committed by GitHub
parent 960d48f80c
commit d2b38ef4b8
725 changed files with 2485 additions and 2226 deletions

View File

@@ -0,0 +1,107 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useAddAttachmentsParameters } from "@app/hooks/tools/addAttachments/useAddAttachmentsParameters";
import { useAddAttachmentsOperation } from "@app/hooks/tools/addAttachments/useAddAttachmentsOperation";
import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps";
import AddAttachmentsSettings from "@app/components/tools/addAttachments/AddAttachmentsSettings";
const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const params = useAddAttachmentsParameters();
const operation = useAddAttachmentsOperation();
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-attachments");
useEffect(() => {
operation.resetResults();
onPreviewFile?.(null);
}, [params.parameters]);
const handleExecute = async () => {
try {
await operation.executeOperation(params.parameters, selectedFiles);
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
onError?.(error?.message || t("AddAttachmentsRequest.error.failed", "Add attachments operation failed"));
}
};
const hasFiles = selectedFiles.length > 0;
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
enum AddAttachmentsStep {
NONE = 'none',
ATTACHMENTS = 'attachments'
}
const accordion = useAccordionSteps<AddAttachmentsStep>({
noneValue: AddAttachmentsStep.NONE,
initialStep: AddAttachmentsStep.ATTACHMENTS,
stateConditions: {
hasFiles,
hasResults: false // Don't collapse when there are results for add attachments
},
afterResults: () => {
operation.resetResults();
onPreviewFile?.(null);
}
});
const getSteps = () => {
const steps: any[] = [];
// Step 1: Attachments Selection
steps.push({
title: t("AddAttachmentsRequest.attachments", "Select Attachments"),
isCollapsed: accordion.getCollapsedState(AddAttachmentsStep.ATTACHMENTS),
onCollapsedClick: () => accordion.handleStepToggle(AddAttachmentsStep.ATTACHMENTS),
isVisible: true,
content: (
<AddAttachmentsSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
/>
),
});
return steps;
};
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: getSteps(),
executeButton: {
text: t('AddAttachmentsRequest.submit', 'Add Attachments'),
isVisible: !hasResults,
loadingText: t('loading'),
onClick: handleExecute,
disabled: !params.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: operation,
title: t('AddAttachmentsRequest.results.title', 'Attachment Results'),
onFileClick: (file) => onPreviewFile?.(file),
onUndo: async () => {
await operation.undoOperation();
onPreviewFile?.(null);
},
},
});
};
AddAttachments.tool = () => useAddAttachmentsOperation;
export default AddAttachments as ToolComponent;

View File

@@ -0,0 +1,126 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useAddPageNumbersParameters } from "@app/components/tools/addPageNumbers/useAddPageNumbersParameters";
import { useAddPageNumbersOperation } from "@app/components/tools/addPageNumbers/useAddPageNumbersOperation";
import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps";
import AddPageNumbersPositionSettings from "@app/components/tools/addPageNumbers/AddPageNumbersPositionSettings";
import AddPageNumbersAppearanceSettings from "@app/components/tools/addPageNumbers/AddPageNumbersAppearanceSettings";
const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const params = useAddPageNumbersParameters();
const operation = useAddPageNumbersOperation();
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-page-numbers");
useEffect(() => {
operation.resetResults();
onPreviewFile?.(null);
}, [params.parameters]);
const handleExecute = async () => {
try {
await operation.executeOperation(params.parameters, selectedFiles);
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
onError?.(error?.message || t("addPageNumbers.error.failed", "Add page numbers operation failed"));
}
};
const hasFiles = selectedFiles.length > 0;
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
enum AddPageNumbersStep {
NONE = 'none',
POSITION_AND_PAGES = 'position_and_pages',
CUSTOMIZE = 'customize'
}
const accordion = useAccordionSteps<AddPageNumbersStep>({
noneValue: AddPageNumbersStep.NONE,
initialStep: AddPageNumbersStep.POSITION_AND_PAGES,
stateConditions: {
hasFiles,
hasResults
},
afterResults: () => {
operation.resetResults();
onPreviewFile?.(null);
}
});
const getSteps = () => {
const steps: any[] = [];
// Step 1: Position Selection & Pages/Starting Number
steps.push({
title: t("addPageNumbers.positionAndPages", "Position & Pages"),
isCollapsed: accordion.getCollapsedState(AddPageNumbersStep.POSITION_AND_PAGES),
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.POSITION_AND_PAGES),
isVisible: hasFiles || hasResults,
content: (
<AddPageNumbersPositionSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
file={selectedFiles[0] || null}
showQuickGrid={true}
/>
),
});
// Step 2: Customize Appearance
steps.push({
title: t("addPageNumbers.customize", "Customize Appearance"),
isCollapsed: accordion.getCollapsedState(AddPageNumbersStep.CUSTOMIZE),
onCollapsedClick: () => accordion.handleStepToggle(AddPageNumbersStep.CUSTOMIZE),
isVisible: hasFiles || hasResults,
content: (
<AddPageNumbersAppearanceSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
/>
),
});
return steps;
};
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: getSteps(),
executeButton: {
text: t('addPageNumbers.submit', 'Add Page Numbers'),
isVisible: !hasResults,
loadingText: t('loading'),
onClick: handleExecute,
disabled: !params.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: operation,
title: t('addPageNumbers.results.title', 'Page Number Results'),
onFileClick: (file) => onPreviewFile?.(file),
onUndo: async () => {
await operation.undoOperation();
onPreviewFile?.(null);
},
},
});
};
AddPageNumbers.tool = () => useAddPageNumbersOperation;
export default AddPageNumbers as ToolComponent;

View File

@@ -0,0 +1,121 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import AddPasswordSettings from "@app/components/tools/addPassword/AddPasswordSettings";
import ChangePermissionsSettings from "@app/components/tools/changePermissions/ChangePermissionsSettings";
import { useAddPasswordParameters } from "@app/hooks/tools/addPassword/useAddPasswordParameters";
import { useAddPasswordOperation } from "@app/hooks/tools/addPassword/useAddPasswordOperation";
import { useAddPasswordTips } from "@app/components/tooltips/useAddPasswordTips";
import { useAddPasswordPermissionsTips } from "@app/components/tooltips/useAddPasswordPermissionsTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const [collapsedPermissions, setCollapsedPermissions] = useState(true);
const addPasswordParams = useAddPasswordParameters();
const addPasswordOperation = useAddPasswordOperation();
const addPasswordTips = useAddPasswordTips();
const addPasswordPermissionsTips = useAddPasswordPermissionsTips();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(addPasswordParams.getEndpointName());
useEffect(() => {
addPasswordOperation.resetResults();
onPreviewFile?.(null);
}, [addPasswordParams.parameters]);
const handleAddPassword = async () => {
try {
await addPasswordOperation.executeOperation(addPasswordParams.fullParameters, selectedFiles);
if (addPasswordOperation.files && onComplete) {
onComplete(addPasswordOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : t("addPassword.error.failed", "Add password operation failed"));
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "addPassword");
};
const handleSettingsReset = () => {
addPasswordOperation.resetResults();
onPreviewFile?.(null);
};
const handleUndo = async () => {
await addPasswordOperation.undoOperation();
onPreviewFile?.(null);
};
const hasFiles = selectedFiles.length > 0;
const hasResults = addPasswordOperation.files.length > 0 || addPasswordOperation.downloadUrl !== null;
const passwordsCollapsed = !hasFiles || hasResults;
const permissionsCollapsed = collapsedPermissions || hasResults;
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: [
{
title: t("addPassword.passwords.stepTitle", "Passwords & Encryption"),
isCollapsed: passwordsCollapsed,
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
tooltip: addPasswordTips,
content: (
<AddPasswordSettings
parameters={addPasswordParams.parameters}
onParameterChange={addPasswordParams.updateParameter}
disabled={endpointLoading}
/>
),
},
{
title: t("changePermissions.title", "Document Permissions"),
isCollapsed: permissionsCollapsed,
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedPermissions(!collapsedPermissions),
content: (
<ChangePermissionsSettings
parameters={addPasswordParams.permissions.parameters}
onParameterChange={addPasswordParams.permissions.updateParameter}
disabled={endpointLoading}
/>
),
tooltip: addPasswordPermissionsTips,
},
],
executeButton: {
text: t("addPassword.submit", "Encrypt"),
isVisible: !hasResults,
loadingText: t("loading"),
onClick: handleAddPassword,
disabled: !addPasswordParams.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: addPasswordOperation,
title: t("addPassword.results.title", "Encrypted PDFs"),
onFileClick: handleThumbnailClick,
onUndo: handleUndo,
},
});
};
export default AddPassword as ToolComponent;

View File

@@ -0,0 +1,188 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useAddStampParameters } from "@app/components/tools/addStamp/useAddStampParameters";
import { useAddStampOperation } from "@app/components/tools/addStamp/useAddStampOperation";
import { Stack, Text } from "@mantine/core";
import StampPreview from "@app/components/tools/addStamp/StampPreview";
import styles from "@app/components/tools/addStamp/StampPreview.module.css";
import ButtonSelector from "@app/components/shared/ButtonSelector";
import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps";
import ObscuredOverlay from "@app/components/shared/ObscuredOverlay";
import StampSetupSettings from "@app/components/tools/addStamp/StampSetupSettings";
import StampPositionFormattingSettings from "@app/components/tools/addStamp/StampPositionFormattingSettings";
const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false);
const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true);
const params = useAddStampParameters();
const operation = useAddStampOperation();
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-stamp");
useEffect(() => {
operation.resetResults();
onPreviewFile?.(null);
}, [params.parameters]);
const handleExecute = async () => {
try {
await operation.executeOperation(params.parameters, selectedFiles);
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
onError?.(error?.message || t("AddStampRequest.error.failed", "Add stamp operation failed"));
}
};
const hasFiles = selectedFiles.length > 0;
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
enum AddStampStep {
NONE = 'none',
STAMP_SETUP = 'stampSetup',
POSITION_FORMATTING = 'positionFormatting'
}
const accordion = useAccordionSteps<AddStampStep>({
noneValue: AddStampStep.NONE,
initialStep: AddStampStep.STAMP_SETUP,
stateConditions: {
hasFiles,
hasResults
},
afterResults: () => {
operation.resetResults();
onPreviewFile?.(null);
}
});
const getSteps = () => {
const steps: any[] = [];
// Step 1: Stamp Setup
steps.push({
title: t("AddStampRequest.stampSetup", "Stamp Setup"),
isCollapsed: accordion.getCollapsedState(AddStampStep.STAMP_SETUP),
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.STAMP_SETUP),
isVisible: hasFiles || hasResults,
content: (
<StampSetupSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
/>
),
});
// Step 2: Formatting & Position
steps.push({
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
isCollapsed: accordion.getCollapsedState(AddStampStep.POSITION_FORMATTING),
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.POSITION_FORMATTING),
isVisible: hasFiles || hasResults,
content: (
<Stack gap="md" justify="space-between">
{/* Mode toggle: Quick grid vs Custom drag - only show for image stamps */}
{params.parameters.stampType === 'image' && (
<ButtonSelector
value={quickPositionModeSelected ? 'quick' : 'custom'}
onChange={(v: 'quick' | 'custom') => {
const isQuick = v === 'quick';
setQuickPositionModeSelected(isQuick);
setCustomPositionModeSelected(!isQuick);
}}
options={[
{ value: 'quick', label: t('quickPosition', 'Quick Position') },
{ value: 'custom', label: t('customPosition', 'Custom Position') },
]}
disabled={endpointLoading}
buttonClassName={styles.modeToggleButton}
textClassName={styles.modeToggleButtonText}
/>
)}
{params.parameters.stampType === 'image' && customPositionModeSelected && (
<div className={styles.informationContainer}>
<Text className={styles.informationText}>{t('AddStampRequest.customPosition', 'Drag the stamp to the desired location in the preview window.')}</Text>
</div>
)}
{params.parameters.stampType === 'image' && !customPositionModeSelected && (
<div className={styles.informationContainer}>
<Text className={styles.informationText}>{t('AddStampRequest.quickPosition', 'Select a position on the page to place the stamp.')}</Text>
</div>
)}
<StampPositionFormattingSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
/>
{/* Unified preview wrapped with obscured overlay if no stamp selected */}
<ObscuredOverlay
obscured={
accordion.currentStep === AddStampStep.POSITION_FORMATTING &&
((params.parameters.stampType === 'text' && params.parameters.stampText.trim().length === 0) ||
(params.parameters.stampType === 'image' && !params.parameters.stampImage))
}
overlayMessage={
<Text size="sm" c="white" fw={600}>
{t('AddStampRequest.noStampSelected', 'No stamp selected. Return to Step 1.')}
</Text>
}
>
<StampPreview
parameters={params.parameters}
onParameterChange={params.updateParameter}
file={selectedFiles[0] || null}
showQuickGrid={params.parameters.stampType === 'text' ? true : quickPositionModeSelected}
/>
</ObscuredOverlay>
</Stack>
),
});
return steps;
};
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: getSteps(),
executeButton: {
text: t('AddStampRequest.submit', 'Add Stamp'),
isVisible: !hasResults,
loadingText: t('loading'),
onClick: handleExecute,
disabled: !params.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: operation,
title: t('AddStampRequest.results.title', 'Stamp Results'),
onFileClick: (file) => onPreviewFile?.(file),
onUndo: async () => {
await operation.undoOperation();
onPreviewFile?.(null);
},
},
});
};
AddStamp.tool = () => useAddStampOperation;
export default AddStamp as ToolComponent;

View File

@@ -0,0 +1,218 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import WatermarkTypeSettings from "@app/components/tools/addWatermark/WatermarkTypeSettings";
import WatermarkWording from "@app/components/tools/addWatermark/WatermarkWording";
import WatermarkTextStyle from "@app/components/tools/addWatermark/WatermarkTextStyle";
import WatermarkImageFile from "@app/components/tools/addWatermark/WatermarkImageFile";
import WatermarkFormatting from "@app/components/tools/addWatermark/WatermarkFormatting";
import { useAddWatermarkParameters } from "@app/hooks/tools/addWatermark/useAddWatermarkParameters";
import { useAddWatermarkOperation } from "@app/hooks/tools/addWatermark/useAddWatermarkOperation";
import {
useWatermarkTypeTips,
useWatermarkWordingTips,
useWatermarkTextStyleTips,
useWatermarkFileTips,
useWatermarkFormattingTips,
} from "@app/components/tooltips/useWatermarkTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const [collapsedType, setCollapsedType] = useState(false);
const [collapsedStyle, setCollapsedStyle] = useState(true);
const [collapsedFormatting, setCollapsedFormatting] = useState(true);
const watermarkParams = useAddWatermarkParameters();
const watermarkOperation = useAddWatermarkOperation();
const watermarkTypeTips = useWatermarkTypeTips();
const watermarkWordingTips = useWatermarkWordingTips();
const watermarkTextStyleTips = useWatermarkTextStyleTips();
const watermarkFileTips = useWatermarkFileTips();
const watermarkFormattingTips = useWatermarkFormattingTips();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-watermark");
useEffect(() => {
watermarkOperation.resetResults();
onPreviewFile?.(null);
}, [watermarkParams.parameters]);
// Auto-collapse type step after selection
useEffect(() => {
if (watermarkParams.parameters.watermarkType && !collapsedType) {
setCollapsedType(true);
}
}, [watermarkParams.parameters.watermarkType]);
const handleAddWatermark = async () => {
try {
await watermarkOperation.executeOperation(watermarkParams.parameters, selectedFiles);
if (watermarkOperation.files && onComplete) {
onComplete(watermarkOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : t("watermark.error.failed", "Add watermark operation failed"));
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "watermark");
};
const handleSettingsReset = () => {
watermarkOperation.resetResults();
onPreviewFile?.(null);
};
const handleUndo = async () => {
await watermarkOperation.undoOperation();
onPreviewFile?.(null);
};
const hasFiles = selectedFiles.length > 0;
const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null;
// Dynamic step structure based on watermark type
const getSteps = () => {
const steps = [];
steps.push({
title: t("watermark.steps.type", "Watermark Type"),
isCollapsed: hasResults ? true : collapsedType,
isVisible: hasFiles || hasResults,
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedType(!collapsedType),
tooltip: watermarkTypeTips,
content: (
<WatermarkTypeSettings
watermarkType={watermarkParams.parameters.watermarkType}
onWatermarkTypeChange={(type) => watermarkParams.updateParameter("watermarkType", type)}
disabled={endpointLoading}
/>
),
});
if (hasFiles || hasResults) {
// Text watermark path
if (watermarkParams.parameters.watermarkType === "text") {
// Step 2: Wording
steps.push({
title: t("watermark.steps.wording", "Wording"),
isCollapsed: hasResults,
tooltip: watermarkWordingTips,
content: (
<WatermarkWording
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
),
});
// Step 3: Style
steps.push({
title: t("watermark.steps.textStyle", "Style"),
isCollapsed: hasResults ? true : collapsedStyle,
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedStyle(!collapsedStyle),
tooltip: watermarkTextStyleTips,
content: (
<WatermarkTextStyle
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
),
});
// Step 4: Formatting
steps.push({
title: t("watermark.steps.formatting", "Formatting"),
isCollapsed: hasResults ? true : collapsedFormatting,
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting),
tooltip: watermarkFormattingTips,
content: (
<WatermarkFormatting
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
),
});
}
// Image watermark path
if (watermarkParams.parameters.watermarkType === "image") {
// Step 2: Watermark File
steps.push({
title: t("watermark.steps.file", "Watermark File"),
isCollapsed: hasResults,
tooltip: watermarkFileTips,
content: (
<WatermarkImageFile
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
),
});
// Step 3: Formatting
steps.push({
title: t("watermark.steps.formatting", "Formatting"),
isCollapsed: hasResults ? true : collapsedFormatting,
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting),
tooltip: watermarkFormattingTips,
content: (
<WatermarkFormatting
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
),
});
}
}
return steps;
};
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: getSteps(),
executeButton: {
text: t("watermark.submit", "Add Watermark"),
isVisible: !hasResults,
loadingText: t("loading"),
onClick: handleAddWatermark,
disabled: !watermarkParams.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: watermarkOperation,
title: t("watermark.results.title", "Watermark Results"),
onFileClick: handleThumbnailClick,
onUndo: handleUndo,
},
forceStepNumbers: true,
});
};
// Static method to get the operation hook for automation
AddWatermark.tool = () => useAddWatermarkOperation;
export default AddWatermark as ToolComponent;

View File

@@ -0,0 +1,121 @@
import { useTranslation } from 'react-i18next';
import { useEffect, useMemo, useState } from 'react';
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
import { BaseToolProps, ToolComponent } from '@app/types/tool';
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
import { useAdjustContrastParameters } from '@app/hooks/tools/adjustContrast/useAdjustContrastParameters';
import { useAdjustContrastOperation } from '@app/hooks/tools/adjustContrast/useAdjustContrastOperation';
import AdjustContrastBasicSettings from '@app/components/tools/adjustContrast/AdjustContrastBasicSettings';
import AdjustContrastColorSettings from '@app/components/tools/adjustContrast/AdjustContrastColorSettings';
import AdjustContrastPreview from '@app/components/tools/adjustContrast/AdjustContrastPreview';
import { useAccordionSteps } from '@app/hooks/tools/shared/useAccordionSteps';
import NavigationArrows from '@app/components/shared/filePreview/NavigationArrows';
const AdjustContrast = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'adjustContrast',
useAdjustContrastParameters,
useAdjustContrastOperation,
props
);
enum Step { NONE='none', BASIC='basic', COLORS='colors' }
const accordion = useAccordionSteps<Step>({
noneValue: Step.NONE,
initialStep: Step.BASIC,
stateConditions: { hasFiles: base.hasFiles, hasResults: base.hasResults },
afterResults: base.handleSettingsReset
});
// Track which selected file is being previewed. Clamp when selection changes.
const [previewIndex, setPreviewIndex] = useState(0);
const totalSelected = base.selectedFiles.length;
useEffect(() => {
if (previewIndex >= totalSelected) {
setPreviewIndex(Math.max(0, totalSelected - 1));
}
}, [totalSelected, previewIndex]);
const currentFile = useMemo(() => {
return totalSelected > 0 ? base.selectedFiles[previewIndex] : null;
}, [base.selectedFiles, previewIndex, totalSelected]);
const handlePrev = () => setPreviewIndex((i) => Math.max(0, i - 1));
const handleNext = () => setPreviewIndex((i) => Math.min(totalSelected - 1, i + 1));
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t('adjustContrast.basic', 'Basic Adjustments'),
isCollapsed: accordion.getCollapsedState(Step.BASIC),
onCollapsedClick: () => accordion.handleStepToggle(Step.BASIC),
content: (
<AdjustContrastBasicSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
{
title: t('adjustContrast.adjustColors', 'Adjust Colors'),
isCollapsed: accordion.getCollapsedState(Step.COLORS),
onCollapsedClick: () => accordion.handleStepToggle(Step.COLORS),
content: (
<AdjustContrastColorSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
preview: (
<div>
<NavigationArrows
onPrevious={handlePrev}
onNext={handleNext}
disabled={totalSelected <= 1}
>
<div style={{ width: '100%' }}>
<AdjustContrastPreview
file={currentFile || null}
parameters={base.params.parameters}
/>
</div>
</NavigationArrows>
{totalSelected > 1 && (
<div style={{ textAlign: 'center', marginTop: 8, fontSize: 12, color: 'var(--text-color-muted)' }}>
{`${previewIndex + 1} of ${totalSelected}`}
</div>
)}
</div>
),
executeButton: {
text: t('adjustContrast.confirm', 'Confirm'),
isVisible: !base.hasResults,
loadingText: t('loading'),
onClick: base.handleExecute,
disabled: !base.hasFiles,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t('adjustContrast.results.title', 'Adjusted PDF'),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
forceStepNumbers: true,
});
};
export default AdjustContrast as ToolComponent;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import AdjustPageScaleSettings from "@app/components/tools/adjustPageScale/AdjustPageScaleSettings";
import { useAdjustPageScaleParameters } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
import { useAdjustPageScaleOperation } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useAdjustPageScaleTips } from "@app/components/tooltips/useAdjustPageScaleTips";
const AdjustPageScale = (props: BaseToolProps) => {
const { t } = useTranslation();
const adjustPageScaleTips = useAdjustPageScaleTips();
const base = useBaseTool(
'adjustPageScale',
useAdjustPageScaleParameters,
useAdjustPageScaleOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: adjustPageScaleTips,
content: (
<AdjustPageScaleSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("adjustPageScale.submit", "Adjust Page Scale"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("adjustPageScale.title", "Page Scale Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default AdjustPageScale as ToolComponent;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps } from "@app/types/tool";
import { useAutoRenameParameters } from "@app/hooks/tools/autoRename/useAutoRenameParameters";
import { useAutoRenameOperation } from "@app/hooks/tools/autoRename/useAutoRenameOperation";
import { useAutoRenameTips } from "@app/components/tooltips/useAutoRenameTips";
const AutoRename =(props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'"auto-rename-pdf-file',
useAutoRenameParameters,
useAutoRenameOperation,
props
);
return createToolFlow({
title: { title:t("auto-rename.title", "Auto Rename PDF"), description: t("auto-rename.description", "Auto Rename PDF"), tooltip: useAutoRenameTips()},
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [],
executeButton: {
text: t("auto-rename.submit", "Auto Rename"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("auto-rename.results.title", "Auto-Rename Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default AutoRename;

View File

@@ -0,0 +1,222 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFileSelection } from "@app/contexts/FileContext";
import { useNavigationActions } from "@app/contexts/NavigationContext";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { createFilesToolStep } from "@app/components/tools/shared/FilesToolStep";
import AutomationSelection from "@app/components/tools/automate/AutomationSelection";
import AutomationCreation from "@app/components/tools/automate/AutomationCreation";
import AutomationRun from "@app/components/tools/automate/AutomationRun";
import { useAutomateOperation } from "@app/hooks/tools/automate/useAutomateOperation";
import { BaseToolProps } from "@app/types/tool";
import { useToolRegistry } from "@app/contexts/ToolRegistryContext";
import { useSavedAutomations } from "@app/hooks/tools/automate/useSavedAutomations";
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "@app/types/automation";
import { AUTOMATION_STEPS } from "@app/constants/automation";
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const { actions } = useNavigationActions();
const { registerToolReset } = useToolWorkflow();
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
const automateOperation = useAutomateOperation();
const { regularTools: toolRegistry } = useToolRegistry();
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
// Use ref to store the latest reset function to avoid closure issues
const resetFunctionRef = React.useRef<() => void>(null);
// Update ref with latest reset function
resetFunctionRef.current = () => {
automateOperation.resetResults();
automateOperation.clearError();
setCurrentStep(AUTOMATION_STEPS.SELECTION);
setStepData({ step: AUTOMATION_STEPS.SELECTION });
};
const handleUndo = async () => {
await automateOperation.undoOperation();
onPreviewFile?.(null);
};
// Register reset function with the tool workflow context - only once on mount
useEffect(() => {
const stableResetFunction = () => {
if (resetFunctionRef.current) {
resetFunctionRef.current();
}
};
registerToolReset('automate', stableResetFunction);
}, [registerToolReset]); // Only depend on registerToolReset which should be stable
const handleStepChange = (data: AutomationStepData) => {
// If navigating away from run step, reset automation results
if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) {
automateOperation.resetResults();
}
// If navigating to selection step, always clear results
if (data.step === AUTOMATION_STEPS.SELECTION) {
automateOperation.resetResults();
automateOperation.clearError();
}
// If navigating to run step with a different automation, reset results
if (data.step === AUTOMATION_STEPS.RUN && data.automation &&
stepData.automation && data.automation.id !== stepData.automation.id) {
automateOperation.resetResults();
}
setStepData(data);
setCurrentStep(data.step);
};
const handleComplete = () => {
// Reset automation results when completing
automateOperation.resetResults();
// Reset to selection step
setCurrentStep(AUTOMATION_STEPS.SELECTION);
setStepData({ step: AUTOMATION_STEPS.SELECTION });
onComplete?.([]); // Pass empty array since automation creation doesn't produce files
};
const renderCurrentStep = () => {
switch (currentStep) {
case AUTOMATION_STEPS.SELECTION:
return (
<AutomationSelection
savedAutomations={savedAutomations}
onCreateNew={() => handleStepChange({ step: AUTOMATION_STEPS.CREATION, mode: AutomationMode.CREATE })}
onRun={(automation: AutomationConfig) => handleStepChange({ step: AUTOMATION_STEPS.RUN, automation })}
onEdit={(automation: AutomationConfig) => handleStepChange({ step: AUTOMATION_STEPS.CREATION, mode: AutomationMode.EDIT, automation })}
onDelete={async (automation: AutomationConfig) => {
try {
await deleteAutomation(automation.id);
} catch (error) {
console.error('Failed to delete automation:', error);
onError?.(`Failed to delete automation: ${automation.name}`);
}
}}
onCopyFromSuggested={async (suggestedAutomation) => {
try {
await copyFromSuggested(suggestedAutomation);
} catch (error) {
console.error('Failed to copy suggested automation:', error);
onError?.(`Failed to copy automation: ${suggestedAutomation.name}`);
}
}}
toolRegistry={toolRegistry}
/>
);
case AUTOMATION_STEPS.CREATION:
if (!stepData.mode) {
console.error('Creation mode is undefined');
return null;
}
return (
<AutomationCreation
mode={stepData.mode}
existingAutomation={stepData.automation}
onBack={() => handleStepChange({ step: AUTOMATION_STEPS.SELECTION })}
onComplete={() => {
refreshAutomations();
handleStepChange({ step: AUTOMATION_STEPS.SELECTION });
}}
toolRegistry={toolRegistry}
/>
);
case AUTOMATION_STEPS.RUN:
if (!stepData.automation) {
console.error('Automation config is undefined');
return null;
}
return (
<AutomationRun
automation={stepData.automation}
onComplete={handleComplete}
automateOperation={automateOperation}
/>
);
default:
return <div>{t('automate.invalidStep', 'Invalid step')}</div>;
}
};
const createStep = (title: string, props: any, content?: React.ReactNode) => ({
title,
...props,
content
});
// Always create files step to avoid conditional hook calls
const filesStep = createFilesToolStep(createStep, {
selectedFiles,
isCollapsed: hasResults,
});
const automationSteps = [
createStep(t('automate.selection.title', 'Automation Selection'), {
isVisible: true,
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
onCollapsedClick: () => {
// Clear results when clicking back to selection
automateOperation.resetResults();
setCurrentStep(AUTOMATION_STEPS.SELECTION);
setStepData({ step: AUTOMATION_STEPS.SELECTION });
}
}, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null),
createStep(stepData.mode === AutomationMode.EDIT
? t('automate.creation.editTitle', 'Edit Automation')
: t('automate.creation.createTitle', 'Create Automation'), {
isVisible: currentStep === AUTOMATION_STEPS.CREATION,
isCollapsed: false
}, currentStep === AUTOMATION_STEPS.CREATION ? renderCurrentStep() : null),
// Files step - only visible during run mode
{
...filesStep,
isVisible: currentStep === AUTOMATION_STEPS.RUN
},
// Run step
createStep(t('automate.run.title', 'Run Automation'), {
isVisible: currentStep === AUTOMATION_STEPS.RUN,
isCollapsed: hasResults,
}, currentStep === AUTOMATION_STEPS.RUN ? renderCurrentStep() : null)
];
return createToolFlow({
files: {
selectedFiles: currentStep === AUTOMATION_STEPS.RUN ? selectedFiles : [],
isCollapsed: currentStep !== AUTOMATION_STEPS.RUN || hasResults,
isVisible: false, // Hide the default files step since we add our own
},
steps: automationSteps,
review: {
isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN,
operation: automateOperation,
title: t('automate.reviewTitle', 'Automation Results'),
onFileClick: (file: File) => {
onPreviewFile?.(file);
actions.setWorkbench('viewer');
},
onUndo: handleUndo
}
});
};
export default Automate;

View File

@@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import BookletImpositionSettings from "@app/components/tools/bookletImposition/BookletImpositionSettings";
import { useBookletImpositionParameters } from "@app/hooks/tools/bookletImposition/useBookletImpositionParameters";
import { useBookletImpositionOperation } from "@app/hooks/tools/bookletImposition/useBookletImpositionOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useBookletImpositionTips } from "@app/components/tooltips/useBookletImpositionTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const BookletImposition = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'bookletImposition',
useBookletImpositionParameters,
useBookletImpositionOperation,
props
);
const bookletTips = useBookletImpositionTips();
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: bookletTips,
content: (
<BookletImpositionSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("bookletImposition.submit", "Create Booklet"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("bookletImposition.title", "Booklet Imposition Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default BookletImposition as ToolComponent;

View File

@@ -0,0 +1,131 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import CertificateTypeSettings from "@app/components/tools/certSign/CertificateTypeSettings";
import CertificateFormatSettings from "@app/components/tools/certSign/CertificateFormatSettings";
import CertificateFilesSettings from "@app/components/tools/certSign/CertificateFilesSettings";
import SignatureAppearanceSettings from "@app/components/tools/certSign/SignatureAppearanceSettings";
import { useCertSignParameters } from "@app/hooks/tools/certSign/useCertSignParameters";
import { useCertSignOperation } from "@app/hooks/tools/certSign/useCertSignOperation";
import { useCertificateTypeTips } from "@app/components/tooltips/useCertificateTypeTips";
import { useSignatureAppearanceTips } from "@app/components/tooltips/useSignatureAppearanceTips";
import { useSignModeTips } from "@app/components/tooltips/useSignModeTips";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const CertSign = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'certSign',
useCertSignParameters,
useCertSignOperation,
props
);
const certTypeTips = useCertificateTypeTips();
const appearanceTips = useSignatureAppearanceTips();
const signModeTips = useSignModeTips();
// Check if certificate files are configured for appearance step
const areCertFilesConfigured = () => {
const params = base.params.parameters;
// Auto mode (server certificate) - always configured
if (params.signMode === 'AUTO') {
return true;
}
// Manual mode - check for required files based on cert type
switch (params.certType) {
case 'PEM':
return !!(params.privateKeyFile && params.certFile);
case 'PKCS12':
case 'PFX':
return !!params.p12File;
case 'JKS':
return !!params.jksFile;
default:
return false;
}
};
return createToolFlow({
forceStepNumbers: true,
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("certSign.signMode.stepTitle", "Sign Mode"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: signModeTips,
content: (
<CertificateTypeSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
...(base.params.parameters.signMode === 'MANUAL' ? [{
title: t("certSign.certTypeStep.stepTitle", "Certificate Format"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: certTypeTips,
content: (
<CertificateFormatSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
}] : []),
...(base.params.parameters.signMode === 'MANUAL' ? [{
title: t("certSign.certFiles.stepTitle", "Certificate Files"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<CertificateFilesSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
}] : []),
{
title: t("certSign.appearance.stepTitle", "Signature Appearance"),
isCollapsed: base.settingsCollapsed || !areCertFilesConfigured(),
onCollapsedClick: (base.settingsCollapsed || !areCertFilesConfigured()) ? base.handleSettingsReset : undefined,
tooltip: appearanceTips,
content: (
<SignatureAppearanceSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("certSign.sign.submit", "Sign PDF"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("certSign.sign.results", "Signed PDF"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
CertSign.tool = () => useCertSignOperation;
export default CertSign as ToolComponent;

View File

@@ -0,0 +1,156 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps";
import DeleteAllStep from "@app/components/tools/changeMetadata/steps/DeleteAllStep";
import StandardMetadataStep from "@app/components/tools/changeMetadata/steps/StandardMetadataStep";
import DocumentDatesStep from "@app/components/tools/changeMetadata/steps/DocumentDatesStep";
import AdvancedOptionsStep from "@app/components/tools/changeMetadata/steps/AdvancedOptionsStep";
import { useChangeMetadataParameters } from "@app/hooks/tools/changeMetadata/useChangeMetadataParameters";
import { useChangeMetadataOperation } from "@app/hooks/tools/changeMetadata/useChangeMetadataOperation";
import { useMetadataExtraction } from "@app/hooks/tools/changeMetadata/useMetadataExtraction";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import {
useDeleteAllTips,
useStandardMetadataTips,
useDocumentDatesTips,
useAdvancedOptionsTips
} from "@app/components/tooltips/useChangeMetadataTips";
enum MetadataStep {
NONE = 'none',
DELETE_ALL = 'deleteAll',
STANDARD_METADATA = 'standardMetadata',
DOCUMENT_DATES = 'documentDates',
ADVANCED_OPTIONS = 'advancedOptions'
}
const ChangeMetadata = (props: BaseToolProps) => {
const { t } = useTranslation();
// Individual tooltips for each step
const deleteAllTips = useDeleteAllTips();
const standardMetadataTips = useStandardMetadataTips();
const documentDatesTips = useDocumentDatesTips();
const advancedOptionsTips = useAdvancedOptionsTips();
const base = useBaseTool(
'changeMetadata',
useChangeMetadataParameters,
useChangeMetadataOperation,
props,
);
// Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction(base.params);
// Accordion step management
const accordion = useAccordionSteps<MetadataStep>({
noneValue: MetadataStep.NONE,
initialStep: MetadataStep.DELETE_ALL,
stateConditions: {
hasFiles: base.hasFiles,
hasResults: base.hasResults
},
afterResults: base.handleSettingsReset,
});
// Create step objects
const createStandardMetadataStep = () => ({
title: t("changeMetadata.standardFields.title", "Standard Fields"),
isCollapsed: accordion.getCollapsedState(MetadataStep.STANDARD_METADATA),
onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.STANDARD_METADATA),
tooltip: standardMetadataTips,
content: (
<StandardMetadataStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata}
/>
),
});
const createDocumentDatesStep = () => ({
title: t("changeMetadata.dates.title", "Date Fields"),
isCollapsed: accordion.getCollapsedState(MetadataStep.DOCUMENT_DATES),
onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.DOCUMENT_DATES),
tooltip: documentDatesTips,
content: (
<DocumentDatesStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata}
/>
),
});
const createAdvancedOptionsStep = () => ({
title: t("changeMetadata.advanced.title", "Advanced Options"),
isCollapsed: accordion.getCollapsedState(MetadataStep.ADVANCED_OPTIONS),
onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.ADVANCED_OPTIONS),
tooltip: advancedOptionsTips,
content: (
<AdvancedOptionsStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata}
addCustomMetadata={base.params.addCustomMetadata}
removeCustomMetadata={base.params.removeCustomMetadata}
updateCustomMetadata={base.params.updateCustomMetadata}
/>
),
});
// Build steps array based on deleteAll state
const buildSteps = () => {
const steps = [
{
title: t("changeMetadata.deleteAll.label", "Remove Existing Metadata"),
isCollapsed: accordion.getCollapsedState(MetadataStep.DELETE_ALL),
onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.DELETE_ALL),
tooltip: deleteAllTips,
content: (
<DeleteAllStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata}
/>
),
},
];
if (!base.params.parameters.deleteAll) {
steps.push(
createStandardMetadataStep(),
createDocumentDatesStep(),
createAdvancedOptionsStep()
);
}
return steps;
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: buildSteps(),
executeButton: {
text: t("changeMetadata.submit", "Update Metadata"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("changeMetadata.results.title", "Updated PDFs"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default ChangeMetadata as ToolComponent;

View File

@@ -0,0 +1,61 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import ChangePermissionsSettings from "@app/components/tools/changePermissions/ChangePermissionsSettings";
import { useChangePermissionsParameters } from "@app/hooks/tools/changePermissions/useChangePermissionsParameters";
import { useChangePermissionsOperation } from "@app/hooks/tools/changePermissions/useChangePermissionsOperation";
import { useChangePermissionsTips } from "@app/components/tooltips/useChangePermissionsTips";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const ChangePermissions = (props: BaseToolProps) => {
const { t } = useTranslation();
const changePermissionsTips = useChangePermissionsTips();
const base = useBaseTool(
'changePermissions',
useChangePermissionsParameters,
useChangePermissionsOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("changePermissions.title", "Document Permissions"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: changePermissionsTips,
content: (
<ChangePermissionsSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("changePermissions.submit", "Change Permissions"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("changePermissions.results.title", "Modified PDFs"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
ChangePermissions.tool = () => useChangePermissionsOperation;
export default ChangePermissions as ToolComponent;

View File

@@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import CompressSettings from "@app/components/tools/compress/CompressSettings";
import { useCompressParameters } from "@app/hooks/tools/compress/useCompressParameters";
import { useCompressOperation } from "@app/hooks/tools/compress/useCompressOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useCompressTips } from "@app/components/tooltips/useCompressTips";
const Compress = (props: BaseToolProps) => {
const { t } = useTranslation();
const compressTips = useCompressTips();
const base = useBaseTool(
'compress',
useCompressParameters,
useCompressOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: compressTips,
content: (
<CompressSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("compress.submit", "Compress"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("compress.title", "Compression Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Compress as ToolComponent;

View File

@@ -0,0 +1,142 @@
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useFileState, useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import ConvertSettings from "@app/components/tools/convert/ConvertSettings";
import { useConvertParameters } from "@app/hooks/tools/convert/useConvertParameters";
import { useConvertOperation } from "@app/hooks/tools/convert/useConvertOperation";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectors } = useFileState();
const activeFiles = selectors.getFiles();
const { selectedFiles } = useFileSelection();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const convertParams = useConvertParameters();
const convertOperation = useConvertOperation();
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(convertParams.getEndpointName());
const scrollToBottom = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
top: scrollContainerRef.current.scrollHeight,
behavior: "smooth",
});
}
};
const hasFiles = selectedFiles.length > 0;
const hasResults = convertOperation.downloadUrl !== null;
const settingsCollapsed = hasResults;
useEffect(() => {
if (selectedFiles.length > 0) {
convertParams.analyzeFileTypes(selectedFiles);
} else {
// Only reset when there are no active files at all
// If there are active files but no selected files, keep current format (user filtered by format)
if (activeFiles.length === 0) {
convertParams.resetParameters();
}
}
}, [selectedFiles, activeFiles, convertParams.analyzeFileTypes, convertParams.resetParameters]);
useEffect(() => {
// Only clear results if we're not currently processing and parameters changed
if (!convertOperation.isLoading) {
convertOperation.resetResults();
onPreviewFile?.(null);
}
}, [convertParams.parameters.fromExtension, convertParams.parameters.toExtension]);
useEffect(() => {
if (hasFiles) {
setTimeout(scrollToBottom, 100);
}
}, [hasFiles]);
useEffect(() => {
if (hasResults) {
setTimeout(scrollToBottom, 100);
}
}, [hasResults]);
const handleConvert = async () => {
try {
await convertOperation.executeOperation(convertParams.parameters, selectedFiles);
if (convertOperation.files && onComplete) {
onComplete(convertOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : "Convert operation failed");
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "convert");
};
const handleSettingsReset = () => {
convertOperation.resetResults();
onPreviewFile?.(null);
};
const handleUndo = async () => {
await convertOperation.undoOperation();
onPreviewFile?.(null);
};
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: [
{
title: t("convert.settings", "Settings"),
isCollapsed: settingsCollapsed,
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
content: (
<ConvertSettings
parameters={convertParams.parameters}
onParameterChange={convertParams.updateParameter}
getAvailableToExtensions={convertParams.getAvailableToExtensions}
selectedFiles={selectedFiles}
disabled={endpointLoading}
/>
),
},
],
executeButton: {
text: t("convert.convertFiles", "Convert Files"),
loadingText: t("convert.converting", "Converting..."),
onClick: handleConvert,
isVisible: !hasResults,
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
testId: "convert-button",
},
review: {
isVisible: hasResults,
operation: convertOperation,
title: t("convert.conversionResults", "Conversion Results"),
onFileClick: handleThumbnailClick,
onUndo: handleUndo,
testId: "conversion-results",
},
});
};
// Static method to get the operation hook for automation
Convert.tool = () => useConvertOperation;
export default Convert as ToolComponent;

View File

@@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import CropSettings from "@app/components/tools/crop/CropSettings";
import { useCropParameters } from "@app/hooks/tools/crop/useCropParameters";
import { useCropOperation } from "@app/hooks/tools/crop/useCropOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useCropTooltips } from "@app/components/tooltips/useCropTooltips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const Crop = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'crop',
useCropParameters,
useCropOperation,
props
);
const tooltips = useCropTooltips();
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
minFiles: 1,
},
steps: [
{
title: t("crop.steps.selectArea", "Select Crop Area"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
tooltip: tooltips,
content: (
<CropSettings
parameters={base.params}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("crop.submit", "Apply Crop"),
loadingText: t("loading"),
onClick: base.handleExecute,
isVisible: !base.hasResults,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("crop.results.title", "Crop Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Crop as ToolComponent;

View File

@@ -0,0 +1,55 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractImagesSettings";
import { useExtractImagesParameters } from "@app/hooks/tools/extractImages/useExtractImagesParameters";
import { useExtractImagesOperation } from "@app/hooks/tools/extractImages/useExtractImagesOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const ExtractImages = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'extractImages',
useExtractImagesParameters,
useExtractImagesOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("extractImages.settings.title", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<ExtractImagesSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("extractImages.submit", "Extract Images"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("extractImages.title", "Extracted Images"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default ExtractImages as ToolComponent;

View File

@@ -0,0 +1,61 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import FlattenSettings from "@app/components/tools/flatten/FlattenSettings";
import { useFlattenParameters } from "@app/hooks/tools/flatten/useFlattenParameters";
import { useFlattenOperation } from "@app/hooks/tools/flatten/useFlattenOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useFlattenTips } from "@app/components/tooltips/useFlattenTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const Flatten = (props: BaseToolProps) => {
const { t } = useTranslation();
const flattenTips = useFlattenTips();
const base = useBaseTool(
'flatten',
useFlattenParameters,
useFlattenOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("flatten.options.stepTitle", "Flatten Options"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: flattenTips,
content: (
<FlattenSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("flatten.submit", "Flatten PDF"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("flatten.results.title", "Flatten Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
Flatten.tool = () => useFlattenOperation;
export default Flatten as ToolComponent;

View File

@@ -0,0 +1,98 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import MergeSettings from "@app/components/tools/merge/MergeSettings";
import MergeFileSorter from "@app/components/tools/merge/MergeFileSorter";
import { useMergeParameters } from "@app/hooks/tools/merge/useMergeParameters";
import { useMergeOperation } from "@app/hooks/tools/merge/useMergeOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useMergeTips } from "@app/components/tooltips/useMergeTips";
import { useFileManagement, useSelectedFiles, useAllFiles } from "@app/contexts/FileContext";
const Merge = (props: BaseToolProps) => {
const { t } = useTranslation();
const mergeTips = useMergeTips();
// File selection hooks for custom sorting
const { fileIds } = useAllFiles();
const { selectedFileStubs } = useSelectedFiles();
const { reorderFiles } = useFileManagement();
const base = useBaseTool(
'merge',
useMergeParameters,
useMergeOperation,
props,
{ minFiles: 2 }
);
// Custom file sorting logic for merge tool
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
const sortedStubs = [...selectedFileStubs].sort((stubA, stubB) => {
let comparison = 0;
switch (sortType) {
case 'filename':
comparison = stubA.name.localeCompare(stubB.name);
break;
case 'dateModified':
comparison = stubA.lastModified - stubB.lastModified;
break;
}
return ascending ? comparison : -comparison;
});
const selectedIds = sortedStubs.map(record => record.id);
const deselectedIds = fileIds.filter(id => !selectedIds.includes(id));
reorderFiles([...selectedIds, ...deselectedIds]);
}, [selectedFileStubs, fileIds, reorderFiles]);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
minFiles: 2,
},
steps: [
{
title: "Sort Files",
isCollapsed: base.settingsCollapsed,
content: (
<MergeFileSorter
onSortFiles={sortFiles}
disabled={!base.hasFiles || base.endpointLoading}
/>
),
},
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: mergeTips,
content: (
<MergeSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("merge.submit", "Merge PDFs"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("merge.title", "Merge Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Merge as ToolComponent;

View File

@@ -0,0 +1,147 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useFileSelection } from "@app/contexts/FileContext";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import OCRSettings from "@app/components/tools/ocr/OCRSettings";
import AdvancedOCRSettings from "@app/components/tools/ocr/AdvancedOCRSettings";
import { useOCRParameters } from "@app/hooks/tools/ocr/useOCRParameters";
import { useOCROperation } from "@app/hooks/tools/ocr/useOCROperation";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useOCRTips } from "@app/components/tooltips/useOCRTips";
import { useAdvancedOCRTips } from "@app/components/tooltips/useAdvancedOCRTips";
const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const ocrParams = useOCRParameters();
const ocrOperation = useOCROperation();
const ocrTips = useOCRTips();
const advancedOCRTips = useAdvancedOCRTips();
// Step expansion state management
const [expandedStep, setExpandedStep] = useState<"files" | "settings" | "advanced" | null>("files");
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("ocr-pdf");
const hasFiles = selectedFiles.length > 0;
const hasResults = ocrOperation.files.length > 0 || ocrOperation.downloadUrl !== null;
const hasValidSettings = ocrParams.validateParameters();
useEffect(() => {
ocrOperation.resetResults();
onPreviewFile?.(null);
}, [ocrParams.parameters]);
useEffect(() => {
if (selectedFiles.length > 0 && expandedStep === "files") {
setExpandedStep("settings");
}
}, [selectedFiles.length, expandedStep]);
// Collapse all steps when results appear
useEffect(() => {
if (hasResults) {
setExpandedStep(null);
}
}, [hasResults]);
const handleOCR = async () => {
try {
await ocrOperation.executeOperation(ocrParams.parameters, selectedFiles);
if (ocrOperation.files && onComplete) {
onComplete(ocrOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : "OCR operation failed");
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "ocr");
};
const handleSettingsReset = () => {
ocrOperation.resetResults();
onPreviewFile?.(null);
};
const handleUndo = async () => {
await ocrOperation.undoOperation();
onPreviewFile?.(null);
};
const settingsCollapsed = expandedStep !== "settings";
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps: [
{
title: t("ocr.settings.title", "Settings"),
isCollapsed: !hasFiles || settingsCollapsed,
onCollapsedClick: hasResults
? handleSettingsReset
: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === "settings" ? null : "settings");
},
tooltip: ocrTips,
content: (
<OCRSettings
parameters={ocrParams.parameters}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
),
},
{
title: "Advanced",
isCollapsed: expandedStep !== "advanced",
onCollapsedClick: hasResults
? handleSettingsReset
: () => {
if (!hasFiles) return; // Only allow if files are selected
setExpandedStep(expandedStep === "advanced" ? null : "advanced");
},
tooltip: advancedOCRTips,
content: (
<AdvancedOCRSettings
advancedOptions={ocrParams.parameters.additionalOptions}
ocrRenderType={ocrParams.parameters.ocrRenderType}
onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading}
/>
),
},
],
executeButton: {
text: t("ocr.operation.submit", "Process OCR and Review"),
loadingText: t("loading"),
onClick: handleOCR,
isVisible: hasValidSettings && !hasResults,
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: ocrOperation,
title: t("ocr.results.title", "OCR Results"),
onFileClick: handleThumbnailClick,
onUndo: handleUndo,
},
});
};
// Static method to get the operation hook for automation
OCR.tool = () => useOCROperation;
export default OCR as ToolComponent;

View File

@@ -0,0 +1,60 @@
import { useTranslation } from 'react-i18next';
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
import { BaseToolProps, ToolComponent } from '@app/types/tool';
import OverlayPdfsSettings from '@app/components/tools/overlayPdfs/OverlayPdfsSettings';
import { useOverlayPdfsParameters } from '@app/hooks/tools/overlayPdfs/useOverlayPdfsParameters';
import { useOverlayPdfsOperation } from '@app/hooks/tools/overlayPdfs/useOverlayPdfsOperation';
import { useOverlayPdfsTips } from '@app/components/tooltips/useOverlayPdfsTips';
const OverlayPdfs = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'overlay-pdfs',
useOverlayPdfsParameters,
useOverlayPdfsOperation,
props
);
const overlayTips = useOverlayPdfsTips();
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t('overlay-pdfs.settings.title', 'Settings'),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: overlayTips,
content: (
<OverlayPdfsSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t('overlay-pdfs.submit', 'Overlay and Review'),
isVisible: !base.hasResults,
loadingText: t('loading'),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t('overlay-pdfs.results.title', 'Overlay Results'),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default OverlayPdfs as ToolComponent;

View File

@@ -0,0 +1,57 @@
import { useTranslation } from 'react-i18next';
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
import { BaseToolProps, ToolComponent } from '@app/types/tool';
import { usePageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import { usePageLayoutOperation } from '@app/hooks/tools/pageLayout/usePageLayoutOperation';
import PageLayoutSettings from '@app/components/tools/pageLayout/PageLayoutSettings';
const PageLayout = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'pageLayout',
usePageLayoutParameters,
usePageLayoutOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: 'Settings',
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<PageLayoutSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t('pageLayout.submit', 'Create Layout'),
isVisible: !base.hasResults,
loadingText: t('loading'),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t('pageLayout.title', 'Multi Page Layout Results'),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default PageLayout as ToolComponent;

View File

@@ -0,0 +1,120 @@
import { useTranslation } from "react-i18next";
import { useState } 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 { 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 RedactAdvancedSettings from "@app/components/tools/redact/RedactAdvancedSettings";
import WordsToRedactInput from "@app/components/tools/redact/WordsToRedactInput";
const Redact = (props: BaseToolProps) => {
const { t } = useTranslation();
// State for managing step collapse status
const [methodCollapsed, setMethodCollapsed] = useState(false);
const [wordsCollapsed, setWordsCollapsed] = useState(false);
const [advancedCollapsed, setAdvancedCollapsed] = useState(true);
const base = useBaseTool(
'redact',
useRedactParameters,
useRedactOperation,
props
);
// Tooltips for each step
const modeTips = useRedactModeTips();
const wordsTips = useRedactWordsTips();
const advancedTips = useRedactAdvancedTips();
const isExecuteDisabled = () => {
if (base.params.parameters.mode === 'manual') {
return true; // Manual mode not implemented yet
}
return !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled;
};
// Compute actual collapsed state based on results and user state
const getActualCollapsedState = (userCollapsed: boolean) => {
return (!base.hasFiles || base.hasResults) ? true : userCollapsed; // Force collapse when results are shown
};
// Build conditional steps based on redaction mode
const buildSteps = () => {
const steps = [
// Method selection step (always present)
{
title: t("redact.modeSelector.title", "Redaction Method"),
isCollapsed: getActualCollapsedState(methodCollapsed),
onCollapsedClick: () => base.settingsCollapsed ? base.handleSettingsReset() : setMethodCollapsed(!methodCollapsed),
tooltip: modeTips,
content: (
<RedactModeSelector
mode={base.params.parameters.mode}
onModeChange={(mode) => base.params.updateParameter('mode', mode)}
disabled={base.endpointLoading}
/>
),
}
];
// Add mode-specific steps
if (base.params.parameters.mode === 'automatic') {
steps.push(
{
title: t("redact.auto.settings.title", "Redaction Settings"),
isCollapsed: getActualCollapsedState(wordsCollapsed),
onCollapsedClick: () => base.settingsCollapsed ? base.handleSettingsReset() : setWordsCollapsed(!wordsCollapsed),
tooltip: wordsTips,
content: <WordsToRedactInput
wordsToRedact={base.params.parameters.wordsToRedact}
onWordsChange={(words) => base.params.updateParameter('wordsToRedact', words)}
disabled={base.endpointLoading}
/>,
},
{
title: t("redact.auto.settings.advancedTitle", "Advanced Settings"),
isCollapsed: getActualCollapsedState(advancedCollapsed),
onCollapsedClick: () => base.settingsCollapsed ? base.handleSettingsReset() : setAdvancedCollapsed(!advancedCollapsed),
tooltip: advancedTips,
content: <RedactAdvancedSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>,
},
);
} else if (base.params.parameters.mode === 'manual') {
// Manual mode steps would go here when implemented
}
return steps;
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: buildSteps(),
executeButton: {
text: t("redact.submit", "Redact"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: isExecuteDisabled(),
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("redact.title", "Redaction Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Redact as ToolComponent;

View File

@@ -0,0 +1,49 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import RemoveAnnotationsSettings from "@app/components/tools/removeAnnotations/RemoveAnnotationsSettings";
import { useRemoveAnnotationsParameters } from "@app/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters";
import { useRemoveAnnotationsOperation } from "@app/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const RemoveAnnotations = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'removeAnnotations',
useRemoveAnnotationsParameters,
useRemoveAnnotationsOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("removeAnnotations.settings.title", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: <RemoveAnnotationsSettings />,
},
],
executeButton: {
text: t("removeAnnotations.submit", "Remove Annotations"),
isVisible: !base.hasResults,
loadingText: t("loading", "Processing..."),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removeAnnotations.title", "Annotations Removed"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default RemoveAnnotations as ToolComponent;

View File

@@ -0,0 +1,70 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useRemoveBlanksParameters } from "@app/hooks/tools/removeBlanks/useRemoveBlanksParameters";
import { useRemoveBlanksOperation } from "@app/hooks/tools/removeBlanks/useRemoveBlanksOperation";
import RemoveBlanksSettings from "@app/components/tools/removeBlanks/RemoveBlanksSettings";
import { useRemoveBlanksTips } from "@app/components/tooltips/useRemoveBlanksTips";
const RemoveBlanks = (props: BaseToolProps) => {
const { t } = useTranslation();
const tooltipContent = useRemoveBlanksTips();
const base = useBaseTool(
'remove-blanks',
useRemoveBlanksParameters,
useRemoveBlanksOperation,
props
);
const settingsContent = (
<RemoveBlanksSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
);
const handleSettingsClick = () => {
if (base.hasResults) {
base.handleSettingsReset();
}
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("removeBlanks.settings.title", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: handleSettingsClick,
content: settingsContent,
tooltip: tooltipContent,
},
],
executeButton: {
text: t("removeBlanks.submit", "Remove blank pages"),
loadingText: t("loading"),
onClick: base.handleExecute,
isVisible: !base.hasResults,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removeBlanks.results.title", "Removed Blank Pages"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
RemoveBlanks.tool = () => useRemoveBlanksOperation;
export default RemoveBlanks as ToolComponent;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useRemoveCertificateSignParameters } from "@app/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters";
import { useRemoveCertificateSignOperation } from "@app/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const RemoveCertificateSign = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'removeCertificateSign',
useRemoveCertificateSignParameters,
useRemoveCertificateSignOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [],
executeButton: {
text: t("removeCertSign.submit", "Remove Signature"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removeCertSign.results.title", "Certificate Removal Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
RemoveCertificateSign.tool = () => useRemoveCertificateSignOperation;
export default RemoveCertificateSign as ToolComponent;

View File

@@ -0,0 +1,45 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useRemoveImageParameters } from "@app/hooks/tools/removeImage/useRemoveImageParameters";
import { useRemoveImageOperation } from "@app/hooks/tools/removeImage/useRemoveImageOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const RemoveImage = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'removeImage',
useRemoveImageParameters,
useRemoveImageOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [],
executeButton: {
text: t("removeImage.submit", "Remove Images"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removeImage.results.title", "Remove Images Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
RemoveImage.tool = () => useRemoveImageOperation;
export default RemoveImage as ToolComponent;

View File

@@ -0,0 +1,63 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useRemovePagesParameters } from "@app/hooks/tools/removePages/useRemovePagesParameters";
import { useRemovePagesOperation } from "@app/hooks/tools/removePages/useRemovePagesOperation";
import RemovePagesSettings from "@app/components/tools/removePages/RemovePagesSettings";
import { useRemovePagesTips } from "@app/components/tooltips/useRemovePagesTips";
const RemovePages = (props: BaseToolProps) => {
const { t } = useTranslation();
const tooltipContent = useRemovePagesTips();
const base = useBaseTool(
'remove-pages',
useRemovePagesParameters,
useRemovePagesOperation,
props
);
const settingsContent = (
<RemovePagesSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("removePages.settings.title", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: settingsContent,
tooltip: tooltipContent,
},
],
executeButton: {
text: t("removePages.submit", "Remove Pages"),
loadingText: t("loading"),
onClick: base.handleExecute,
isVisible: !base.hasResults,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removePages.results.title", "Pages Removed"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
RemovePages.tool = () => useRemovePagesOperation;
export default RemovePages as ToolComponent;

View File

@@ -0,0 +1,61 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import RemovePasswordSettings from "@app/components/tools/removePassword/RemovePasswordSettings";
import { useRemovePasswordParameters } from "@app/hooks/tools/removePassword/useRemovePasswordParameters";
import { useRemovePasswordOperation } from "@app/hooks/tools/removePassword/useRemovePasswordOperation";
import { useRemovePasswordTips } from "@app/components/tooltips/useRemovePasswordTips";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const RemovePassword = (props: BaseToolProps) => {
const { t } = useTranslation();
const removePasswordTips = useRemovePasswordTips();
const base = useBaseTool(
'removePassword',
useRemovePasswordParameters,
useRemovePasswordOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("removePassword.password.stepTitle", "Remove Password"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
tooltip: removePasswordTips,
content: (
<RemovePasswordSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("removePassword.submit", "Remove Password"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("removePassword.results.title", "Decrypted PDFs"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
RemovePassword.tool = () => useRemovePasswordOperation;
export default RemovePassword as ToolComponent;

View File

@@ -0,0 +1,104 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useEndpointEnabled } from "@app/hooks/useEndpointConfig";
import { useFileSelection } from "@app/contexts/FileContext";
import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps";
import ReorganizePagesSettings from "@app/components/tools/reorganizePages/ReorganizePagesSettings";
import { useReorganizePagesParameters } from "@app/hooks/tools/reorganizePages/useReorganizePagesParameters";
import { useReorganizePagesOperation } from "@app/hooks/tools/reorganizePages/useReorganizePagesOperation";
const ReorganizePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const params = useReorganizePagesParameters();
const operation = useReorganizePagesOperation();
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("rearrange-pages");
useEffect(() => {
operation.resetResults();
onPreviewFile?.(null);
}, [params.parameters]);
const handleExecute = async () => {
try {
await operation.executeOperation(params.parameters, selectedFiles);
if (operation.files && onComplete) {
onComplete(operation.files);
}
} catch (error: any) {
onError?.(error?.message || t("reorganizePages.error.failed", "Failed to reorganize pages"));
}
};
const hasFiles = selectedFiles.length > 0;
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
enum Step {
NONE = 'none',
SETTINGS = 'settings'
}
const accordion = useAccordionSteps<Step>({
noneValue: Step.NONE,
initialStep: Step.SETTINGS,
stateConditions: {
hasFiles,
hasResults
},
afterResults: () => {
operation.resetResults();
onPreviewFile?.(null);
}
});
const steps = [
{
title: t("reorganizePages.settings.title", "Settings"),
isCollapsed: accordion.getCollapsedState(Step.SETTINGS),
onCollapsedClick: () => accordion.handleStepToggle(Step.SETTINGS),
isVisible: true,
content: (
<ReorganizePagesSettings
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
/>
),
}
];
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasResults,
},
steps,
executeButton: {
text: t('reorganizePages.submit', 'Reorganize Pages'),
isVisible: !hasResults,
loadingText: t('loading'),
onClick: handleExecute,
disabled: !params.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: operation,
title: t('reorganizePages.results.title', 'Pages Reorganized'),
onFileClick: (file) => onPreviewFile?.(file),
onUndo: async () => {
await operation.undoOperation();
onPreviewFile?.(null);
},
},
});
};
(ReorganizePages as any).tool = () => useReorganizePagesOperation;
export default ReorganizePages as ToolComponent;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useRepairParameters } from "@app/hooks/tools/repair/useRepairParameters";
import { useRepairOperation } from "@app/hooks/tools/repair/useRepairOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const Repair = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'repair',
useRepairParameters,
useRepairOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [],
executeButton: {
text: t("repair.submit", "Repair PDF"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("repair.results.title", "Repair Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
Repair.tool = () => useRepairOperation;
export default Repair as ToolComponent;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings";
import { useReplaceColorParameters } from "@app/hooks/tools/replaceColor/useReplaceColorParameters";
import { useReplaceColorOperation } from "@app/hooks/tools/replaceColor/useReplaceColorOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useReplaceColorTips } from "@app/components/tooltips/useReplaceColorTips";
const ReplaceColor = (props: BaseToolProps) => {
const { t } = useTranslation();
const replaceColorTips = useReplaceColorTips();
const base = useBaseTool(
'replaceColor',
useReplaceColorParameters,
useReplaceColorOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("replaceColor.labels.settings", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: replaceColorTips,
content: (
<ReplaceColorSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("replace-color.submit", "Replace"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("replace-color.title", "Replace-Invert-Color"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default ReplaceColor as ToolComponent;

View File

@@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import RotateSettings from "@app/components/tools/rotate/RotateSettings";
import { useRotateParameters } from "@app/hooks/tools/rotate/useRotateParameters";
import { useRotateOperation } from "@app/hooks/tools/rotate/useRotateOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useRotateTips } from "@app/components/tooltips/useRotateTips";
const Rotate = (props: BaseToolProps) => {
const { t } = useTranslation();
const rotateTips = useRotateTips();
const base = useBaseTool(
'rotate',
useRotateParameters,
useRotateOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: rotateTips,
content: (
<RotateSettings
parameters={base.params}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("rotate.submit", "Apply Rotation"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("rotate.title", "Rotation Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Rotate as ToolComponent;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import SanitizeSettings from "@app/components/tools/sanitize/SanitizeSettings";
import { useSanitizeParameters } from "@app/hooks/tools/sanitize/useSanitizeParameters";
import { useSanitizeOperation } from "@app/hooks/tools/sanitize/useSanitizeOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const Sanitize = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'sanitize',
useSanitizeParameters,
useSanitizeOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("sanitize.steps.settings", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<SanitizeSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("sanitize.submit", "Sanitize PDF"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("sanitize.sanitizationResults", "Sanitization Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
Sanitize.tool = () => useSanitizeOperation;
export default Sanitize as ToolComponent;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import ScannerImageSplitSettings from "@app/components/tools/scannerImageSplit/ScannerImageSplitSettings";
import { useScannerImageSplitParameters } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitParameters";
import { useScannerImageSplitOperation } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { useScannerImageSplitTips } from "@app/components/tooltips/useScannerImageSplitTips";
const ScannerImageSplit = (props: BaseToolProps) => {
const { t } = useTranslation();
const scannerImageSplitTips = useScannerImageSplitTips();
const base = useBaseTool(
'scannerImageSplit',
useScannerImageSplitParameters,
useScannerImageSplitOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: scannerImageSplitTips,
content: (
<ScannerImageSplitSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("scannerImageSplit.submit", "Extract Image Scans"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("scannerImageSplit.title", "Extracted Images"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default ScannerImageSplit as ToolComponent;

View File

@@ -0,0 +1,182 @@
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";
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();
// 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, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, 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: '',
});
export default Sign as ToolComponent;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useSingleLargePageParameters } from "@app/hooks/tools/singleLargePage/useSingleLargePageParameters";
import { useSingleLargePageOperation } from "@app/hooks/tools/singleLargePage/useSingleLargePageOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const SingleLargePage = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'singleLargePage',
useSingleLargePageParameters,
useSingleLargePageOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [],
executeButton: {
text: t("pdfToSinglePage.submit", "Convert To Single Page"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("pdfToSinglePage.results.title", "Single Page Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
SingleLargePage.tool = () => useSingleLargePageOperation;
export default SingleLargePage as ToolComponent;

View File

@@ -0,0 +1,96 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import CardSelector from "@app/components/shared/CardSelector";
import SplitSettings from "@app/components/tools/split/SplitSettings";
import { useSplitParameters } from "@app/hooks/tools/split/useSplitParameters";
import { useSplitOperation } from "@app/hooks/tools/split/useSplitOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useSplitMethodTips } from "@app/components/tooltips/useSplitMethodTips";
import { useSplitSettingsTips } from "@app/components/tooltips/useSplitSettingsTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { type SplitMethod, METHOD_OPTIONS, type MethodOption } from "@app/constants/splitConstants";
const Split = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'split',
useSplitParameters,
useSplitOperation,
props
);
const methodTips = useSplitMethodTips();
const settingsTips = useSplitSettingsTips(base.params.parameters.method);
// Get tooltip content for a specific method
const getMethodTooltip = (option: MethodOption) => {
const tooltipContent = useSplitSettingsTips(option.value);
return tooltipContent?.tips || [];
};
// Get the method name for the settings step title
const getSettingsTitle = () => {
if (!base.params.parameters.method) return t("split.steps.settings", "Settings");
const methodOption = METHOD_OPTIONS.find(option => option.value === base.params.parameters.method);
if (!methodOption) return t("split.steps.settings", "Settings");
const prefix = t(methodOption.prefixKey, "Split by");
const name = t(methodOption.nameKey, "Method Name");
return `${prefix} ${name}`;
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("split.steps.chooseMethod", "Choose Method"),
isCollapsed: !!base.params.parameters.method, // Collapse when method is selected
onCollapsedClick: () => base.params.updateParameter('method', '')
,
tooltip: methodTips,
content: (
<CardSelector<SplitMethod, MethodOption>
options={METHOD_OPTIONS}
onSelect={(method) => base.params.updateParameter('method', method)}
disabled={base.endpointLoading}
getTooltipContent={getMethodTooltip}
/>
),
},
{
title: getSettingsTitle(),
isCollapsed: !base.params.parameters.method, // Collapsed until method selected
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
tooltip: settingsTips || undefined,
content: (
<SplitSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("split.submit", "Split PDF"),
loadingText: t("loading"),
onClick: base.handleExecute,
isVisible: !base.hasResults,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: "Split Results",
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Split as ToolComponent;

View File

@@ -0,0 +1,24 @@
import React, { useEffect } from "react";
import { BaseToolProps } from "@app/types/tool";
import { withBasePath } from "@app/constants/app";
const SwaggerUI: React.FC<BaseToolProps> = () => {
useEffect(() => {
// Redirect to Swagger UI
window.open(withBasePath("/swagger-ui/5.21.0/index.html"), "_blank");
}, []);
return (
<div style={{ textAlign: "center", padding: "2rem" }}>
<p>Opening Swagger UI in a new tab...</p>
<p>
If it didn't open automatically,{" "}
<a href={withBasePath("/swagger-ui/5.21.0/index.html")} target="_blank" rel="noopener noreferrer">
click here
</a>
</p>
</div>
);
};
export default SwaggerUI;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import { useUnlockPdfFormsParameters } from "@app/hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters";
import { useUnlockPdfFormsOperation } from "@app/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const UnlockPdfForms = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'unlockPdfForms',
useUnlockPdfFormsParameters,
useUnlockPdfFormsOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasFiles || base.hasResults,
},
steps: [],
executeButton: {
text: t("unlockPDFForms.submit", "Unlock Forms"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("unlockPDFForms.results.title", "Unlocked Forms Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
UnlockPdfForms.tool = () => useUnlockPdfFormsOperation;
export default UnlockPdfForms as ToolComponent;

View File

@@ -0,0 +1,163 @@
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
import { BaseToolProps, ToolComponent } from '@app/types/tool';
import { useValidateSignatureParameters, defaultParameters } from '@app/hooks/tools/validateSignature/useValidateSignatureParameters';
import ValidateSignatureSettings from '@app/components/tools/validateSignature/ValidateSignatureSettings';
import ValidateSignatureResults from '@app/components/tools/validateSignature/ValidateSignatureResults';
import { useValidateSignatureOperation, ValidateSignatureOperationHook } from '@app/hooks/tools/validateSignature/useValidateSignatureOperation';
import ValidateSignatureReportView from '@app/components/tools/validateSignature/ValidateSignatureReportView';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
import type { SignatureValidationReportData } from '@app/types/validateSignature';
const ValidateSignature = (props: BaseToolProps) => {
const { t } = useTranslation();
const { actions: navigationActions } = useNavigationActions();
const navigationState = useNavigationState();
const {
registerCustomWorkbenchView,
unregisterCustomWorkbenchView,
setCustomWorkbenchViewData,
clearCustomWorkbenchViewData,
} = useToolWorkflow();
const REPORT_VIEW_ID = 'validateSignatureReport';
const REPORT_WORKBENCH_ID = 'custom:validateSignatureReport' as const;
const reportIcon = useMemo(() => <PictureAsPdfIcon fontSize="small" />, []);
const base = useBaseTool(
'validateSignature',
useValidateSignatureParameters,
useValidateSignatureOperation,
props
);
const operation = base.operation as ValidateSignatureOperationHook;
const hasResults = operation.results.length > 0;
const showResultsStep = hasResults || base.operation.isLoading || !!base.operation.errorMessage;
useEffect(() => {
registerCustomWorkbenchView({
id: REPORT_VIEW_ID,
workbenchId: REPORT_WORKBENCH_ID,
label: t('validateSignature.report.shortTitle', 'Signature Report'),
icon: reportIcon,
component: ValidateSignatureReportView,
});
return () => {
clearCustomWorkbenchViewData(REPORT_VIEW_ID);
unregisterCustomWorkbenchView(REPORT_VIEW_ID);
};
}, [
clearCustomWorkbenchViewData,
registerCustomWorkbenchView,
reportIcon,
t,
unregisterCustomWorkbenchView,
]);
const reportData = useMemo<SignatureValidationReportData | null>(() => {
if (operation.results.length === 0) {
return null;
}
const generatedAt = operation.results[0].summaryGeneratedAt ?? Date.now();
return {
generatedAt,
entries: operation.results,
};
}, [operation.results]);
// Track last time we auto-navigated to the report so we don't override
// the user's manual tab change. Only navigate when a new report is generated.
const lastReportGeneratedAtRef = useRef<number | null>(null);
useEffect(() => {
if (reportData) {
setCustomWorkbenchViewData(REPORT_VIEW_ID, reportData);
const generatedAt = reportData.generatedAt ?? null;
const isNewReport = generatedAt && generatedAt !== lastReportGeneratedAtRef.current;
if (isNewReport) {
lastReportGeneratedAtRef.current = generatedAt;
if (navigationState.selectedTool === 'validateSignature' && navigationState.workbench !== REPORT_WORKBENCH_ID) {
navigationActions.setWorkbench(REPORT_WORKBENCH_ID);
}
}
} else {
clearCustomWorkbenchViewData(REPORT_VIEW_ID);
lastReportGeneratedAtRef.current = null;
}
}, [
clearCustomWorkbenchViewData,
navigationActions,
navigationState.selectedTool,
navigationState.workbench,
reportData,
setCustomWorkbenchViewData,
]);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: hasResults,
},
steps: [
{
title: t('validateSignature.settings.title', 'Validation Settings'),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<ValidateSignatureSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.operation.isLoading || base.endpointLoading}
/>
),
},
{
title: t('validateSignature.results', 'Validation Results'),
isVisible: showResultsStep,
isCollapsed: false,
content: (
<ValidateSignatureResults
operation={operation}
results={operation.results}
isLoading={base.operation.isLoading}
errorMessage={base.operation.errorMessage}
reportAvailable={Boolean(reportData)}
/>
),
},
],
executeButton: {
text: t('validateSignature.submit', 'Validate Signatures'),
loadingText: t('loading', 'Loading...'),
onClick: base.handleExecute,
disabled:
!base.params.validateParameters() ||
!base.hasFiles ||
base.operation.isLoading ||
!base.endpointEnabled,
isVisible: true,
},
review: {
isVisible: false,
operation: base.operation,
title: t('validateSignature.results', 'Validation Results'),
onUndo: base.handleUndo,
},
});
};
const ValidateSignatureTool = ValidateSignature as ToolComponent;
ValidateSignatureTool.tool = () => useValidateSignatureOperation;
ValidateSignatureTool.getDefaultParameters = () => ({ ...defaultParameters });
export default ValidateSignatureTool;