mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
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:
107
frontend/src/core/tools/AddAttachments.tsx
Normal file
107
frontend/src/core/tools/AddAttachments.tsx
Normal 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;
|
||||
126
frontend/src/core/tools/AddPageNumbers.tsx
Normal file
126
frontend/src/core/tools/AddPageNumbers.tsx
Normal 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;
|
||||
121
frontend/src/core/tools/AddPassword.tsx
Normal file
121
frontend/src/core/tools/AddPassword.tsx
Normal 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;
|
||||
188
frontend/src/core/tools/AddStamp.tsx
Normal file
188
frontend/src/core/tools/AddStamp.tsx
Normal 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;
|
||||
|
||||
|
||||
218
frontend/src/core/tools/AddWatermark.tsx
Normal file
218
frontend/src/core/tools/AddWatermark.tsx
Normal 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;
|
||||
121
frontend/src/core/tools/AdjustContrast.tsx
Normal file
121
frontend/src/core/tools/AdjustContrast.tsx
Normal 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;
|
||||
|
||||
|
||||
58
frontend/src/core/tools/AdjustPageScale.tsx
Normal file
58
frontend/src/core/tools/AdjustPageScale.tsx
Normal 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;
|
||||
44
frontend/src/core/tools/AutoRename.tsx
Normal file
44
frontend/src/core/tools/AutoRename.tsx
Normal 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;
|
||||
222
frontend/src/core/tools/Automate.tsx
Normal file
222
frontend/src/core/tools/Automate.tsx
Normal 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;
|
||||
59
frontend/src/core/tools/BookletImposition.tsx
Normal file
59
frontend/src/core/tools/BookletImposition.tsx
Normal 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;
|
||||
131
frontend/src/core/tools/CertSign.tsx
Normal file
131
frontend/src/core/tools/CertSign.tsx
Normal 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;
|
||||
156
frontend/src/core/tools/ChangeMetadata.tsx
Normal file
156
frontend/src/core/tools/ChangeMetadata.tsx
Normal 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;
|
||||
61
frontend/src/core/tools/ChangePermissions.tsx
Normal file
61
frontend/src/core/tools/ChangePermissions.tsx
Normal 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;
|
||||
59
frontend/src/core/tools/Compress.tsx
Normal file
59
frontend/src/core/tools/Compress.tsx
Normal 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;
|
||||
142
frontend/src/core/tools/Convert.tsx
Normal file
142
frontend/src/core/tools/Convert.tsx
Normal 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;
|
||||
59
frontend/src/core/tools/Crop.tsx
Normal file
59
frontend/src/core/tools/Crop.tsx
Normal 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;
|
||||
55
frontend/src/core/tools/ExtractImages.tsx
Normal file
55
frontend/src/core/tools/ExtractImages.tsx
Normal 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;
|
||||
61
frontend/src/core/tools/Flatten.tsx
Normal file
61
frontend/src/core/tools/Flatten.tsx
Normal 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;
|
||||
98
frontend/src/core/tools/Merge.tsx
Normal file
98
frontend/src/core/tools/Merge.tsx
Normal 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;
|
||||
147
frontend/src/core/tools/OCR.tsx
Normal file
147
frontend/src/core/tools/OCR.tsx
Normal 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;
|
||||
60
frontend/src/core/tools/OverlayPdfs.tsx
Normal file
60
frontend/src/core/tools/OverlayPdfs.tsx
Normal 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;
|
||||
|
||||
|
||||
57
frontend/src/core/tools/PageLayout.tsx
Normal file
57
frontend/src/core/tools/PageLayout.tsx
Normal 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;
|
||||
|
||||
|
||||
120
frontend/src/core/tools/Redact.tsx
Normal file
120
frontend/src/core/tools/Redact.tsx
Normal 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;
|
||||
49
frontend/src/core/tools/RemoveAnnotations.tsx
Normal file
49
frontend/src/core/tools/RemoveAnnotations.tsx
Normal 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;
|
||||
70
frontend/src/core/tools/RemoveBlanks.tsx
Normal file
70
frontend/src/core/tools/RemoveBlanks.tsx
Normal 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;
|
||||
|
||||
|
||||
44
frontend/src/core/tools/RemoveCertificateSign.tsx
Normal file
44
frontend/src/core/tools/RemoveCertificateSign.tsx
Normal 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;
|
||||
45
frontend/src/core/tools/RemoveImage.tsx
Normal file
45
frontend/src/core/tools/RemoveImage.tsx
Normal 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;
|
||||
|
||||
|
||||
63
frontend/src/core/tools/RemovePages.tsx
Normal file
63
frontend/src/core/tools/RemovePages.tsx
Normal 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;
|
||||
61
frontend/src/core/tools/RemovePassword.tsx
Normal file
61
frontend/src/core/tools/RemovePassword.tsx
Normal 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;
|
||||
104
frontend/src/core/tools/ReorganizePages.tsx
Normal file
104
frontend/src/core/tools/ReorganizePages.tsx
Normal 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;
|
||||
|
||||
|
||||
44
frontend/src/core/tools/Repair.tsx
Normal file
44
frontend/src/core/tools/Repair.tsx
Normal 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;
|
||||
58
frontend/src/core/tools/ReplaceColor.tsx
Normal file
58
frontend/src/core/tools/ReplaceColor.tsx
Normal 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;
|
||||
57
frontend/src/core/tools/Rotate.tsx
Normal file
57
frontend/src/core/tools/Rotate.tsx
Normal 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;
|
||||
58
frontend/src/core/tools/Sanitize.tsx
Normal file
58
frontend/src/core/tools/Sanitize.tsx
Normal 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;
|
||||
58
frontend/src/core/tools/ScannerImageSplit.tsx
Normal file
58
frontend/src/core/tools/ScannerImageSplit.tsx
Normal 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;
|
||||
182
frontend/src/core/tools/Sign.tsx
Normal file
182
frontend/src/core/tools/Sign.tsx
Normal 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;
|
||||
44
frontend/src/core/tools/SingleLargePage.tsx
Normal file
44
frontend/src/core/tools/SingleLargePage.tsx
Normal 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;
|
||||
96
frontend/src/core/tools/Split.tsx
Normal file
96
frontend/src/core/tools/Split.tsx
Normal 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;
|
||||
24
frontend/src/core/tools/SwaggerUI.tsx
Normal file
24
frontend/src/core/tools/SwaggerUI.tsx
Normal 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;
|
||||
44
frontend/src/core/tools/UnlockPdfForms.tsx
Normal file
44
frontend/src/core/tools/UnlockPdfForms.tsx
Normal 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;
|
||||
163
frontend/src/core/tools/ValidateSignature.tsx
Normal file
163
frontend/src/core/tools/ValidateSignature.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user