mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-24 13:19:23 +00:00
Feature/v2/automate (#4248)
* automate feature * Moved all providers to app level to simplify homepage * Circular dependency fixes * You will see that now toolRegistry gets a tool config and a tool settings object. These enable automate to run the tools using as much static code as possible. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
parent
7d9c0b0298
commit
23d86deae7
1431
frontend/package-lock.json
generated
1431
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -74,6 +74,7 @@
|
|||||||
"@vitest/coverage-v8": "^1.0.0",
|
"@vitest/coverage-v8": "^1.0.0",
|
||||||
"jsdom": "^23.0.0",
|
"jsdom": "^23.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
|
"madge": "^8.0.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
@ -85,6 +85,7 @@
|
|||||||
"warning": {
|
"warning": {
|
||||||
"tooltipTitle": "Warning"
|
"tooltipTitle": "Warning"
|
||||||
},
|
},
|
||||||
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
@ -538,10 +539,6 @@
|
|||||||
"title": "Edit Table of Contents",
|
"title": "Edit Table of Contents",
|
||||||
"desc": "Add or edit bookmarks and table of contents in PDF documents"
|
"desc": "Add or edit bookmarks and table of contents in PDF documents"
|
||||||
},
|
},
|
||||||
"automate": {
|
|
||||||
"title": "Automate",
|
|
||||||
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
|
||||||
},
|
|
||||||
"manageCertificates": {
|
"manageCertificates": {
|
||||||
"title": "Manage Certificates",
|
"title": "Manage Certificates",
|
||||||
"desc": "Import, export, or delete digital certificate files used for signing PDFs."
|
"desc": "Import, export, or delete digital certificate files used for signing PDFs."
|
||||||
@ -601,6 +598,10 @@
|
|||||||
"changePermissions": {
|
"changePermissions": {
|
||||||
"title": "Change Permissions",
|
"title": "Change Permissions",
|
||||||
"desc": "Change document restrictions and permissions"
|
"desc": "Change document restrictions and permissions"
|
||||||
|
},
|
||||||
|
"automate": {
|
||||||
|
"title": "Automate",
|
||||||
|
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"viewPdf": {
|
"viewPdf": {
|
||||||
@ -731,7 +732,8 @@
|
|||||||
"officeDocs": "Office Documents (Word, Excel, PowerPoint)",
|
"officeDocs": "Office Documents (Word, Excel, PowerPoint)",
|
||||||
"imagesExt": "Images (JPG, PNG, etc.)",
|
"imagesExt": "Images (JPG, PNG, etc.)",
|
||||||
"markdown": "Markdown",
|
"markdown": "Markdown",
|
||||||
"textRtf": "Text/RTF"
|
"textRtf": "Text/RTF",
|
||||||
|
"grayscale": "Greyscale"
|
||||||
},
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"tags": "conversion,img,jpg,picture,photo"
|
"tags": "conversion,img,jpg,picture,photo"
|
||||||
@ -2021,7 +2023,8 @@
|
|||||||
"downloadSelected": "Download Selected",
|
"downloadSelected": "Download Selected",
|
||||||
"selectedCount": "{{count}} selected",
|
"selectedCount": "{{count}} selected",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"delete": "Delete"
|
"delete": "Delete",
|
||||||
|
"unsupported":"Unsupported"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
||||||
@ -2191,5 +2194,68 @@
|
|||||||
"results": {
|
"results": {
|
||||||
"title": "Decrypted PDFs"
|
"title": "Decrypted PDFs"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"automate": {
|
||||||
|
"title": "Automate",
|
||||||
|
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks.",
|
||||||
|
"invalidStep": "Invalid step",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select files to process with this automation"
|
||||||
|
},
|
||||||
|
"selection": {
|
||||||
|
"title": "Automation Selection",
|
||||||
|
"saved": {
|
||||||
|
"title": "Saved"
|
||||||
|
},
|
||||||
|
"createNew": {
|
||||||
|
"title": "Create New Automation"
|
||||||
|
},
|
||||||
|
"suggested": {
|
||||||
|
"title": "Suggested"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"creation": {
|
||||||
|
"createTitle": "Create Automation",
|
||||||
|
"editTitle": "Edit Automation",
|
||||||
|
"description": "Automations run tools sequentially. To get started, add tools in the order you want them to run.",
|
||||||
|
"name": {
|
||||||
|
"placeholder": "Automation name"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"selectTool": "Select a tool...",
|
||||||
|
"selected": "Selected Tools",
|
||||||
|
"remove": "Remove tool",
|
||||||
|
"configure": "Configure tool",
|
||||||
|
"notConfigured": "! Not Configured",
|
||||||
|
"addTool": "Add Tool",
|
||||||
|
"add": "Add a tool..."
|
||||||
|
},
|
||||||
|
"save": "Save Automation",
|
||||||
|
"unsavedChanges": {
|
||||||
|
"title": "Unsaved Changes",
|
||||||
|
"message": "You have unsaved changes. Are you sure you want to go back? All changes will be lost.",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Go Back"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"title": "Run Automation"
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"unnamed": "Unnamed Automation",
|
||||||
|
"steps": "{{count}} steps",
|
||||||
|
"running": "Running Automation...",
|
||||||
|
"run": "Run Automation",
|
||||||
|
"finish": "Finish"
|
||||||
|
},
|
||||||
|
"reviewTitle": "Automation Results",
|
||||||
|
"config": {
|
||||||
|
"loading": "Loading tool configuration...",
|
||||||
|
"noSettings": "This tool does not have configurable settings.",
|
||||||
|
"title": "Configure {{toolName}}",
|
||||||
|
"description": "Configure the settings for this tool. These settings will be applied when the automation runs.",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save Configuration"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from "react";
|
||||||
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
|
||||||
import { FileContextProvider } from './contexts/FileContext';
|
import { FileContextProvider } from "./contexts/FileContext";
|
||||||
import { NavigationProvider } from './contexts/NavigationContext';
|
import { NavigationProvider } from "./contexts/NavigationContext";
|
||||||
import { FilesModalProvider } from './contexts/FilesModalContext';
|
import { FilesModalProvider } from "./contexts/FilesModalContext";
|
||||||
import HomePage from './pages/HomePage';
|
import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
||||||
|
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||||
|
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
||||||
|
import HomePage from "./pages/HomePage";
|
||||||
|
|
||||||
// Import global styles
|
// Import global styles
|
||||||
import './styles/tailwind.css';
|
import "./styles/tailwind.css";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
// Loading component for i18next suspense
|
// Loading component for i18next suspense
|
||||||
const LoadingFallback = () => (
|
const LoadingFallback = () => (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
justifyContent: 'center',
|
display: "flex",
|
||||||
alignItems: 'center',
|
justifyContent: "center",
|
||||||
height: '100vh',
|
alignItems: "center",
|
||||||
fontSize: '18px',
|
height: "100vh",
|
||||||
color: '#666'
|
fontSize: "18px",
|
||||||
}}>
|
color: "#666",
|
||||||
|
}}
|
||||||
|
>
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -27,13 +32,19 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<RainbowThemeProvider>
|
<RainbowThemeProvider>
|
||||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
<ErrorBoundary>
|
||||||
<NavigationProvider>
|
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||||
<FilesModalProvider>
|
<NavigationProvider>
|
||||||
<HomePage />
|
<FilesModalProvider>
|
||||||
</FilesModalProvider>
|
<ToolWorkflowProvider>
|
||||||
</NavigationProvider>
|
<SidebarProvider>
|
||||||
</FileContextProvider>
|
<HomePage />
|
||||||
|
</SidebarProvider>
|
||||||
|
</ToolWorkflowProvider>
|
||||||
|
</FilesModalProvider>
|
||||||
|
</NavigationProvider>
|
||||||
|
</FileContextProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</RainbowThemeProvider>
|
</RainbowThemeProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
@ -111,7 +111,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
onClose={closeFilesModal}
|
onClose={closeFilesModal}
|
||||||
size={isMobile ? "100%" : "auto"}
|
size={isMobile ? "100%" : "auto"}
|
||||||
centered
|
centered
|
||||||
radius={30}
|
radius="md"
|
||||||
className="overflow-hidden p-0"
|
className="overflow-hidden p-0"
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
styles={{
|
styles={{
|
||||||
@ -144,7 +144,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '30px',
|
borderRadius: 'var(--radius-md)',
|
||||||
backgroundColor: 'var(--bg-file-manager)'
|
backgroundColor: 'var(--bg-file-manager)'
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
|
56
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
56
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text, Button, Stack } from '@mantine/core';
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
fallback?: React.ComponentType<{error?: Error; retry: () => void}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
retry = () => {
|
||||||
|
this.setState({ hasError: false, error: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
const Fallback = this.props.fallback;
|
||||||
|
return <Fallback error={this.state.error} retry={this.retry} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" style={{ minHeight: '200px', padding: '2rem' }}>
|
||||||
|
<Text size="lg" fw={500} c="red">Something went wrong</Text>
|
||||||
|
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||||
|
<Text size="sm" c="dimmed" style={{ textAlign: 'center', fontFamily: 'monospace' }}>
|
||||||
|
{this.state.error.message}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Button onClick={this.retry} variant="light">
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,11 @@
|
|||||||
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
|
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
|
||||||
import { Box, Text, Stack } from "@mantine/core";
|
import { Box, Stack } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getSubcategoryLabel, ToolRegistryEntry } from "../../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
|
||||||
import ToolButton from "./toolPicker/ToolButton";
|
|
||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
import { SubcategoryGroup, useToolSections } from "../../hooks/useToolSections";
|
import { useToolSections } from "../../hooks/useToolSections";
|
||||||
import SubcategoryHeader from "./shared/SubcategoryHeader";
|
|
||||||
import NoToolsFound from "./shared/NoToolsFound";
|
import NoToolsFound from "./shared/NoToolsFound";
|
||||||
import { TFunction } from "i18next";
|
import { renderToolButtons } from "./shared/renderToolButtons";
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
@ -16,32 +14,6 @@ interface ToolPickerProps {
|
|||||||
isSearching?: boolean;
|
isSearching?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to render tool buttons for a subcategory
|
|
||||||
const renderToolButtons = (
|
|
||||||
t: TFunction,
|
|
||||||
subcategory: SubcategoryGroup,
|
|
||||||
selectedToolKey: string | null,
|
|
||||||
onSelect: (id: string) => void,
|
|
||||||
showSubcategoryHeader: boolean = true
|
|
||||||
) => (
|
|
||||||
<Box key={subcategory.subcategoryId} w="100%">
|
|
||||||
{showSubcategoryHeader && (
|
|
||||||
<SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
|
|
||||||
)}
|
|
||||||
<Stack gap="xs">
|
|
||||||
{subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => (
|
|
||||||
<ToolButton
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
tool={tool}
|
|
||||||
isSelected={selectedToolKey === id}
|
|
||||||
onSelect={onSelect}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* AddWatermarkSingleStepSettings - Used for automation only
|
||||||
|
*
|
||||||
|
* This component combines all watermark settings into a single step interface
|
||||||
|
* for use in the automation system. It includes type selection and all relevant
|
||||||
|
* settings in one unified component.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import WatermarkTypeSettings from "./WatermarkTypeSettings";
|
||||||
|
import WatermarkWording from "./WatermarkWording";
|
||||||
|
import WatermarkTextStyle from "./WatermarkTextStyle";
|
||||||
|
import WatermarkImageFile from "./WatermarkImageFile";
|
||||||
|
import WatermarkFormatting from "./WatermarkFormatting";
|
||||||
|
|
||||||
|
interface AddWatermarkSingleStepSettingsProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disabled = false }: AddWatermarkSingleStepSettingsProps) => {
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Watermark Type Selection */}
|
||||||
|
<WatermarkTypeSettings
|
||||||
|
watermarkType={parameters.watermarkType}
|
||||||
|
onWatermarkTypeChange={(type) => onParameterChange("watermarkType", type)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Conditional settings based on watermark type */}
|
||||||
|
{parameters.watermarkType === "text" && (
|
||||||
|
<>
|
||||||
|
<WatermarkWording
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<WatermarkTextStyle
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parameters.watermarkType === "image" && (
|
||||||
|
<WatermarkImageFile
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Formatting settings for both text and image */}
|
||||||
|
{parameters.watermarkType && (
|
||||||
|
<WatermarkFormatting
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddWatermarkSingleStepSettings;
|
@ -6,7 +6,7 @@ import NumberInputWithUnit from "../shared/NumberInputWithUnit";
|
|||||||
|
|
||||||
interface WatermarkFormattingProps {
|
interface WatermarkFormattingProps {
|
||||||
parameters: AddWatermarkParameters;
|
parameters: AddWatermarkParameters;
|
||||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import FileUploadButton from "../../shared/FileUploadButton";
|
|||||||
|
|
||||||
interface WatermarkImageFileProps {
|
interface WatermarkImageFileProps {
|
||||||
parameters: AddWatermarkParameters;
|
parameters: AddWatermarkParameters;
|
||||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }:
|
|||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<FileUploadButton
|
<FileUploadButton
|
||||||
file={parameters.watermarkImage}
|
file={parameters.watermarkImage}
|
||||||
onChange={(file) => onParameterChange('watermarkImage', file)}
|
onChange={(file) => onParameterChange('watermarkImage', file || undefined)}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={t('watermark.settings.image.choose', 'Choose Image')}
|
placeholder={t('watermark.settings.image.choose', 'Choose Image')}
|
||||||
|
@ -5,7 +5,7 @@ import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAdd
|
|||||||
|
|
||||||
interface WatermarkStyleSettingsProps {
|
interface WatermarkStyleSettingsProps {
|
||||||
parameters: AddWatermarkParameters;
|
parameters: AddWatermarkParameters;
|
||||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
|
|||||||
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
|
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={parameters.rotation}
|
value={parameters.rotation}
|
||||||
onChange={(value) => onParameterChange('rotation', value || 0)}
|
onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : (parseInt(value as string, 10) || 0))}
|
||||||
min={-360}
|
min={-360}
|
||||||
max={360}
|
max={360}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -28,7 +28,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
|
|||||||
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
|
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={parameters.opacity}
|
value={parameters.opacity}
|
||||||
onChange={(value) => onParameterChange('opacity', value || 50)}
|
onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))}
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -40,7 +40,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
|
|||||||
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
|
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={parameters.widthSpacer}
|
value={parameters.widthSpacer}
|
||||||
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
|
onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))}
|
||||||
min={0}
|
min={0}
|
||||||
max={200}
|
max={200}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -49,7 +49,7 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
|
|||||||
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
|
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={parameters.heightSpacer}
|
value={parameters.heightSpacer}
|
||||||
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
|
onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : (parseInt(value as string, 10) || 50))}
|
||||||
min={0}
|
min={0}
|
||||||
max={200}
|
max={200}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -6,7 +6,7 @@ import { alphabetOptions } from "../../../constants/addWatermarkConstants";
|
|||||||
|
|
||||||
interface WatermarkTextStyleProps {
|
interface WatermarkTextStyleProps {
|
||||||
parameters: AddWatermarkParameters;
|
parameters: AddWatermarkParameters;
|
||||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { removeEmojis } from "../../../utils/textUtils";
|
|||||||
|
|
||||||
interface WatermarkWordingProps {
|
interface WatermarkWordingProps {
|
||||||
parameters: AddWatermarkParameters;
|
parameters: AddWatermarkParameters;
|
||||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
onParameterChange: <K extends keyof AddWatermarkParameters>(key: K, value: AddWatermarkParameters[K]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
199
frontend/src/components/tools/automate/AutomationCreation.tsx
Normal file
199
frontend/src/components/tools/automate/AutomationCreation.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
Divider,
|
||||||
|
Modal
|
||||||
|
} from '@mantine/core';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
|
import ToolList from './ToolList';
|
||||||
|
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
||||||
|
import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm';
|
||||||
|
|
||||||
|
|
||||||
|
interface AutomationCreationProps {
|
||||||
|
mode: AutomationMode;
|
||||||
|
existingAutomation?: AutomationConfig;
|
||||||
|
onBack: () => void;
|
||||||
|
onComplete: (automation: AutomationConfig) => void;
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
automationName,
|
||||||
|
setAutomationName,
|
||||||
|
selectedTools,
|
||||||
|
addTool,
|
||||||
|
removeTool,
|
||||||
|
updateTool,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
canSaveAutomation,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
} = useAutomationForm({ mode, existingAutomation, toolRegistry });
|
||||||
|
|
||||||
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
|
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
|
||||||
|
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const configureTool = (index: number) => {
|
||||||
|
setConfiguringToolIndex(index);
|
||||||
|
setConfigModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolConfigSave = (parameters: Record<string, any>) => {
|
||||||
|
if (configuraingToolIndex >= 0) {
|
||||||
|
updateTool(configuraingToolIndex, {
|
||||||
|
configured: true,
|
||||||
|
parameters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setConfigModalOpen(false);
|
||||||
|
setConfiguringToolIndex(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolConfigCancel = () => {
|
||||||
|
setConfigModalOpen(false);
|
||||||
|
setConfiguringToolIndex(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolAdd = () => {
|
||||||
|
const newTool: AutomationTool = {
|
||||||
|
id: `tool-${Date.now()}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
};
|
||||||
|
updateTool(selectedTools.length, newTool);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
if (hasUnsavedChanges()) {
|
||||||
|
setUnsavedWarningOpen(true);
|
||||||
|
} else {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmBack = () => {
|
||||||
|
setUnsavedWarningOpen(false);
|
||||||
|
onBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelBack = () => {
|
||||||
|
setUnsavedWarningOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAutomation = async () => {
|
||||||
|
if (!canSaveAutomation()) return;
|
||||||
|
|
||||||
|
const automation = {
|
||||||
|
name: automationName.trim(),
|
||||||
|
description: '',
|
||||||
|
operations: selectedTools.map(tool => ({
|
||||||
|
operation: tool.operation,
|
||||||
|
parameters: tool.parameters || {}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
|
const savedAutomation = await automationStorage.saveAutomation(automation);
|
||||||
|
onComplete(savedAutomation);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving automation:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentConfigTool = configuraingToolIndex >= 0 ? selectedTools[configuraingToolIndex] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" mb="md" p="md" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")}
|
||||||
|
</Text>
|
||||||
|
<Divider mb="md" />
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Automation Name */}
|
||||||
|
<TextInput
|
||||||
|
placeholder={t('automate.creation.name.placeholder', 'Automation name')}
|
||||||
|
value={automationName}
|
||||||
|
onChange={(e) => setAutomationName(e.currentTarget.value)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Selected Tools List */}
|
||||||
|
{selectedTools.length > 0 && (
|
||||||
|
<ToolList
|
||||||
|
tools={selectedTools}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
onToolUpdate={updateTool}
|
||||||
|
onToolRemove={removeTool}
|
||||||
|
onToolConfigure={configureTool}
|
||||||
|
onToolAdd={handleToolAdd}
|
||||||
|
getToolName={getToolName}
|
||||||
|
getToolDefaultParameters={getToolDefaultParameters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button
|
||||||
|
leftSection={<CheckIcon />}
|
||||||
|
onClick={saveAutomation}
|
||||||
|
disabled={!canSaveAutomation()}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t('automate.creation.save', 'Save Automation')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Tool Configuration Modal */}
|
||||||
|
{currentConfigTool && (
|
||||||
|
<ToolConfigurationModal
|
||||||
|
opened={configModalOpen}
|
||||||
|
tool={currentConfigTool}
|
||||||
|
onSave={handleToolConfigSave}
|
||||||
|
onCancel={handleToolConfigCancel}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Unsaved Changes Warning Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={unsavedWarningOpen}
|
||||||
|
onClose={handleCancelBack}
|
||||||
|
title={t('automate.creation.unsavedChanges.title', 'Unsaved Changes')}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text>
|
||||||
|
{t('automate.creation.unsavedChanges.message', 'You have unsaved changes. Are you sure you want to go back? All changes will be lost.')}
|
||||||
|
</Text>
|
||||||
|
<Group gap="md" justify="flex-end">
|
||||||
|
<Button variant="outline" onClick={handleCancelBack}>
|
||||||
|
{t('automate.creation.unsavedChanges.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={handleConfirmBack}>
|
||||||
|
{t('automate.creation.unsavedChanges.confirm', 'Go Back')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
163
frontend/src/components/tools/automate/AutomationEntry.tsx
Normal file
163
frontend/src/components/tools/automate/AutomationEntry.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
interface AutomationEntryProps {
|
||||||
|
/** Optional title for the automation (usually for custom ones) */
|
||||||
|
title?: string;
|
||||||
|
/** MUI Icon component for the badge */
|
||||||
|
badgeIcon?: React.ComponentType<any>;
|
||||||
|
/** Array of tool operation names in the workflow */
|
||||||
|
operations: string[];
|
||||||
|
/** Click handler */
|
||||||
|
onClick: () => void;
|
||||||
|
/** Whether to keep the icon at normal color (for special cases like "Add New") */
|
||||||
|
keepIconColor?: boolean;
|
||||||
|
/** Show menu for saved/suggested automations */
|
||||||
|
showMenu?: boolean;
|
||||||
|
/** Edit handler */
|
||||||
|
onEdit?: () => void;
|
||||||
|
/** Delete handler */
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutomationEntry({
|
||||||
|
title,
|
||||||
|
badgeIcon: BadgeIcon,
|
||||||
|
operations,
|
||||||
|
onClick,
|
||||||
|
keepIconColor = false,
|
||||||
|
showMenu = false,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}: AutomationEntryProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// Keep item in hovered state if menu is open
|
||||||
|
const shouldShowHovered = isHovered || isMenuOpen;
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (title) {
|
||||||
|
// Custom automation with title
|
||||||
|
return (
|
||||||
|
<Group gap="md" align="center" justify="flex-start" style={{ width: '100%' }}>
|
||||||
|
{BadgeIcon && (
|
||||||
|
<BadgeIcon
|
||||||
|
style={{
|
||||||
|
color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text size="xs" style={{ flex: 1, textAlign: 'left', color: 'var(--mantine-color-text)' }}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Suggested automation showing tool chain
|
||||||
|
return (
|
||||||
|
<Group gap="md" align="center" justify="flex-start" style={{ width: '100%' }}>
|
||||||
|
{BadgeIcon && (
|
||||||
|
<BadgeIcon
|
||||||
|
style={{
|
||||||
|
color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Group gap="xs" justify="flex-start" style={{ flex: 1 }}>
|
||||||
|
{operations.map((op, index) => (
|
||||||
|
<React.Fragment key={`${op}-${index}`}>
|
||||||
|
<Text size="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t(`${op}.title`, op)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{index < operations.length - 1 && (
|
||||||
|
<Text size="xs" c="dimmed" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
→
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'transparent',
|
||||||
|
borderRadius: 'var(--mantine-radius-md)',
|
||||||
|
transition: 'background-color 0.15s ease',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<Group gap="md" align="center" justify="space-between" style={{ width: '100%' }}>
|
||||||
|
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start' }}>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showMenu && (
|
||||||
|
<Menu
|
||||||
|
position="bottom-end"
|
||||||
|
withinPortal
|
||||||
|
onOpen={() => setIsMenuOpen(true)}
|
||||||
|
onClose={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
c="dimmed"
|
||||||
|
size="md"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
opacity: shouldShowHovered ? 1 : 0,
|
||||||
|
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
|
||||||
|
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||||
|
pointerEvents: shouldShowHovered ? 'auto' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVertIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{onEdit && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<EditIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('edit', 'Edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('delete', 'Delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
223
frontend/src/components/tools/automate/AutomationRun.tsx
Normal file
223
frontend/src/components/tools/automate/AutomationRun.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core";
|
||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import { useFileSelection } from "../../../contexts/FileContext";
|
||||||
|
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
|
||||||
|
import { AutomationConfig, ExecutionStep } from "../../../types/automation";
|
||||||
|
import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation";
|
||||||
|
import { useResourceCleanup } from "../../../utils/resourceManager";
|
||||||
|
|
||||||
|
interface AutomationRunProps {
|
||||||
|
automation: AutomationConfig;
|
||||||
|
onComplete: () => void;
|
||||||
|
automateOperation?: any; // TODO: Type this properly when available
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { selectedFiles } = useFileSelection();
|
||||||
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
const cleanup = useResourceCleanup();
|
||||||
|
|
||||||
|
// Progress tracking state
|
||||||
|
const [executionSteps, setExecutionSteps] = useState<ExecutionStep[]>([]);
|
||||||
|
const [currentStepIndex, setCurrentStepIndex] = useState(-1);
|
||||||
|
|
||||||
|
// Use the operation hook's loading state
|
||||||
|
const isExecuting = automateOperation?.isLoading || false;
|
||||||
|
const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null;
|
||||||
|
|
||||||
|
// Initialize execution steps from automation
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (automation?.operations) {
|
||||||
|
const steps = automation.operations.map((op: any, index: number) => {
|
||||||
|
const tool = toolRegistry[op.operation];
|
||||||
|
return {
|
||||||
|
id: `${op.operation}-${index}`,
|
||||||
|
operation: op.operation,
|
||||||
|
name: tool?.name || op.operation,
|
||||||
|
status: EXECUTION_STATUS.PENDING
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setExecutionSteps(steps);
|
||||||
|
setCurrentStepIndex(-1);
|
||||||
|
}
|
||||||
|
}, [automation, toolRegistry]);
|
||||||
|
|
||||||
|
// Cleanup when component unmounts
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Reset progress state when component unmounts
|
||||||
|
setExecutionSteps([]);
|
||||||
|
setCurrentStepIndex(-1);
|
||||||
|
// Clean up any blob URLs
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
}, [cleanup]);
|
||||||
|
|
||||||
|
const executeAutomation = async () => {
|
||||||
|
if (!selectedFiles || selectedFiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!automateOperation) {
|
||||||
|
console.error('No automateOperation provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset progress tracking
|
||||||
|
setCurrentStepIndex(0);
|
||||||
|
setExecutionSteps(prev => prev.map(step => ({ ...step, status: EXECUTION_STATUS.PENDING, error: undefined })));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the automateOperation.executeOperation to handle file consumption properly
|
||||||
|
await automateOperation.executeOperation(
|
||||||
|
{
|
||||||
|
automationConfig: automation,
|
||||||
|
onStepStart: (stepIndex: number, operationName: string) => {
|
||||||
|
setCurrentStepIndex(stepIndex);
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onStepComplete: (stepIndex: number, resultFiles: File[]) => {
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onStepError: (stepIndex: number, error: string) => {
|
||||||
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.ERROR, error } : step
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark all as completed and reset current step
|
||||||
|
setCurrentStepIndex(-1);
|
||||||
|
console.log(`✅ Automation completed successfully`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Automation execution failed:", error);
|
||||||
|
setCurrentStepIndex(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgress = () => {
|
||||||
|
if (executionSteps.length === 0) return 0;
|
||||||
|
const completedSteps = executionSteps.filter(step => step.status === EXECUTION_STATUS.COMPLETED).length;
|
||||||
|
return (completedSteps / executionSteps.length) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStepIcon = (step: ExecutionStep) => {
|
||||||
|
switch (step.status) {
|
||||||
|
case EXECUTION_STATUS.COMPLETED:
|
||||||
|
return <CheckIcon style={{ fontSize: 16, color: 'green' }} />;
|
||||||
|
case EXECUTION_STATUS.ERROR:
|
||||||
|
return <span style={{ fontSize: 16, color: 'red' }}>✕</span>;
|
||||||
|
case EXECUTION_STATUS.RUNNING:
|
||||||
|
return <div style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
border: '2px solid #ccc',
|
||||||
|
borderTop: '2px solid #007bff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: `spin ${AUTOMATION_CONSTANTS.SPINNER_ANIMATION_DURATION} linear infinite`
|
||||||
|
}} />;
|
||||||
|
default:
|
||||||
|
return <div style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
border: '2px solid #ccc',
|
||||||
|
borderRadius: '50%'
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Automation Info */}
|
||||||
|
<Card padding="md" withBorder>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{automation?.name || t("automate.sequence.unnamed", "Unnamed Automation")}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("automate.sequence.steps", "{{count}} steps", { count: executionSteps.length })}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{isExecuting && (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" mb="xs">
|
||||||
|
Progress: {currentStepIndex + 1}/{executionSteps.length}
|
||||||
|
</Text>
|
||||||
|
<Progress value={getProgress()} size="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Execution Steps */}
|
||||||
|
<Stack gap="xs">
|
||||||
|
{executionSteps.map((step, index) => (
|
||||||
|
<Group key={step.id} gap="sm" align="center">
|
||||||
|
<Text size="xs" c="dimmed" style={{ minWidth: "1rem", textAlign: "center" }}>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{getStepIcon(step)}
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
color: step.status === EXECUTION_STATUS.RUNNING ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)',
|
||||||
|
fontWeight: step.status === EXECUTION_STATUS.RUNNING ? 500 : 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.name}
|
||||||
|
</Text>
|
||||||
|
{step.error && (
|
||||||
|
<Text size="xs" c="red" mt="xs">
|
||||||
|
{step.error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Group justify="space-between" mt="xl">
|
||||||
|
<Button
|
||||||
|
leftSection={<PlayArrowIcon />}
|
||||||
|
onClick={executeAutomation}
|
||||||
|
disabled={isExecuting || !selectedFiles || selectedFiles.length === 0}
|
||||||
|
loading={isExecuting}
|
||||||
|
>
|
||||||
|
{isExecuting
|
||||||
|
? t("automate.sequence.running", "Running Automation...")
|
||||||
|
: t("automate.sequence.run", "Run Automation")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{hasResults && (
|
||||||
|
<Button variant="light" onClick={onComplete}>
|
||||||
|
{t("automate.sequence.finish", "Finish")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Title, Stack, Divider } from "@mantine/core";
|
||||||
|
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
||||||
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
|
import AutomationEntry from "./AutomationEntry";
|
||||||
|
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
||||||
|
import { AutomationConfig } from "../../../types/automation";
|
||||||
|
|
||||||
|
interface AutomationSelectionProps {
|
||||||
|
savedAutomations: AutomationConfig[];
|
||||||
|
onCreateNew: () => void;
|
||||||
|
onRun: (automation: AutomationConfig) => void;
|
||||||
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutomationSelection({
|
||||||
|
savedAutomations,
|
||||||
|
onCreateNew,
|
||||||
|
onRun,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}: AutomationSelectionProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const suggestedAutomations = useSuggestedAutomations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title order={3} size="h4" fw={600} mb="md" style={{color: 'var(--mantine-color-dimmed)'}}>
|
||||||
|
{t("automate.selection.saved.title", "Saved")}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<AutomationEntry
|
||||||
|
title={t("automate.selection.createNew.title", "Create New Automation")}
|
||||||
|
badgeIcon={AddCircleOutline}
|
||||||
|
operations={[]}
|
||||||
|
onClick={onCreateNew}
|
||||||
|
keepIconColor={true}
|
||||||
|
/>
|
||||||
|
{/* Saved Automations */}
|
||||||
|
{savedAutomations.map((automation) => (
|
||||||
|
<AutomationEntry
|
||||||
|
key={automation.id}
|
||||||
|
title={automation.name}
|
||||||
|
badgeIcon={SettingsIcon}
|
||||||
|
operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)}
|
||||||
|
onClick={() => onRun(automation)}
|
||||||
|
showMenu={true}
|
||||||
|
onEdit={() => onEdit(automation)}
|
||||||
|
onDelete={() => onDelete(automation)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Divider pb='sm' />
|
||||||
|
|
||||||
|
{/* Suggested Automations */}
|
||||||
|
<div>
|
||||||
|
<Title order={3} size="h4" fw={600} mb="md"style={ {color: 'var(--mantine-color-dimmed)'}}>
|
||||||
|
{t("automate.selection.suggested.title", "Suggested")}
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{suggestedAutomations.map((automation) => (
|
||||||
|
<AutomationEntry
|
||||||
|
key={automation.id}
|
||||||
|
badgeIcon={automation.icon}
|
||||||
|
operations={automation.operations.map(op => op.operation)}
|
||||||
|
onClick={() => onRun(automation)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Title,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Alert
|
||||||
|
} from '@mantine/core';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { getAvailableToExtensions } from '../../../utils/convertUtils';
|
||||||
|
interface ToolConfigurationModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
tool: {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
parameters?: any;
|
||||||
|
};
|
||||||
|
onSave: (parameters: any) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
toolRegistry: ToolRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [parameters, setParameters] = useState<any>({});
|
||||||
|
const [isValid, setIsValid] = useState(true);
|
||||||
|
|
||||||
|
// Get tool info from registry
|
||||||
|
const toolInfo = toolRegistry[tool.operation];
|
||||||
|
const SettingsComponent = toolInfo?.settingsComponent;
|
||||||
|
|
||||||
|
// Initialize parameters from tool (which should contain defaults from registry)
|
||||||
|
useEffect(() => {
|
||||||
|
if (tool.parameters) {
|
||||||
|
setParameters(tool.parameters);
|
||||||
|
} else {
|
||||||
|
// Fallback to empty parameters if none provided
|
||||||
|
setParameters({});
|
||||||
|
}
|
||||||
|
}, [tool.parameters, tool.operation]);
|
||||||
|
|
||||||
|
// Render the settings component
|
||||||
|
const renderToolSettings = () => {
|
||||||
|
if (!SettingsComponent) {
|
||||||
|
return (
|
||||||
|
<Alert icon={<WarningIcon />} color="orange">
|
||||||
|
<Text size="sm">
|
||||||
|
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for ConvertSettings which needs additional props
|
||||||
|
if (tool.operation === 'convert') {
|
||||||
|
return (
|
||||||
|
<SettingsComponent
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={(key: string, value: any) => {
|
||||||
|
setParameters((prev: any) => ({ ...prev, [key]: value }));
|
||||||
|
}}
|
||||||
|
getAvailableToExtensions={getAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsComponent
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={(key: string, value: any) => {
|
||||||
|
setParameters((prev: any) => ({ ...prev, [key]: value }));
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (isValid) {
|
||||||
|
onSave(parameters);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onCancel}
|
||||||
|
title={
|
||||||
|
<Group gap="xs">
|
||||||
|
<SettingsIcon />
|
||||||
|
<Title order={3}>
|
||||||
|
{t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })}
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('automate.config.description', 'Configure the settings for this tool. These settings will be applied when the automation runs.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||||
|
{renderToolSettings()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
leftSection={<CloseIcon />}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
{t('automate.config.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<CheckIcon />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!isValid}
|
||||||
|
>
|
||||||
|
{t('automate.config.save', 'Save Configuration')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
149
frontend/src/components/tools/automate/ToolList.tsx
Normal file
149
frontend/src/components/tools/automate/ToolList.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Text, Stack, Group, ActionIcon } from '@mantine/core';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import AddCircleOutline from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import { AutomationTool } from '../../../types/automation';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import ToolSelector from './ToolSelector';
|
||||||
|
import AutomationEntry from './AutomationEntry';
|
||||||
|
|
||||||
|
interface ToolListProps {
|
||||||
|
tools: AutomationTool[];
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
|
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
||||||
|
onToolRemove: (index: number) => void;
|
||||||
|
onToolConfigure: (index: number) => void;
|
||||||
|
onToolAdd: () => void;
|
||||||
|
getToolName: (operation: string) => string;
|
||||||
|
getToolDefaultParameters: (operation: string) => Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolList({
|
||||||
|
tools,
|
||||||
|
toolRegistry,
|
||||||
|
onToolUpdate,
|
||||||
|
onToolRemove,
|
||||||
|
onToolConfigure,
|
||||||
|
onToolAdd,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
}: ToolListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleToolSelect = (index: number, newOperation: string) => {
|
||||||
|
const defaultParams = getToolDefaultParameters(newOperation);
|
||||||
|
|
||||||
|
onToolUpdate(index, {
|
||||||
|
operation: newOperation,
|
||||||
|
name: getToolName(newOperation),
|
||||||
|
configured: false,
|
||||||
|
parameters: defaultParams
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length})
|
||||||
|
</Text>
|
||||||
|
<Stack gap="0">
|
||||||
|
{tools.map((tool, index) => (
|
||||||
|
<React.Fragment key={tool.id}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
position: 'relative',
|
||||||
|
padding: 'var(--mantine-spacing-xs)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Delete X in top right */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => onToolRemove(index)}
|
||||||
|
title={t('automate.creation.tools.remove', 'Remove tool')}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '4px',
|
||||||
|
right: '4px',
|
||||||
|
zIndex: 1,
|
||||||
|
color: 'var(--mantine-color-gray-6)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon style={{ fontSize: 12 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<div style={{ paddingRight: '1.25rem' }}>
|
||||||
|
{/* Tool Selection Dropdown with inline settings cog */}
|
||||||
|
<Group gap="xs" align="center" wrap="nowrap">
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<ToolSelector
|
||||||
|
key={`tool-selector-${tool.id}`}
|
||||||
|
onSelect={(newOperation) => handleToolSelect(index, newOperation)}
|
||||||
|
excludeTools={['automate']}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
selectedValue={tool.operation}
|
||||||
|
placeholder={tool.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings cog - only show if tool is selected, aligned right */}
|
||||||
|
{tool.operation && (
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onToolConfigure(index)}
|
||||||
|
title={t('automate.creation.tools.configure', 'Configure tool')}
|
||||||
|
style={{ color: 'var(--mantine-color-gray-6)' }}
|
||||||
|
>
|
||||||
|
<SettingsIcon style={{ fontSize: 16 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Configuration status underneath */}
|
||||||
|
{tool.operation && !tool.configured && (
|
||||||
|
<Text pl="md" size="xs" c="dimmed" mt="xs">
|
||||||
|
{t('automate.creation.tools.notConfigured', "! Not Configured")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < tools.length - 1 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Arrow before Add Tool Button */}
|
||||||
|
{tools.length > 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Tool Button */}
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<AutomationEntry
|
||||||
|
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
||||||
|
badgeIcon={AddCircleOutline}
|
||||||
|
operations={[]}
|
||||||
|
onClick={onToolAdd}
|
||||||
|
keepIconColor={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
182
frontend/src/components/tools/automate/ToolSelector.tsx
Normal file
182
frontend/src/components/tools/automate/ToolSelector.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Menu, Stack, Text, ScrollArea } from '@mantine/core';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { useToolSections } from '../../../hooks/useToolSections';
|
||||||
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
|
|
||||||
|
interface ToolSelectorProps {
|
||||||
|
onSelect: (toolKey: string) => void;
|
||||||
|
excludeTools?: string[];
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
||||||
|
selectedValue?: string; // For showing current selection when editing existing tool
|
||||||
|
placeholder?: string; // Custom placeholder text
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolSelector({
|
||||||
|
onSelect,
|
||||||
|
excludeTools = [],
|
||||||
|
toolRegistry,
|
||||||
|
selectedValue,
|
||||||
|
placeholder
|
||||||
|
}: ToolSelectorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Filter out excluded tools (like 'automate' itself)
|
||||||
|
const baseFilteredTools = useMemo(() => {
|
||||||
|
return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key));
|
||||||
|
}, [toolRegistry, excludeTools]);
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
return baseFilteredTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowercaseSearch = searchTerm.toLowerCase();
|
||||||
|
return baseFilteredTools.filter(([key, tool]) => {
|
||||||
|
return (
|
||||||
|
tool.name.toLowerCase().includes(lowercaseSearch) ||
|
||||||
|
tool.description?.toLowerCase().includes(lowercaseSearch) ||
|
||||||
|
key.toLowerCase().includes(lowercaseSearch)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [baseFilteredTools, searchTerm]);
|
||||||
|
|
||||||
|
// Create filtered tool registry for ToolSearch
|
||||||
|
const filteredToolRegistry = useMemo(() => {
|
||||||
|
const registry: Record<string, ToolRegistryEntry> = {};
|
||||||
|
baseFilteredTools.forEach(([key, tool]) => {
|
||||||
|
registry[key] = tool;
|
||||||
|
});
|
||||||
|
return registry;
|
||||||
|
}, [baseFilteredTools]);
|
||||||
|
|
||||||
|
// Use the same tool sections logic as the main ToolPicker
|
||||||
|
const { sections, searchGroups } = useToolSections(filteredTools);
|
||||||
|
|
||||||
|
// Determine what to display: search results or organized sections
|
||||||
|
const isSearching = searchTerm.trim().length > 0;
|
||||||
|
const displayGroups = useMemo(() => {
|
||||||
|
if (isSearching) {
|
||||||
|
return searchGroups || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sections || sections.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the "all" section which contains all tools without duplicates
|
||||||
|
const allSection = sections.find(s => (s as any).key === 'all');
|
||||||
|
return allSection?.subcategories || [];
|
||||||
|
}, [isSearching, searchGroups, sections]);
|
||||||
|
|
||||||
|
const handleToolSelect = useCallback((toolKey: string) => {
|
||||||
|
onSelect(toolKey);
|
||||||
|
setOpened(false);
|
||||||
|
setSearchTerm(''); // Clear search to show the selected tool display
|
||||||
|
}, [onSelect]);
|
||||||
|
|
||||||
|
const renderedTools = useMemo(() =>
|
||||||
|
displayGroups.map((subcategory) =>
|
||||||
|
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching)
|
||||||
|
), [displayGroups, handleToolSelect, isSearching, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchFocus = () => {
|
||||||
|
setOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
if (!opened) {
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get display value for selected tool
|
||||||
|
const getDisplayValue = () => {
|
||||||
|
if (selectedValue && toolRegistry[selectedValue]) {
|
||||||
|
return toolRegistry[selectedValue].name;
|
||||||
|
}
|
||||||
|
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%' }}>
|
||||||
|
<Menu
|
||||||
|
opened={opened}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
setOpened(isOpen);
|
||||||
|
// Clear search term when menu closes to show proper display
|
||||||
|
if (!isOpen) {
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
closeOnClickOutside={true}
|
||||||
|
closeOnEscape={true}
|
||||||
|
position="bottom-start"
|
||||||
|
offset={4}
|
||||||
|
withinPortal={false}
|
||||||
|
trapFocus={false}
|
||||||
|
shadow="sm"
|
||||||
|
transitionProps={{ duration: 0 }}
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
||||||
|
// Show selected tool in AutomationEntry style when tool is selected and not searching
|
||||||
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 'var(--mantine-spacing-sm)',
|
||||||
|
padding: '0 0.5rem',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
}}>
|
||||||
|
<div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
|
||||||
|
{toolRegistry[selectedValue].icon}
|
||||||
|
</div>
|
||||||
|
<Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
|
||||||
|
{toolRegistry[selectedValue].name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Show search input when no tool selected or actively searching
|
||||||
|
<ToolSearch
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
toolRegistry={filteredToolRegistry}
|
||||||
|
mode="filter"
|
||||||
|
placeholder={getDisplayValue()}
|
||||||
|
hideIcon={true}
|
||||||
|
onFocus={handleSearchFocus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown p={0} style={{ minWidth: '16rem' }}>
|
||||||
|
<ScrollArea h={350}>
|
||||||
|
<Stack gap="sm" p="sm">
|
||||||
|
{displayGroups.length === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||||
|
{isSearching
|
||||||
|
? t('tools.noSearchResults', 'No tools found')
|
||||||
|
: t('tools.noTools', 'No tools available')
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
renderedTools
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Stack, Text, Divider, Card, Group } from '@mantine/core';
|
import { Stack, Text, Divider, Card, Group } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
|
import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
|
||||||
|
|
||||||
export interface SuggestedToolsSectionProps {}
|
export interface SuggestedToolsSectionProps {}
|
||||||
|
|
||||||
export function SuggestedToolsSection(): React.ReactElement {
|
export function SuggestedToolsSection(): React.ReactElement {
|
||||||
|
@ -9,6 +9,7 @@ export interface FilesStepConfig {
|
|||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onCollapsedClick?: () => void;
|
onCollapsedClick?: () => void;
|
||||||
|
isVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiddleStepConfig {
|
export interface MiddleStepConfig {
|
||||||
@ -63,7 +64,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
||||||
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
{steps.createFilesStep({
|
{config.files.isVisible !== false && steps.createFilesStep({
|
||||||
selectedFiles: config.files.selectedFiles,
|
selectedFiles: config.files.selectedFiles,
|
||||||
isCollapsed: config.files.isCollapsed,
|
isCollapsed: config.files.isCollapsed,
|
||||||
placeholder: config.files.placeholder,
|
placeholder: config.files.placeholder,
|
||||||
|
34
frontend/src/components/tools/shared/renderToolButtons.tsx
Normal file
34
frontend/src/components/tools/shared/renderToolButtons.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Stack } from '@mantine/core';
|
||||||
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
|
import SubcategoryHeader from './SubcategoryHeader';
|
||||||
|
|
||||||
|
import { getSubcategoryLabel } from "../../../data/toolsTaxonomy";
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
|
import { SubcategoryGroup } from '../../../hooks/useToolSections';
|
||||||
|
|
||||||
|
// Helper function to render tool buttons for a subcategory
|
||||||
|
export const renderToolButtons = (
|
||||||
|
t: TFunction,
|
||||||
|
subcategory: SubcategoryGroup,
|
||||||
|
selectedToolKey: string | null,
|
||||||
|
onSelect: (id: string) => void,
|
||||||
|
showSubcategoryHeader: boolean = true
|
||||||
|
) => (
|
||||||
|
<Box key={subcategory.subcategoryId} w="100%">
|
||||||
|
{showSubcategoryHeader && (
|
||||||
|
<SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
|
||||||
|
)}
|
||||||
|
<Stack gap="xs">
|
||||||
|
{subcategory.tools.map(({ id, tool }) => (
|
||||||
|
<ToolButton
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
tool={tool}
|
||||||
|
isSelected={selectedToolKey === id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
@ -12,6 +12,9 @@ interface ToolSearchProps {
|
|||||||
onToolSelect?: (toolId: string) => void;
|
onToolSelect?: (toolId: string) => void;
|
||||||
mode: 'filter' | 'dropdown';
|
mode: 'filter' | 'dropdown';
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
|
placeholder?: string;
|
||||||
|
hideIcon?: boolean;
|
||||||
|
onFocus?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolSearch = ({
|
const ToolSearch = ({
|
||||||
@ -20,11 +23,15 @@ const ToolSearch = ({
|
|||||||
toolRegistry,
|
toolRegistry,
|
||||||
onToolSelect,
|
onToolSelect,
|
||||||
mode = 'filter',
|
mode = 'filter',
|
||||||
selectedToolKey
|
selectedToolKey,
|
||||||
|
placeholder,
|
||||||
|
hideIcon = false,
|
||||||
|
onFocus
|
||||||
}: ToolSearchProps) => {
|
}: ToolSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const filteredTools = useMemo(() => {
|
const filteredTools = useMemo(() => {
|
||||||
if (!value.trim()) return [];
|
if (!value.trim()) return [];
|
||||||
@ -47,7 +54,12 @@ const ToolSearch = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
|
if (
|
||||||
|
searchRef.current &&
|
||||||
|
dropdownRef.current &&
|
||||||
|
!searchRef.current.contains(event.target as Node) &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -61,9 +73,10 @@ const ToolSearch = ({
|
|||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||||
icon={<span className="material-symbols-rounded">search</span>}
|
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -77,19 +90,19 @@ const ToolSearch = ({
|
|||||||
{searchInput}
|
{searchInput}
|
||||||
{dropdownOpen && filteredTools.length > 0 && (
|
{dropdownOpen && filteredTools.length > 0 && (
|
||||||
<div
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '100%',
|
top: '100%',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundColor: 'var(--bg-toolbar)',
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
border: '1px solid var(--border-default)',
|
border: '1px solid var(--mantine-color-gray-3)',
|
||||||
borderRadius: '8px',
|
borderRadius: '6px',
|
||||||
marginTop: '4px',
|
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||||
maxHeight: '300px',
|
maxHeight: '300px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto'
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
<Stack gap="xs" style={{ padding: '8px' }}>
|
||||||
@ -97,7 +110,10 @@ const ToolSearch = ({
|
|||||||
<Button
|
<Button
|
||||||
key={id}
|
key={id}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => onToolSelect && onToolSelect(id)}
|
onClick={() => {
|
||||||
|
onToolSelect && onToolSelect(id);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}}
|
||||||
leftSection={
|
leftSection={
|
||||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
||||||
{tool.icon}
|
{tool.icon}
|
||||||
|
42
frontend/src/constants/automation.ts
Normal file
42
frontend/src/constants/automation.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Constants for automation functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AUTOMATION_CONSTANTS = {
|
||||||
|
// Timeouts
|
||||||
|
OPERATION_TIMEOUT: 300000, // 5 minutes in milliseconds
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
DEFAULT_TOOL_COUNT: 2,
|
||||||
|
MIN_TOOL_COUNT: 2,
|
||||||
|
|
||||||
|
// File prefixes
|
||||||
|
FILE_PREFIX: 'automated_',
|
||||||
|
RESPONSE_ZIP_PREFIX: 'response_',
|
||||||
|
RESULT_FILE_PREFIX: 'result_',
|
||||||
|
PROCESSED_FILE_PREFIX: 'processed_',
|
||||||
|
|
||||||
|
// Operation types
|
||||||
|
CONVERT_OPERATION_TYPE: 'convert',
|
||||||
|
|
||||||
|
// Storage keys
|
||||||
|
DB_NAME: 'StirlingPDF_Automations',
|
||||||
|
DB_VERSION: 1,
|
||||||
|
STORE_NAME: 'automations',
|
||||||
|
|
||||||
|
// UI delays
|
||||||
|
SPINNER_ANIMATION_DURATION: '1s'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const AUTOMATION_STEPS = {
|
||||||
|
SELECTION: 'selection',
|
||||||
|
CREATION: 'creation',
|
||||||
|
RUN: 'run'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXECUTION_STATUS = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
RUNNING: 'running',
|
||||||
|
COMPLETED: 'completed',
|
||||||
|
ERROR: 'error'
|
||||||
|
} as const;
|
@ -1,5 +1,6 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
||||||
import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
||||||
|
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NavigationContext - Complete navigation management system
|
* NavigationContext - Complete navigation management system
|
||||||
@ -9,32 +10,13 @@ import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
|||||||
* maintain clear separation of concerns.
|
* maintain clear separation of concerns.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Navigation mode types - complete list to match fileContext.ts
|
|
||||||
export type ModeType =
|
|
||||||
| 'viewer'
|
|
||||||
| 'pageEditor'
|
|
||||||
| 'fileEditor'
|
|
||||||
| 'merge'
|
|
||||||
| 'split'
|
|
||||||
| 'compress'
|
|
||||||
| 'ocr'
|
|
||||||
| 'convert'
|
|
||||||
| 'sanitize'
|
|
||||||
| 'addPassword'
|
|
||||||
| 'changePermissions'
|
|
||||||
| 'addWatermark'
|
|
||||||
| 'removePassword'
|
|
||||||
| 'single-large-page'
|
|
||||||
| 'repair'
|
|
||||||
| 'unlockPdfForms'
|
|
||||||
| 'removeCertificateSign';
|
|
||||||
|
|
||||||
// Navigation state
|
// Navigation state
|
||||||
interface NavigationState {
|
interface NavigationState {
|
||||||
currentMode: ModeType;
|
currentMode: ModeType;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
pendingNavigation: (() => void) | null;
|
pendingNavigation: (() => void) | null;
|
||||||
showNavigationWarning: boolean;
|
showNavigationWarning: boolean;
|
||||||
|
selectedToolKey: string | null; // Add tool selection to navigation state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation actions
|
// Navigation actions
|
||||||
@ -42,7 +24,8 @@ type NavigationAction =
|
|||||||
| { type: 'SET_MODE'; payload: { mode: ModeType } }
|
| { type: 'SET_MODE'; payload: { mode: ModeType } }
|
||||||
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
||||||
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
||||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } };
|
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }
|
||||||
|
| { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } };
|
||||||
|
|
||||||
// Navigation reducer
|
// Navigation reducer
|
||||||
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
|
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
|
||||||
@ -59,6 +42,9 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
|
|||||||
case 'SHOW_NAVIGATION_WARNING':
|
case 'SHOW_NAVIGATION_WARNING':
|
||||||
return { ...state, showNavigationWarning: action.payload.show };
|
return { ...state, showNavigationWarning: action.payload.show };
|
||||||
|
|
||||||
|
case 'SET_SELECTED_TOOL':
|
||||||
|
return { ...state, selectedToolKey: action.payload.toolKey };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -66,10 +52,11 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
|
|||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
const initialState: NavigationState = {
|
const initialState: NavigationState = {
|
||||||
currentMode: 'pageEditor',
|
currentMode: getDefaultMode(),
|
||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
pendingNavigation: null,
|
pendingNavigation: null,
|
||||||
showNavigationWarning: false
|
showNavigationWarning: false,
|
||||||
|
selectedToolKey: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigation context actions interface
|
// Navigation context actions interface
|
||||||
@ -80,6 +67,9 @@ export interface NavigationContextActions {
|
|||||||
requestNavigation: (navigationFn: () => void) => void;
|
requestNavigation: (navigationFn: () => void) => void;
|
||||||
confirmNavigation: () => void;
|
confirmNavigation: () => void;
|
||||||
cancelNavigation: () => void;
|
cancelNavigation: () => void;
|
||||||
|
selectTool: (toolKey: string) => void;
|
||||||
|
clearToolSelection: () => void;
|
||||||
|
handleToolSelect: (toolId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split context values
|
// Split context values
|
||||||
@ -88,6 +78,7 @@ export interface NavigationContextStateValue {
|
|||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
pendingNavigation: (() => void) | null;
|
pendingNavigation: (() => void) | null;
|
||||||
showNavigationWarning: boolean;
|
showNavigationWarning: boolean;
|
||||||
|
selectedToolKey: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationContextActionsValue {
|
export interface NavigationContextActionsValue {
|
||||||
@ -145,6 +136,31 @@ export const NavigationProvider: React.FC<{
|
|||||||
// Clear navigation without executing
|
// Clear navigation without executing
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
selectTool: useCallback((toolKey: string) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } });
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
clearToolSelection: useCallback(() => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
handleToolSelect: useCallback((toolId: string) => {
|
||||||
|
// Handle special cases
|
||||||
|
if (toolId === 'allTools') {
|
||||||
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special-case: if tool is a dedicated reader tool, enter reader mode
|
||||||
|
if (toolId === 'read' || toolId === 'view-pdf') {
|
||||||
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } });
|
||||||
|
dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } });
|
||||||
}, [])
|
}, [])
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,7 +168,8 @@ export const NavigationProvider: React.FC<{
|
|||||||
currentMode: state.currentMode,
|
currentMode: state.currentMode,
|
||||||
hasUnsavedChanges: state.hasUnsavedChanges,
|
hasUnsavedChanges: state.hasUnsavedChanges,
|
||||||
pendingNavigation: state.pendingNavigation,
|
pendingNavigation: state.pendingNavigation,
|
||||||
showNavigationWarning: state.showNavigationWarning
|
showNavigationWarning: state.showNavigationWarning,
|
||||||
|
selectedToolKey: state.selectedToolKey
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionsValue: NavigationContextActionsValue = {
|
const actionsValue: NavigationContextActionsValue = {
|
||||||
@ -212,16 +229,8 @@ export const useNavigationGuard = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility functions for mode handling
|
// Re-export utility functions from types for backward compatibility
|
||||||
export const isValidMode = (mode: string): mode is ModeType => {
|
export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation';
|
||||||
const validModes: ModeType[] = [
|
|
||||||
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
|
|
||||||
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
|
|
||||||
];
|
|
||||||
return validModes.includes(mode as ModeType);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDefaultMode = (): ModeType => 'pageEditor';
|
|
||||||
|
|
||||||
// TODO: This will be expanded for URL-based routing system
|
// TODO: This will be expanded for URL-based routing system
|
||||||
// - URL parsing utilities
|
// - URL parsing utilities
|
||||||
|
@ -8,6 +8,7 @@ import { useToolManagement } from '../hooks/useToolManagement';
|
|||||||
import { PageEditorFunctions } from '../types/pageEditor';
|
import { PageEditorFunctions } from '../types/pageEditor';
|
||||||
import { ToolRegistryEntry } from '../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../data/toolsTaxonomy';
|
||||||
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
|
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
|
||||||
|
import { useNavigationActions, useNavigationState } from './NavigationContext';
|
||||||
|
|
||||||
// State interface
|
// State interface
|
||||||
interface ToolWorkflowState {
|
interface ToolWorkflowState {
|
||||||
@ -100,24 +101,24 @@ const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(
|
|||||||
// Provider component
|
// Provider component
|
||||||
interface ToolWorkflowProviderProps {
|
interface ToolWorkflowProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** Handler for view changes (passed from parent) */
|
|
||||||
onViewChange?: (view: string) => void;
|
|
||||||
/** Enable URL synchronization for tool selection */
|
|
||||||
enableUrlSync?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = true }: ToolWorkflowProviderProps) {
|
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||||
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
||||||
|
|
||||||
|
// Navigation actions and state are available since we're inside NavigationProvider
|
||||||
|
const { actions } = useNavigationActions();
|
||||||
|
const navigationState = useNavigationState();
|
||||||
|
|
||||||
// Tool management hook
|
// Tool management hook
|
||||||
const {
|
const {
|
||||||
selectedToolKey,
|
|
||||||
selectedTool,
|
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
selectTool,
|
getSelectedTool,
|
||||||
clearToolSelection,
|
|
||||||
} = useToolManagement();
|
} = useToolManagement();
|
||||||
|
|
||||||
|
// Get selected tool from navigation context
|
||||||
|
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
|
||||||
|
|
||||||
// UI Action creators
|
// UI Action creators
|
||||||
const setSidebarsVisible = useCallback((visible: boolean) => {
|
const setSidebarsVisible = useCallback((visible: boolean) => {
|
||||||
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
|
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
|
||||||
@ -145,28 +146,30 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
|
|||||||
|
|
||||||
// Workflow actions (compound actions that coordinate multiple state changes)
|
// Workflow actions (compound actions that coordinate multiple state changes)
|
||||||
const handleToolSelect = useCallback((toolId: string) => {
|
const handleToolSelect = useCallback((toolId: string) => {
|
||||||
// Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent
|
actions.handleToolSelect(toolId);
|
||||||
if (toolId === 'read' || toolId === 'view-pdf') {
|
|
||||||
setReaderMode(true);
|
|
||||||
setLeftPanelView('toolPicker');
|
|
||||||
clearToolSelection();
|
|
||||||
setSearchQuery('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectTool(toolId);
|
// Clear search query when selecting a tool
|
||||||
onViewChange?.('fileEditor');
|
|
||||||
setLeftPanelView('toolContent');
|
|
||||||
setReaderMode(false);
|
|
||||||
// Clear search so the tool content becomes visible immediately
|
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
}, [selectTool, onViewChange, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]);
|
|
||||||
|
// Handle view switching logic
|
||||||
|
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
|
||||||
|
setLeftPanelView('toolPicker');
|
||||||
|
if (toolId === 'read' || toolId === 'view-pdf') {
|
||||||
|
setReaderMode(true);
|
||||||
|
} else {
|
||||||
|
setReaderMode(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLeftPanelView('toolContent');
|
||||||
|
setReaderMode(false); // Disable read mode when selecting tools
|
||||||
|
}
|
||||||
|
}, [actions, setLeftPanelView, setReaderMode, setSearchQuery]);
|
||||||
|
|
||||||
const handleBackToTools = useCallback(() => {
|
const handleBackToTools = useCallback(() => {
|
||||||
setLeftPanelView('toolPicker');
|
setLeftPanelView('toolPicker');
|
||||||
setReaderMode(false);
|
setReaderMode(false);
|
||||||
clearToolSelection();
|
actions.clearToolSelection();
|
||||||
}, [setLeftPanelView, setReaderMode, clearToolSelection]);
|
}, [setLeftPanelView, setReaderMode, actions]);
|
||||||
|
|
||||||
const handleReaderToggle = useCallback(() => {
|
const handleReaderToggle = useCallback(() => {
|
||||||
setReaderMode(true);
|
setReaderMode(true);
|
||||||
@ -186,13 +189,13 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Enable URL synchronization for tool selection
|
// Enable URL synchronization for tool selection
|
||||||
useToolWorkflowUrlSync(selectedToolKey, selectTool, clearToolSelection, enableUrlSync);
|
useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true);
|
||||||
|
|
||||||
// Simple context value with basic memoization
|
// Properly memoized context value
|
||||||
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
||||||
// State
|
// State
|
||||||
...state,
|
...state,
|
||||||
selectedToolKey,
|
selectedToolKey: navigationState.selectedToolKey,
|
||||||
selectedTool,
|
selectedTool,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
|
|
||||||
@ -203,8 +206,8 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
|
|||||||
setPreviewFile,
|
setPreviewFile,
|
||||||
setPageEditorFunctions,
|
setPageEditorFunctions,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
selectTool,
|
selectTool: actions.selectTool,
|
||||||
clearToolSelection,
|
clearToolSelection: actions.clearToolSelection,
|
||||||
|
|
||||||
// Workflow Actions
|
// Workflow Actions
|
||||||
handleToolSelect,
|
handleToolSelect,
|
||||||
@ -214,7 +217,25 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
|
|||||||
// Computed
|
// Computed
|
||||||
filteredTools,
|
filteredTools,
|
||||||
isPanelVisible,
|
isPanelVisible,
|
||||||
}), [state, selectedToolKey, selectedTool, toolRegistry, filteredTools, isPanelVisible]);
|
}), [
|
||||||
|
state,
|
||||||
|
navigationState.selectedToolKey,
|
||||||
|
selectedTool,
|
||||||
|
toolRegistry,
|
||||||
|
setSidebarsVisible,
|
||||||
|
setLeftPanelView,
|
||||||
|
setReaderMode,
|
||||||
|
setPreviewFile,
|
||||||
|
setPageEditorFunctions,
|
||||||
|
setSearchQuery,
|
||||||
|
actions.selectTool,
|
||||||
|
actions.clearToolSelection,
|
||||||
|
handleToolSelect,
|
||||||
|
handleBackToTools,
|
||||||
|
handleReaderToggle,
|
||||||
|
filteredTools,
|
||||||
|
isPanelVisible,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolWorkflowContext.Provider value={contextValue}>
|
<ToolWorkflowContext.Provider value={contextValue}>
|
||||||
@ -227,6 +248,38 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
|
|||||||
export function useToolWorkflow(): ToolWorkflowContextValue {
|
export function useToolWorkflow(): ToolWorkflowContextValue {
|
||||||
const context = useContext(ToolWorkflowContext);
|
const context = useContext(ToolWorkflowContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
// During development hot reload, temporarily return a safe fallback
|
||||||
|
if (false && process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn('ToolWorkflowContext temporarily unavailable during hot reload, using fallback');
|
||||||
|
|
||||||
|
// Return minimal safe fallback to prevent crashes
|
||||||
|
return {
|
||||||
|
sidebarsVisible: true,
|
||||||
|
leftPanelView: 'toolPicker',
|
||||||
|
readerMode: false,
|
||||||
|
previewFile: null,
|
||||||
|
pageEditorFunctions: null,
|
||||||
|
searchQuery: '',
|
||||||
|
selectedToolKey: null,
|
||||||
|
selectedTool: null,
|
||||||
|
toolRegistry: {},
|
||||||
|
filteredTools: [],
|
||||||
|
isPanelVisible: true,
|
||||||
|
setSidebarsVisible: () => {},
|
||||||
|
setLeftPanelView: () => {},
|
||||||
|
setReaderMode: () => {},
|
||||||
|
setPreviewFile: () => {},
|
||||||
|
setPageEditorFunctions: () => {},
|
||||||
|
setSearchQuery: () => {},
|
||||||
|
selectTool: () => {},
|
||||||
|
clearToolSelection: () => {},
|
||||||
|
handleToolSelect: () => {},
|
||||||
|
handleBackToTools: () => {},
|
||||||
|
handleReaderToggle: () => {}
|
||||||
|
} as ToolWorkflowContextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
|
||||||
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { type TFunction } from 'i18next';
|
import { type TFunction } from 'i18next';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
||||||
import { BaseToolProps } from '../types/tool';
|
import { BaseToolProps } from '../types/tool';
|
||||||
|
import { BaseParameters } from '../types/parameters';
|
||||||
|
|
||||||
export enum SubcategoryId {
|
export enum SubcategoryId {
|
||||||
SIGNING = 'signing',
|
SIGNING = 'signing',
|
||||||
@ -23,18 +25,22 @@ export enum ToolCategoryId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ToolRegistryEntry = {
|
export type ToolRegistryEntry = {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
name: string;
|
name: string;
|
||||||
component: React.ComponentType<BaseToolProps> | null;
|
component: React.ComponentType<BaseToolProps> | null;
|
||||||
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
|
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
|
||||||
description: string;
|
description: string;
|
||||||
categoryId: ToolCategoryId;
|
categoryId: ToolCategoryId;
|
||||||
subcategoryId: SubcategoryId;
|
subcategoryId: SubcategoryId;
|
||||||
maxFiles?: number;
|
maxFiles?: number;
|
||||||
supportedFormats?: string[];
|
supportedFormats?: string[];
|
||||||
endpoints?: string[];
|
endpoints?: string[];
|
||||||
link?: string;
|
link?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
// Operation configuration for automation
|
||||||
|
operationConfig?: ToolOperationConfig<any>;
|
||||||
|
// Settings component for automation configuration
|
||||||
|
settingsComponent?: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>;
|
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SplitPdfPanel from "../tools/Split";
|
import SplitPdfPanel from "../tools/Split";
|
||||||
import CompressPdfPanel from "../tools/Compress";
|
import CompressPdfPanel from "../tools/Compress";
|
||||||
@ -13,6 +14,30 @@ import Repair from '../tools/Repair';
|
|||||||
import SingleLargePage from '../tools/SingleLargePage';
|
import SingleLargePage from '../tools/SingleLargePage';
|
||||||
import UnlockPdfForms from '../tools/UnlockPdfForms';
|
import UnlockPdfForms from '../tools/UnlockPdfForms';
|
||||||
import RemoveCertificateSign from '../tools/RemoveCertificateSign';
|
import RemoveCertificateSign from '../tools/RemoveCertificateSign';
|
||||||
|
import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation';
|
||||||
|
import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation';
|
||||||
|
import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation';
|
||||||
|
import { removePasswordOperationConfig } from '../hooks/tools/removePassword/useRemovePasswordOperation';
|
||||||
|
import { sanitizeOperationConfig } from '../hooks/tools/sanitize/useSanitizeOperation';
|
||||||
|
import { repairOperationConfig } from '../hooks/tools/repair/useRepairOperation';
|
||||||
|
import { addWatermarkOperationConfig } from '../hooks/tools/addWatermark/useAddWatermarkOperation';
|
||||||
|
import { unlockPdfFormsOperationConfig } from '../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation';
|
||||||
|
import { singleLargePageOperationConfig } from '../hooks/tools/singleLargePage/useSingleLargePageOperation';
|
||||||
|
import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation';
|
||||||
|
import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation';
|
||||||
|
import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation';
|
||||||
|
import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation';
|
||||||
|
import CompressSettings from '../components/tools/compress/CompressSettings';
|
||||||
|
import SplitSettings from '../components/tools/split/SplitSettings';
|
||||||
|
import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings';
|
||||||
|
import RemovePasswordSettings from '../components/tools/removePassword/RemovePasswordSettings';
|
||||||
|
import SanitizeSettings from '../components/tools/sanitize/SanitizeSettings';
|
||||||
|
import RepairSettings from '../components/tools/repair/RepairSettings';
|
||||||
|
import UnlockPdfFormsSettings from '../components/tools/unlockPdfForms/UnlockPdfFormsSettings';
|
||||||
|
import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/AddWatermarkSingleStepSettings';
|
||||||
|
import OCRSettings from '../components/tools/ocr/OCRSettings';
|
||||||
|
import ConvertSettings from '../components/tools/convert/ConvertSettings';
|
||||||
|
import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings';
|
||||||
|
|
||||||
const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented
|
const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented
|
||||||
|
|
||||||
@ -20,7 +45,8 @@ const showPlaceholderTools = false; // For development purposes. Allows seeing t
|
|||||||
export function useFlatToolRegistry(): ToolRegistry {
|
export function useFlatToolRegistry(): ToolRegistry {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const allTools: ToolRegistry = {
|
return useMemo(() => {
|
||||||
|
const allTools: ToolRegistry = {
|
||||||
// Signing
|
// Signing
|
||||||
|
|
||||||
"certSign": {
|
"certSign": {
|
||||||
@ -54,7 +80,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-password"]
|
endpoints: ["add-password"],
|
||||||
|
operationConfig: addPasswordOperationConfig,
|
||||||
|
settingsComponent: AddPasswordSettings
|
||||||
},
|
},
|
||||||
"watermark": {
|
"watermark": {
|
||||||
icon: <span className="material-symbols-rounded">branding_watermark</span>,
|
icon: <span className="material-symbols-rounded">branding_watermark</span>,
|
||||||
@ -65,7 +93,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
|
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
endpoints: ["add-watermark"]
|
endpoints: ["add-watermark"],
|
||||||
|
operationConfig: addWatermarkOperationConfig,
|
||||||
|
settingsComponent: AddWatermarkSingleStepSettings
|
||||||
},
|
},
|
||||||
"add-stamp": {
|
"add-stamp": {
|
||||||
icon: <span className="material-symbols-rounded">approval</span>,
|
icon: <span className="material-symbols-rounded">approval</span>,
|
||||||
@ -85,7 +115,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
|
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
|
||||||
endpoints: ["sanitize-pdf"]
|
endpoints: ["sanitize-pdf"],
|
||||||
|
operationConfig: sanitizeOperationConfig,
|
||||||
|
settingsComponent: SanitizeSettings
|
||||||
},
|
},
|
||||||
"flatten": {
|
"flatten": {
|
||||||
icon: <span className="material-symbols-rounded">layers_clear</span>,
|
icon: <span className="material-symbols-rounded">layers_clear</span>,
|
||||||
@ -105,7 +137,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["unlock-pdf-forms"]
|
endpoints: ["unlock-pdf-forms"],
|
||||||
|
operationConfig: unlockPdfFormsOperationConfig,
|
||||||
|
settingsComponent: UnlockPdfFormsSettings
|
||||||
},
|
},
|
||||||
"manage-certificates": {
|
"manage-certificates": {
|
||||||
icon: <span className="material-symbols-rounded">license</span>,
|
icon: <span className="material-symbols-rounded">license</span>,
|
||||||
@ -125,7 +159,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["add-password"]
|
endpoints: ["add-password"],
|
||||||
|
operationConfig: changePermissionsOperationConfig,
|
||||||
|
settingsComponent: ChangePermissionsSettings
|
||||||
},
|
},
|
||||||
// Verification
|
// Verification
|
||||||
|
|
||||||
@ -196,7 +232,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
view: "split",
|
view: "split",
|
||||||
description: t("home.split.desc", "Split PDFs into multiple documents"),
|
description: t("home.split.desc", "Split PDFs into multiple documents"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
|
operationConfig: splitOperationConfig,
|
||||||
|
settingsComponent: SplitSettings
|
||||||
},
|
},
|
||||||
"reorganize-pages": {
|
"reorganize-pages": {
|
||||||
icon: <span className="material-symbols-rounded">move_down</span>,
|
icon: <span className="material-symbols-rounded">move_down</span>,
|
||||||
@ -243,7 +281,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["pdf-to-single-page"]
|
endpoints: ["pdf-to-single-page"],
|
||||||
|
operationConfig: singleLargePageOperationConfig
|
||||||
},
|
},
|
||||||
"add-attachments": {
|
"add-attachments": {
|
||||||
icon: <span className="material-symbols-rounded">attachment</span>,
|
icon: <span className="material-symbols-rounded">attachment</span>,
|
||||||
@ -326,7 +365,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
endpoints: ["remove-password"],
|
endpoints: ["remove-password"],
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
|
operationConfig: removePasswordOperationConfig,
|
||||||
|
settingsComponent: RemovePasswordSettings
|
||||||
},
|
},
|
||||||
"remove-certificate-sign": {
|
"remove-certificate-sign": {
|
||||||
icon: <span className="material-symbols-rounded">remove_moderator</span>,
|
icon: <span className="material-symbols-rounded">remove_moderator</span>,
|
||||||
@ -337,7 +377,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.REMOVAL,
|
subcategoryId: SubcategoryId.REMOVAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["remove-certificate-sign"]
|
endpoints: ["remove-certificate-sign"],
|
||||||
|
operationConfig: removeCertificateSignOperationConfig
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -346,11 +387,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"automate": {
|
"automate": {
|
||||||
icon: <span className="material-symbols-rounded">automation</span>,
|
icon: <span className="material-symbols-rounded">automation</span>,
|
||||||
name: t("home.automate.title", "Automate"),
|
name: t("home.automate.title", "Automate"),
|
||||||
component: null,
|
component: React.lazy(() => import('../tools/Automate')),
|
||||||
view: "format",
|
view: "format",
|
||||||
description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."),
|
description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."),
|
||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.AUTOMATION
|
subcategoryId: SubcategoryId.AUTOMATION,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["handleData"]
|
||||||
},
|
},
|
||||||
"auto-rename-pdf-file": {
|
"auto-rename-pdf-file": {
|
||||||
icon: <span className="material-symbols-rounded">match_word</span>,
|
icon: <span className="material-symbols-rounded">match_word</span>,
|
||||||
@ -401,7 +444,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["repair"]
|
endpoints: ["repair"],
|
||||||
|
operationConfig: repairOperationConfig,
|
||||||
|
settingsComponent: RepairSettings
|
||||||
},
|
},
|
||||||
"detect-split-scanned-photos": {
|
"detect-split-scanned-photos": {
|
||||||
icon: <span className="material-symbols-rounded">scanner</span>,
|
icon: <span className="material-symbols-rounded">scanner</span>,
|
||||||
@ -530,7 +575,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
|
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1
|
maxFiles: -1,
|
||||||
|
operationConfig: compressOperationConfig,
|
||||||
|
settingsComponent: CompressSettings
|
||||||
},
|
},
|
||||||
"convert": {
|
"convert": {
|
||||||
icon: <span className="material-symbols-rounded">sync_alt</span>,
|
icon: <span className="material-symbols-rounded">sync_alt</span>,
|
||||||
@ -574,7 +621,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"zip",
|
"zip",
|
||||||
// Other
|
// Other
|
||||||
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
||||||
]
|
],
|
||||||
|
operationConfig: convertOperationConfig,
|
||||||
|
settingsComponent: ConvertSettings
|
||||||
},
|
},
|
||||||
"mergePdfs": {
|
"mergePdfs": {
|
||||||
icon: <span className="material-symbols-rounded">library_add</span>,
|
icon: <span className="material-symbols-rounded">library_add</span>,
|
||||||
@ -604,7 +653,9 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
|
description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1
|
maxFiles: -1,
|
||||||
|
operationConfig: ocrOperationConfig,
|
||||||
|
settingsComponent: OCRSettings
|
||||||
},
|
},
|
||||||
"redact": {
|
"redact": {
|
||||||
icon: <span className="material-symbols-rounded">visibility_off</span>,
|
icon: <span className="material-symbols-rounded">visibility_off</span>,
|
||||||
@ -617,15 +668,16 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showPlaceholderTools) {
|
if (showPlaceholderTools) {
|
||||||
return allTools;
|
return allTools;
|
||||||
} else {
|
} else {
|
||||||
const filteredTools = Object.keys(allTools)
|
const filteredTools = Object.keys(allTools)
|
||||||
.filter(key => allTools[key].component !== null || allTools[key].link)
|
.filter(key => allTools[key].component !== null || allTools[key].link)
|
||||||
.reduce((obj, key) => {
|
.reduce((obj, key) => {
|
||||||
obj[key] = allTools[key];
|
obj[key] = allTools[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {} as ToolRegistry);
|
}, {} as ToolRegistry);
|
||||||
return filteredTools;
|
return filteredTools;
|
||||||
}
|
}
|
||||||
|
}, [t]); // Only re-compute when translations change
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../sha
|
|||||||
describe('useAddPasswordOperation', () => {
|
describe('useAddPasswordOperation', () => {
|
||||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||||
|
|
||||||
const getToolConfig = (): ToolOperationConfig<AddPasswordFullParameters> => mockUseToolOperation.mock.calls[0][0];
|
const getToolConfig = (): ToolOperationConfig<AddPasswordFullParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<AddPasswordFullParameters>;
|
||||||
|
|
||||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||||
files: [],
|
files: [],
|
||||||
|
@ -1,30 +1,45 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { AddPasswordFullParameters } from './useAddPasswordParameters';
|
import { AddPasswordFullParameters, defaultParameters } from './useAddPasswordParameters';
|
||||||
|
import { defaultParameters as permissionsDefaults } from '../changePermissions/useChangePermissionsParameters';
|
||||||
import { getFormData } from '../changePermissions/useChangePermissionsOperation';
|
import { getFormData } from '../changePermissions/useChangePermissionsOperation';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildAddPasswordFormData = (parameters: AddPasswordFullParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("password", parameters.password);
|
||||||
|
formData.append("ownerPassword", parameters.ownerPassword);
|
||||||
|
formData.append("keyLength", parameters.keyLength.toString());
|
||||||
|
getFormData(parameters.permissions).forEach(([key, value]) => {
|
||||||
|
formData.append(key, value);
|
||||||
|
});
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Full default parameters including permissions for automation
|
||||||
|
const fullDefaultParameters: AddPasswordFullParameters = {
|
||||||
|
...defaultParameters,
|
||||||
|
permissions: permissionsDefaults,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const addPasswordOperationConfig = {
|
||||||
|
operationType: 'addPassword',
|
||||||
|
endpoint: '/api/v1/security/add-password',
|
||||||
|
buildFormData: buildAddPasswordFormData,
|
||||||
|
filePrefix: 'encrypted_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters: fullDefaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useAddPasswordOperation = () => {
|
export const useAddPasswordOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: AddPasswordFullParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
formData.append("password", parameters.password);
|
|
||||||
formData.append("ownerPassword", parameters.ownerPassword);
|
|
||||||
formData.append("keyLength", parameters.keyLength.toString());
|
|
||||||
getFormData(parameters.permissions).forEach(([key, value]) => {
|
|
||||||
formData.append(key, value);
|
|
||||||
});
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<AddPasswordFullParameters>({
|
return useToolOperation<AddPasswordFullParameters>({
|
||||||
operationType: 'addPassword',
|
...addPasswordOperationConfig,
|
||||||
endpoint: '/api/v1/security/add-password',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
|
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { AddWatermarkParameters } from './useAddWatermarkParameters';
|
import { AddWatermarkParameters, defaultParameters } from './useAddWatermarkParameters';
|
||||||
|
|
||||||
const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData => {
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildAddWatermarkFormData = (parameters: AddWatermarkParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
|
|
||||||
@ -32,15 +33,22 @@ const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const addWatermarkOperationConfig = {
|
||||||
|
operationType: 'watermark',
|
||||||
|
endpoint: '/api/v1/security/add-watermark',
|
||||||
|
buildFormData: buildAddWatermarkFormData,
|
||||||
|
filePrefix: 'watermarked_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useAddWatermarkOperation = () => {
|
export const useAddWatermarkOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<AddWatermarkParameters>({
|
return useToolOperation<AddWatermarkParameters>({
|
||||||
operationType: 'watermark',
|
...addWatermarkOperationConfig,
|
||||||
endpoint: '/api/v1/security/add-watermark',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
|
filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
|
||||||
multiFileEndpoint: false, // Individual API calls per file
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -46,3 +46,4 @@ export const useAddWatermarkParameters = (): AddWatermarkParametersHook => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
49
frontend/src/hooks/tools/automate/useAutomateOperation.ts
Normal file
49
frontend/src/hooks/tools/automate/useAutomateOperation.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
||||||
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
|
import { AutomateParameters } from '../../../types/automation';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
|
|
||||||
|
export function useAutomateOperation() {
|
||||||
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
|
||||||
|
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
|
||||||
|
console.log('🚀 Starting automation execution via customProcessor', { params, files });
|
||||||
|
|
||||||
|
if (!params.automationConfig) {
|
||||||
|
throw new Error('No automation configuration provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the automation sequence and return the final results
|
||||||
|
const finalResults = await executeAutomationSequence(
|
||||||
|
params.automationConfig!,
|
||||||
|
files,
|
||||||
|
toolRegistry,
|
||||||
|
(stepIndex: number, operationName: string) => {
|
||||||
|
console.log(`Step ${stepIndex + 1} started: ${operationName}`);
|
||||||
|
params.onStepStart?.(stepIndex, operationName);
|
||||||
|
},
|
||||||
|
(stepIndex: number, resultFiles: File[]) => {
|
||||||
|
console.log(`Step ${stepIndex + 1} completed with ${resultFiles.length} files`);
|
||||||
|
params.onStepComplete?.(stepIndex, resultFiles);
|
||||||
|
},
|
||||||
|
(stepIndex: number, error: string) => {
|
||||||
|
console.error(`Step ${stepIndex + 1} failed:`, error);
|
||||||
|
params.onStepError?.(stepIndex, error);
|
||||||
|
throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Automation completed, returning ${finalResults.length} files`);
|
||||||
|
return finalResults;
|
||||||
|
}, [toolRegistry]);
|
||||||
|
|
||||||
|
return useToolOperation<AutomateParameters>({
|
||||||
|
operationType: 'automate',
|
||||||
|
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
|
||||||
|
buildFormData: () => new FormData(), // Not used with customProcessor
|
||||||
|
customProcessor,
|
||||||
|
filePrefix: AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
|
});
|
||||||
|
}
|
114
frontend/src/hooks/tools/automate/useAutomationForm.ts
Normal file
114
frontend/src/hooks/tools/automate/useAutomationForm.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
|
||||||
|
interface UseAutomationFormProps {
|
||||||
|
mode: AutomationMode;
|
||||||
|
existingAutomation?: AutomationConfig;
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [automationName, setAutomationName] = useState('');
|
||||||
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
|
|
||||||
|
const getToolName = (operation: string) => {
|
||||||
|
const tool = toolRegistry?.[operation] as any;
|
||||||
|
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToolDefaultParameters = (operation: string): Record<string, any> => {
|
||||||
|
const config = toolRegistry[operation]?.operationConfig;
|
||||||
|
if (config?.defaultParameters) {
|
||||||
|
return { ...config.defaultParameters };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize based on mode and existing automation
|
||||||
|
useEffect(() => {
|
||||||
|
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
||||||
|
setAutomationName(existingAutomation.name || '');
|
||||||
|
|
||||||
|
const operations = existingAutomation.operations || [];
|
||||||
|
const tools = operations.map((op, index) => {
|
||||||
|
const operation = typeof op === 'string' ? op : op.operation;
|
||||||
|
return {
|
||||||
|
id: `${operation}-${Date.now()}-${index}`,
|
||||||
|
operation: operation,
|
||||||
|
name: getToolName(operation),
|
||||||
|
configured: mode === AutomationMode.EDIT ? true : false,
|
||||||
|
parameters: typeof op === 'object' ? op.parameters || {} : {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedTools(tools);
|
||||||
|
} else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
|
||||||
|
// Initialize with default empty tools for new automation
|
||||||
|
const defaultTools = Array.from({ length: AUTOMATION_CONSTANTS.DEFAULT_TOOL_COUNT }, (_, index) => ({
|
||||||
|
id: `tool-${index + 1}-${Date.now()}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
}));
|
||||||
|
setSelectedTools(defaultTools);
|
||||||
|
}
|
||||||
|
}, [mode, existingAutomation, selectedTools.length, t, getToolName]);
|
||||||
|
|
||||||
|
const addTool = (operation: string) => {
|
||||||
|
const newTool: AutomationTool = {
|
||||||
|
id: `${operation}-${Date.now()}`,
|
||||||
|
operation,
|
||||||
|
name: getToolName(operation),
|
||||||
|
configured: false,
|
||||||
|
parameters: getToolDefaultParameters(operation)
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedTools([...selectedTools, newTool]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTool = (index: number) => {
|
||||||
|
if (selectedTools.length <= AUTOMATION_CONSTANTS.MIN_TOOL_COUNT) return;
|
||||||
|
setSelectedTools(selectedTools.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTool = (index: number, updates: Partial<AutomationTool>) => {
|
||||||
|
const updatedTools = [...selectedTools];
|
||||||
|
updatedTools[index] = { ...updatedTools[index], ...updates };
|
||||||
|
setSelectedTools(updatedTools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUnsavedChanges = () => {
|
||||||
|
return (
|
||||||
|
automationName.trim() !== '' ||
|
||||||
|
selectedTools.some(tool => tool.operation !== '' || tool.configured)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSaveAutomation = () => {
|
||||||
|
return (
|
||||||
|
automationName.trim() !== '' &&
|
||||||
|
selectedTools.length > 0 &&
|
||||||
|
selectedTools.every(tool => tool.configured && tool.operation !== '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
automationName,
|
||||||
|
setAutomationName,
|
||||||
|
selectedTools,
|
||||||
|
setSelectedTools,
|
||||||
|
addTool,
|
||||||
|
removeTool,
|
||||||
|
updateTool,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
canSaveAutomation,
|
||||||
|
getToolName,
|
||||||
|
getToolDefaultParameters
|
||||||
|
};
|
||||||
|
}
|
55
frontend/src/hooks/tools/automate/useSavedAutomations.ts
Normal file
55
frontend/src/hooks/tools/automate/useSavedAutomations.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { AutomationConfig } from '../../../services/automationStorage';
|
||||||
|
|
||||||
|
export interface SavedAutomation extends AutomationConfig {}
|
||||||
|
|
||||||
|
export function useSavedAutomations() {
|
||||||
|
const [savedAutomations, setSavedAutomations] = useState<SavedAutomation[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const loadSavedAutomations = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
|
const automations = await automationStorage.getAllAutomations();
|
||||||
|
setSavedAutomations(automations);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading saved automations:', err);
|
||||||
|
setError(err as Error);
|
||||||
|
setSavedAutomations([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshAutomations = useCallback(() => {
|
||||||
|
loadSavedAutomations();
|
||||||
|
}, [loadSavedAutomations]);
|
||||||
|
|
||||||
|
const deleteAutomation = useCallback(async (id: string) => {
|
||||||
|
try {
|
||||||
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
|
await automationStorage.deleteAutomation(id);
|
||||||
|
// Refresh the list after deletion
|
||||||
|
refreshAutomations();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting automation:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, [refreshAutomations]);
|
||||||
|
|
||||||
|
// Load automations on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadSavedAutomations();
|
||||||
|
}, [loadSavedAutomations]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
savedAutomations,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refreshAutomations,
|
||||||
|
deleteAutomation
|
||||||
|
};
|
||||||
|
}
|
53
frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
Normal file
53
frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
|
import { SuggestedAutomation } from '../../../types/automation';
|
||||||
|
|
||||||
|
export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const suggestedAutomations = useMemo<SuggestedAutomation[]>(() => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "compress-and-merge",
|
||||||
|
name: t("automation.suggested.compressAndMerge", "Compress & Merge"),
|
||||||
|
description: t("automation.suggested.compressAndMergeDesc", "Compress PDFs and merge them into one file"),
|
||||||
|
operations: [
|
||||||
|
{ operation: "compress", parameters: {} },
|
||||||
|
{ operation: "merge", parameters: {} }
|
||||||
|
],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
icon: StarIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ocr-and-convert",
|
||||||
|
name: t("automation.suggested.ocrAndConvert", "OCR & Convert"),
|
||||||
|
description: t("automation.suggested.ocrAndConvertDesc", "Extract text via OCR and convert to different format"),
|
||||||
|
operations: [
|
||||||
|
{ operation: "ocr", parameters: {} },
|
||||||
|
{ operation: "convert", parameters: {} }
|
||||||
|
],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
icon: StarIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secure-workflow",
|
||||||
|
name: t("automation.suggested.secureWorkflow", "Secure Workflow"),
|
||||||
|
description: t("automation.suggested.secureWorkflowDesc", "Sanitize, add password, and set permissions"),
|
||||||
|
operations: [
|
||||||
|
{ operation: "sanitize", parameters: {} },
|
||||||
|
{ operation: "addPassword", parameters: {} },
|
||||||
|
{ operation: "changePermissions", parameters: {} }
|
||||||
|
],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
icon: StarIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
return suggestedAutomations;
|
||||||
|
}
|
@ -25,7 +25,7 @@ import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../sha
|
|||||||
describe('useChangePermissionsOperation', () => {
|
describe('useChangePermissionsOperation', () => {
|
||||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||||
|
|
||||||
const getToolConfig = (): ToolOperationConfig<ChangePermissionsParameters> => mockUseToolOperation.mock.calls[0][0];
|
const getToolConfig = (): ToolOperationConfig<ChangePermissionsParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<ChangePermissionsParameters>;
|
||||||
|
|
||||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||||
files: [],
|
files: [],
|
||||||
@ -109,7 +109,7 @@ describe('useChangePermissionsOperation', () => {
|
|||||||
{ property: 'multiFileEndpoint' as const, expectedValue: false },
|
{ property: 'multiFileEndpoint' as const, expectedValue: false },
|
||||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
|
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
|
||||||
{ property: 'filePrefix' as const, expectedValue: 'permissions_' },
|
{ property: 'filePrefix' as const, expectedValue: 'permissions_' },
|
||||||
{ property: 'operationType' as const, expectedValue: 'changePermissions' }
|
{ property: 'operationType' as const, expectedValue: 'change-permissions' }
|
||||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||||
renderHook(() => useChangePermissionsOperation());
|
renderHook(() => useChangePermissionsOperation());
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import type { ChangePermissionsParameters } from './useChangePermissionsParameters';
|
import { ChangePermissionsParameters, defaultParameters } from './useChangePermissionsParameters';
|
||||||
|
|
||||||
export const getFormData = ((parameters: ChangePermissionsParameters) =>
|
export const getFormData = ((parameters: ChangePermissionsParameters) =>
|
||||||
Object.entries(parameters).map(([key, value]) =>
|
Object.entries(parameters).map(([key, value]) =>
|
||||||
@ -9,27 +9,34 @@ export const getFormData = ((parameters: ChangePermissionsParameters) =>
|
|||||||
) as string[][]
|
) as string[][]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildChangePermissionsFormData = (parameters: ChangePermissionsParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
|
||||||
|
// Add all permission parameters
|
||||||
|
getFormData(parameters).forEach(([key, value]) => {
|
||||||
|
formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const changePermissionsOperationConfig = {
|
||||||
|
operationType: 'change-permissions',
|
||||||
|
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
|
||||||
|
buildFormData: buildChangePermissionsFormData,
|
||||||
|
filePrefix: 'permissions_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useChangePermissionsOperation = () => {
|
export const useChangePermissionsOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: ChangePermissionsParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
|
|
||||||
// Add all permission parameters
|
|
||||||
getFormData(parameters).forEach(([key, value]) => {
|
|
||||||
formData.append(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation({
|
return useToolOperation({
|
||||||
operationType: 'changePermissions',
|
...changePermissionsOperationConfig,
|
||||||
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: 'permissions_',
|
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(
|
getErrorMessage: createStandardErrorHandler(
|
||||||
t('changePermissions.error.failed', 'An error occurred while changing PDF permissions.')
|
t('changePermissions.error.failed', 'An error occurred while changing PDF permissions.')
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { CompressParameters } from './useCompressParameters';
|
import { CompressParameters, defaultParameters } from './useCompressParameters';
|
||||||
|
|
||||||
const buildFormData = (parameters: CompressParameters, file: File): FormData => {
|
// Static configuration that can be used by both the hook and automation executor
|
||||||
|
export const buildCompressFormData = (parameters: CompressParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
|
|
||||||
@ -21,15 +22,21 @@ const buildFormData = (parameters: CompressParameters, file: File): FormData =>
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const compressOperationConfig = {
|
||||||
|
operationType: 'compress',
|
||||||
|
endpoint: '/api/v1/misc/compress-pdf',
|
||||||
|
buildFormData: buildCompressFormData,
|
||||||
|
filePrefix: 'compressed_',
|
||||||
|
multiFileEndpoint: false, // Individual API calls per file
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useCompressOperation = () => {
|
export const useCompressOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<CompressParameters>({
|
return useToolOperation<CompressParameters>({
|
||||||
operationType: 'compress',
|
...compressOperationConfig,
|
||||||
endpoint: '/api/v1/misc/compress-pdf',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: 'compressed_',
|
|
||||||
multiFileEndpoint: false, // Individual API calls per file
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@ export interface CompressParameters extends BaseParameters {
|
|||||||
fileSizeUnit: 'KB' | 'MB';
|
fileSizeUnit: 'KB' | 'MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultParameters: CompressParameters = {
|
export const defaultParameters: CompressParameters = {
|
||||||
compressionLevel: 5,
|
compressionLevel: 5,
|
||||||
grayscale: false,
|
grayscale: false,
|
||||||
expectedSize: '',
|
expectedSize: '',
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConvertParameters } from './useConvertParameters';
|
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
||||||
import { detectFileExtension } from '../../../utils/fileUtils';
|
import { detectFileExtension } from '../../../utils/fileUtils';
|
||||||
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||||
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
||||||
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
||||||
|
|
||||||
const shouldProcessFilesSeparately = (
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const shouldProcessFilesSeparately = (
|
||||||
selectedFiles: File[],
|
selectedFiles: File[],
|
||||||
parameters: ConvertParameters
|
parameters: ConvertParameters
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@ -29,7 +30,8 @@ const shouldProcessFilesSeparately = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => {
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildConvertFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
selectedFiles.forEach(file => {
|
selectedFiles.forEach(file => {
|
||||||
@ -69,7 +71,8 @@ const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): Fo
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFileFromResponse = (
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const createFileFromResponse = (
|
||||||
responseData: any,
|
responseData: any,
|
||||||
headers: any,
|
headers: any,
|
||||||
originalFileName: string,
|
originalFileName: string,
|
||||||
@ -81,6 +84,59 @@ const createFileFromResponse = (
|
|||||||
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static processor that can be used by both the hook and automation executor
|
||||||
|
export const convertProcessor = async (
|
||||||
|
parameters: ConvertParameters,
|
||||||
|
selectedFiles: File[]
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const processedFiles: File[] = [];
|
||||||
|
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error('Unsupported conversion format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert-specific routing logic: decide batch vs individual processing
|
||||||
|
if (shouldProcessFilesSeparately(selectedFiles, parameters)) {
|
||||||
|
// Individual processing for complex cases (PDF→image, smart detection, etc.)
|
||||||
|
for (const file of selectedFiles) {
|
||||||
|
try {
|
||||||
|
const formData = buildConvertFormData(parameters, [file]);
|
||||||
|
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
|
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
||||||
|
|
||||||
|
processedFiles.push(convertedFile);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to convert file ${file.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Batch processing for simple cases (image→PDF combine)
|
||||||
|
const formData = buildConvertFormData(parameters, selectedFiles);
|
||||||
|
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
|
const baseFilename = selectedFiles.length === 1
|
||||||
|
? selectedFiles[0].name
|
||||||
|
: 'converted_files';
|
||||||
|
|
||||||
|
const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension);
|
||||||
|
processedFiles.push(convertedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const convertOperationConfig = {
|
||||||
|
operationType: 'convert',
|
||||||
|
endpoint: '', // Not used with customProcessor but required
|
||||||
|
buildFormData: buildConvertFormData, // Not used with customProcessor but required
|
||||||
|
filePrefix: 'converted_',
|
||||||
|
customProcessor: convertProcessor,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useConvertOperation = () => {
|
export const useConvertOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -88,52 +144,12 @@ export const useConvertOperation = () => {
|
|||||||
parameters: ConvertParameters,
|
parameters: ConvertParameters,
|
||||||
selectedFiles: File[]
|
selectedFiles: File[]
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
|
return convertProcessor(parameters, selectedFiles);
|
||||||
const processedFiles: File[] = [];
|
}, []);
|
||||||
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
|
|
||||||
|
|
||||||
if (!endpoint) {
|
|
||||||
throw new Error(t('errorNotSupported', 'Unsupported conversion format'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert-specific routing logic: decide batch vs individual processing
|
|
||||||
if (shouldProcessFilesSeparately(selectedFiles, parameters)) {
|
|
||||||
// Individual processing for complex cases (PDF→image, smart detection, etc.)
|
|
||||||
for (const file of selectedFiles) {
|
|
||||||
try {
|
|
||||||
const formData = buildFormData(parameters, [file]);
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
|
||||||
|
|
||||||
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
|
||||||
|
|
||||||
processedFiles.push(convertedFile);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to convert file ${file.name}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Batch processing for simple cases (image→PDF combine)
|
|
||||||
const formData = buildFormData(parameters, selectedFiles);
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
|
||||||
|
|
||||||
const baseFilename = selectedFiles.length === 1
|
|
||||||
? selectedFiles[0].name
|
|
||||||
: 'converted_files';
|
|
||||||
|
|
||||||
const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension);
|
|
||||||
processedFiles.push(convertedFile);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedFiles;
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
return useToolOperation<ConvertParameters>({
|
return useToolOperation<ConvertParameters>({
|
||||||
operationType: 'convert',
|
...convertOperationConfig,
|
||||||
endpoint: '', // Not used with customProcessor but required
|
customProcessor: customConvertProcessor, // Use instance-specific processor for translation support
|
||||||
buildFormData, // Not used with customProcessor but required
|
|
||||||
filePrefix: 'converted_',
|
|
||||||
customProcessor: customConvertProcessor, // Convert handles its own routing
|
|
||||||
getErrorMessage: (error) => {
|
getErrorMessage: (error) => {
|
||||||
if (error.response?.data && typeof error.response.data === 'string') {
|
if (error.response?.data && typeof error.response.data === 'string') {
|
||||||
return error.response.data;
|
return error.response.data;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
type OutputOption,
|
type OutputOption,
|
||||||
type FitOption
|
type FitOption
|
||||||
} from '../../../constants/convertConstants';
|
} from '../../../constants/convertConstants';
|
||||||
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat, getAvailableToExtensions as getAvailableToExtensionsUtil } from '../../../utils/convertUtils';
|
||||||
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
|
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
|
||||||
import { BaseParameters } from '../../../types/parameters';
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
@ -47,7 +47,7 @@ export interface ConvertParametersHook extends BaseParametersHook<ConvertParamet
|
|||||||
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultParameters: ConvertParameters = {
|
export const defaultParameters: ConvertParameters = {
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
toExtension: '',
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
@ -155,30 +155,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
return getEndpointUrl(fromExtension, toExtension);
|
return getEndpointUrl(fromExtension, toExtension);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableToExtensions = (fromExtension: string) => {
|
const getAvailableToExtensions = getAvailableToExtensionsUtil;
|
||||||
if (!fromExtension) return [];
|
|
||||||
|
|
||||||
// Handle dynamic format identifiers (file-<extension>)
|
|
||||||
if (fromExtension.startsWith('file-')) {
|
|
||||||
// Dynamic format - use 'any' conversion options (file-to-pdf)
|
|
||||||
const supportedExtensions = CONVERSION_MATRIX['any'] || [];
|
|
||||||
return TO_FORMAT_OPTIONS.filter(option =>
|
|
||||||
supportedExtensions.includes(option.value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
|
|
||||||
|
|
||||||
// If no explicit conversion exists, but file-to-pdf might be available,
|
|
||||||
// fall back to 'any' conversion (which converts unknown files to PDF via file-to-pdf)
|
|
||||||
if (supportedExtensions.length === 0 && fromExtension !== 'any') {
|
|
||||||
supportedExtensions = CONVERSION_MATRIX['any'] || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return TO_FORMAT_OPTIONS.filter(option =>
|
|
||||||
supportedExtensions.includes(option.value)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const analyzeFileTypes = useCallback((files: Array<{name: string}>) => {
|
const analyzeFileTypes = useCallback((files: Array<{name: string}>) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { OCRParameters } from './useOCRParameters';
|
import { OCRParameters, defaultParameters } from './useOCRParameters';
|
||||||
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { useToolResources } from '../shared/useToolResources';
|
import { useToolResources } from '../shared/useToolResources';
|
||||||
@ -37,7 +37,8 @@ function stripExt(name: string): string {
|
|||||||
return i > 0 ? name.slice(0, i) : name;
|
return i > 0 ? name.slice(0, i) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFormData = (parameters: OCRParameters, file: File): FormData => {
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildOCRFormData = (parameters: OCRParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fileInput', file);
|
formData.append('fileInput', file);
|
||||||
parameters.languages.forEach((lang) => formData.append('languages', lang));
|
parameters.languages.forEach((lang) => formData.append('languages', lang));
|
||||||
@ -51,57 +52,70 @@ const buildFormData = (parameters: OCRParameters, file: File): FormData => {
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static response handler for OCR - can be used by automation executor
|
||||||
|
export const ocrResponseHandler = async (blob: Blob, originalFiles: File[], extractZipFiles: (blob: Blob) => Promise<File[]>): Promise<File[]> => {
|
||||||
|
const headBuf = await blob.slice(0, 8).arrayBuffer();
|
||||||
|
const head = new TextDecoder().decode(new Uint8Array(headBuf));
|
||||||
|
|
||||||
|
// ZIP: sidecar or multi-asset output
|
||||||
|
if (head.startsWith('PK')) {
|
||||||
|
const base = stripExt(originalFiles[0].name);
|
||||||
|
try {
|
||||||
|
const extractedFiles = await extractZipFiles(blob);
|
||||||
|
if (extractedFiles.length > 0) return extractedFiles;
|
||||||
|
} catch { /* ignore and try local extractor */ }
|
||||||
|
try {
|
||||||
|
const local = await extractZipFile(blob); // local fallback
|
||||||
|
if (local.length > 0) return local;
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
return [new File([blob], `ocr_${base}.zip`, { type: 'application/zip' })];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a PDF: surface error details if present
|
||||||
|
if (!head.startsWith('%PDF')) {
|
||||||
|
const textBuf = await blob.slice(0, 1024).arrayBuffer();
|
||||||
|
const text = new TextDecoder().decode(new Uint8Array(textBuf));
|
||||||
|
if (/error|exception|html/i.test(text)) {
|
||||||
|
if (text.includes('OCR tools') && text.includes('not installed')) {
|
||||||
|
throw new Error('OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.');
|
||||||
|
}
|
||||||
|
const title =
|
||||||
|
text.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1] ||
|
||||||
|
text.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1] ||
|
||||||
|
'Unknown error';
|
||||||
|
throw new Error(`OCR service error: ${title}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Response is not a valid PDF. Header: "${head}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = stripExt(originalFiles[0].name);
|
||||||
|
return [new File([blob], `ocr_${base}.pdf`, { type: 'application/pdf' })];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object (without t function dependencies)
|
||||||
|
export const ocrOperationConfig = {
|
||||||
|
operationType: 'ocr',
|
||||||
|
endpoint: '/api/v1/misc/ocr-pdf',
|
||||||
|
buildFormData: buildOCRFormData,
|
||||||
|
filePrefix: 'ocr_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useOCROperation = () => {
|
export const useOCROperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { extractZipFiles } = useToolResources();
|
const { extractZipFiles } = useToolResources();
|
||||||
|
|
||||||
// OCR-specific parsing: ZIP (sidecar) vs PDF vs HTML error
|
// OCR-specific parsing: ZIP (sidecar) vs PDF vs HTML error
|
||||||
const responseHandler = useCallback(async (blob: Blob, originalFiles: File[]): Promise<File[]> => {
|
const responseHandler = useCallback(async (blob: Blob, originalFiles: File[]): Promise<File[]> => {
|
||||||
const headBuf = await blob.slice(0, 8).arrayBuffer();
|
// extractZipFiles from useToolResources already returns File[] directly
|
||||||
const head = new TextDecoder().decode(new Uint8Array(headBuf));
|
const simpleExtractZipFiles = extractZipFiles;
|
||||||
|
return ocrResponseHandler(blob, originalFiles, simpleExtractZipFiles);
|
||||||
// ZIP: sidecar or multi-asset output
|
}, [extractZipFiles]);
|
||||||
if (head.startsWith('PK')) {
|
|
||||||
const base = stripExt(originalFiles[0].name);
|
|
||||||
try {
|
|
||||||
const extracted = await extractZipFiles(blob);
|
|
||||||
if (extracted.length > 0) return extracted;
|
|
||||||
} catch { /* ignore and try local extractor */ }
|
|
||||||
try {
|
|
||||||
const local = await extractZipFile(blob); // local fallback
|
|
||||||
if (local.length > 0) return local;
|
|
||||||
} catch { /* fall through */ }
|
|
||||||
return [new File([blob], `ocr_${base}.zip`, { type: 'application/zip' })];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not a PDF: surface error details if present
|
|
||||||
if (!head.startsWith('%PDF')) {
|
|
||||||
const textBuf = await blob.slice(0, 1024).arrayBuffer();
|
|
||||||
const text = new TextDecoder().decode(new Uint8Array(textBuf));
|
|
||||||
if (/error|exception|html/i.test(text)) {
|
|
||||||
if (text.includes('OCR tools') && text.includes('not installed')) {
|
|
||||||
throw new Error('OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.');
|
|
||||||
}
|
|
||||||
const title =
|
|
||||||
text.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1] ||
|
|
||||||
text.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1] ||
|
|
||||||
t('ocr.error.unknown', 'Unknown error');
|
|
||||||
throw new Error(`OCR service error: ${title}`);
|
|
||||||
}
|
|
||||||
throw new Error(`Response is not a valid PDF. Header: "${head}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = stripExt(originalFiles[0].name);
|
|
||||||
return [new File([blob], `ocr_${base}.pdf`, { type: 'application/pdf' })];
|
|
||||||
}, [t, extractZipFiles]);
|
|
||||||
|
|
||||||
const ocrConfig: ToolOperationConfig<OCRParameters> = {
|
const ocrConfig: ToolOperationConfig<OCRParameters> = {
|
||||||
operationType: 'ocr',
|
...ocrOperationConfig,
|
||||||
endpoint: '/api/v1/misc/ocr-pdf',
|
responseHandler,
|
||||||
buildFormData,
|
|
||||||
filePrefix: 'ocr_',
|
|
||||||
multiFileEndpoint: false, // Process files individually
|
|
||||||
responseHandler, // use shared flow
|
|
||||||
getErrorMessage: (error) =>
|
getErrorMessage: (error) =>
|
||||||
error.message?.includes('OCR tools') && error.message?.includes('not installed')
|
error.message?.includes('OCR tools') && error.message?.includes('not installed')
|
||||||
? 'OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.'
|
? 'OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.'
|
||||||
|
@ -10,7 +10,7 @@ export interface OCRParameters extends BaseParameters {
|
|||||||
|
|
||||||
export type OCRParametersHook = BaseParametersHook<OCRParameters>;
|
export type OCRParametersHook = BaseParametersHook<OCRParameters>;
|
||||||
|
|
||||||
const defaultParameters: OCRParameters = {
|
export const defaultParameters: OCRParameters = {
|
||||||
languages: [],
|
languages: [],
|
||||||
ocrType: 'skip-text',
|
ocrType: 'skip-text',
|
||||||
ocrRenderType: 'hocr',
|
ocrRenderType: 'hocr',
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { RemoveCertificateSignParameters } from './useRemoveCertificateSignParameters';
|
import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildRemoveCertificateSignFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const removeCertificateSignOperationConfig = {
|
||||||
|
operationType: 'remove-certificate-sign',
|
||||||
|
endpoint: '/api/v1/security/remove-cert-sign',
|
||||||
|
buildFormData: buildRemoveCertificateSignFormData,
|
||||||
|
filePrefix: 'unsigned_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useRemoveCertificateSignOperation = () => {
|
export const useRemoveCertificateSignOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<RemoveCertificateSignParameters>({
|
return useToolOperation<RemoveCertificateSignParameters>({
|
||||||
operationType: 'removeCertificateSign',
|
...removeCertificateSignOperationConfig,
|
||||||
endpoint: '/api/v1/security/remove-cert-sign',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_',
|
filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -25,7 +25,7 @@ import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../sha
|
|||||||
describe('useRemovePasswordOperation', () => {
|
describe('useRemovePasswordOperation', () => {
|
||||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||||
|
|
||||||
const getToolConfig = (): ToolOperationConfig<RemovePasswordParameters> => mockUseToolOperation.mock.calls[0][0];
|
const getToolConfig = (): ToolOperationConfig<RemovePasswordParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<RemovePasswordParameters>;
|
||||||
|
|
||||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||||
files: [],
|
files: [],
|
||||||
|
@ -1,24 +1,32 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { RemovePasswordParameters } from './useRemovePasswordParameters';
|
import { RemovePasswordParameters, defaultParameters } from './useRemovePasswordParameters';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildRemovePasswordFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("password", parameters.password);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const removePasswordOperationConfig = {
|
||||||
|
operationType: 'removePassword',
|
||||||
|
endpoint: '/api/v1/security/remove-password',
|
||||||
|
buildFormData: buildRemovePasswordFormData,
|
||||||
|
filePrefix: 'decrypted_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useRemovePasswordOperation = () => {
|
export const useRemovePasswordOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
formData.append("password", parameters.password);
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<RemovePasswordParameters>({
|
return useToolOperation<RemovePasswordParameters>({
|
||||||
operationType: 'removePassword',
|
...removePasswordOperationConfig,
|
||||||
endpoint: '/api/v1/security/remove-password',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
|
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { RepairParameters } from './useRepairParameters';
|
import { RepairParameters, defaultParameters } from './useRepairParameters';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildRepairFormData = (parameters: RepairParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const repairOperationConfig = {
|
||||||
|
operationType: 'repair',
|
||||||
|
endpoint: '/api/v1/misc/repair',
|
||||||
|
buildFormData: buildRepairFormData,
|
||||||
|
filePrefix: 'repaired_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useRepairOperation = () => {
|
export const useRepairOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: RepairParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<RepairParameters>({
|
return useToolOperation<RepairParameters>({
|
||||||
operationType: 'repair',
|
...repairOperationConfig,
|
||||||
endpoint: '/api/v1/misc/repair',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('repair.filenamePrefix', 'repaired') + '_',
|
filePrefix: t('repair.filenamePrefix', 'repaired') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,9 +1,10 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { SanitizeParameters } from './useSanitizeParameters';
|
import { SanitizeParameters, defaultParameters } from './useSanitizeParameters';
|
||||||
|
|
||||||
const buildFormData = (parameters: SanitizeParameters, file: File): FormData => {
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildSanitizeFormData = (parameters: SanitizeParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fileInput', file);
|
formData.append('fileInput', file);
|
||||||
|
|
||||||
@ -18,15 +19,22 @@ const buildFormData = (parameters: SanitizeParameters, file: File): FormData =>
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const sanitizeOperationConfig = {
|
||||||
|
operationType: 'sanitize',
|
||||||
|
endpoint: '/api/v1/security/sanitize-pdf',
|
||||||
|
buildFormData: buildSanitizeFormData,
|
||||||
|
filePrefix: 'sanitized_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useSanitizeOperation = () => {
|
export const useSanitizeOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<SanitizeParameters>({
|
return useToolOperation<SanitizeParameters>({
|
||||||
operationType: 'sanitize',
|
...sanitizeOperationConfig,
|
||||||
endpoint: '/api/v1/security/sanitize-pdf',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_',
|
filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_',
|
||||||
multiFileEndpoint: false, // Individual API calls per file
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -61,6 +61,9 @@ export interface ToolOperationConfig<TParams = void> {
|
|||||||
|
|
||||||
/** Extract user-friendly error messages from API errors */
|
/** Extract user-friendly error messages from API errors */
|
||||||
getErrorMessage?: (error: any) => string;
|
getErrorMessage?: (error: any) => string;
|
||||||
|
|
||||||
|
/** Default parameter values for automation */
|
||||||
|
defaultParameters?: TParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { SingleLargePageParameters } from './useSingleLargePageParameters';
|
import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildSingleLargePageFormData = (parameters: SingleLargePageParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const singleLargePageOperationConfig = {
|
||||||
|
operationType: 'single-large-page',
|
||||||
|
endpoint: '/api/v1/general/pdf-to-single-page',
|
||||||
|
buildFormData: buildSingleLargePageFormData,
|
||||||
|
filePrefix: 'single_page_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useSingleLargePageOperation = () => {
|
export const useSingleLargePageOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: SingleLargePageParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<SingleLargePageParameters>({
|
return useToolOperation<SingleLargePageParameters>({
|
||||||
operationType: 'singleLargePage',
|
...singleLargePageOperationConfig,
|
||||||
endpoint: '/api/v1/general/pdf-to-single-page',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_',
|
filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,11 +1,11 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { SplitParameters } from './useSplitParameters';
|
import { SplitParameters, defaultParameters } from './useSplitParameters';
|
||||||
import { SPLIT_MODES } from '../../../constants/splitConstants';
|
import { SPLIT_MODES } from '../../../constants/splitConstants';
|
||||||
|
|
||||||
|
// Static functions that can be used by both the hook and automation executor
|
||||||
const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => {
|
export const buildSplitFormData = (parameters: SplitParameters, selectedFiles: File[]): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
selectedFiles.forEach(file => {
|
selectedFiles.forEach(file => {
|
||||||
@ -40,7 +40,7 @@ const buildFormData = (parameters: SplitParameters, selectedFiles: File[]): Form
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEndpoint = (parameters: SplitParameters): string => {
|
export const getSplitEndpoint = (parameters: SplitParameters): string => {
|
||||||
switch (parameters.mode) {
|
switch (parameters.mode) {
|
||||||
case SPLIT_MODES.BY_PAGES:
|
case SPLIT_MODES.BY_PAGES:
|
||||||
return "/api/v1/general/split-pages";
|
return "/api/v1/general/split-pages";
|
||||||
@ -55,15 +55,21 @@ const getEndpoint = (parameters: SplitParameters): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const splitOperationConfig = {
|
||||||
|
operationType: 'splitPdf',
|
||||||
|
endpoint: getSplitEndpoint,
|
||||||
|
buildFormData: buildSplitFormData,
|
||||||
|
filePrefix: 'split_',
|
||||||
|
multiFileEndpoint: true, // Single API call with all files
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useSplitOperation = () => {
|
export const useSplitOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<SplitParameters>({
|
return useToolOperation<SplitParameters>({
|
||||||
operationType: 'split',
|
...splitOperationConfig,
|
||||||
endpoint: (params) => getEndpoint(params),
|
|
||||||
buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData
|
|
||||||
filePrefix: 'split_',
|
|
||||||
multiFileEndpoint: true, // Single API call with all files
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export interface SplitParameters extends BaseParameters {
|
|||||||
|
|
||||||
export type SplitParametersHook = BaseParametersHook<SplitParameters>;
|
export type SplitParametersHook = BaseParametersHook<SplitParameters>;
|
||||||
|
|
||||||
const defaultParameters: SplitParameters = {
|
export const defaultParameters: SplitParameters = {
|
||||||
mode: '',
|
mode: '',
|
||||||
pages: '',
|
pages: '',
|
||||||
hDiv: '2',
|
hDiv: '2',
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation } from '../shared/useToolOperation';
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { UnlockPdfFormsParameters } from './useUnlockPdfFormsParameters';
|
import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters';
|
||||||
|
|
||||||
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
export const buildUnlockPdfFormsFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const unlockPdfFormsOperationConfig = {
|
||||||
|
operationType: 'unlock-pdf-forms',
|
||||||
|
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
||||||
|
buildFormData: buildUnlockPdfFormsFormData,
|
||||||
|
filePrefix: 'unlocked_forms_', // Will be overridden in hook with translation
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useUnlockPdfFormsOperation = () => {
|
export const useUnlockPdfFormsOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const buildFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useToolOperation<UnlockPdfFormsParameters>({
|
return useToolOperation<UnlockPdfFormsParameters>({
|
||||||
operationType: 'unlockPdfForms',
|
...unlockPdfFormsOperationConfig,
|
||||||
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_',
|
filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_',
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
|
import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
|
||||||
|
|
||||||
// Material UI Icons
|
// Material UI Icons
|
||||||
import CompressIcon from '@mui/icons-material/Compress';
|
import CompressIcon from '@mui/icons-material/Compress';
|
||||||
@ -44,7 +44,8 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function useSuggestedTools(): SuggestedTool[] {
|
export function useSuggestedTools(): SuggestedTool[] {
|
||||||
const { handleToolSelect, selectedToolKey } = useToolWorkflow();
|
const { actions } = useNavigationActions();
|
||||||
|
const { selectedToolKey } = useNavigationState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Filter out the current tool
|
// Filter out the current tool
|
||||||
@ -53,7 +54,7 @@ export function useSuggestedTools(): SuggestedTool[] {
|
|||||||
// Add navigation function to each tool
|
// Add navigation function to each tool
|
||||||
return filteredTools.map(tool => ({
|
return filteredTools.map(tool => ({
|
||||||
...tool,
|
...tool,
|
||||||
navigate: () => handleToolSelect(tool.id)
|
navigate: () => actions.handleToolSelect(tool.id)
|
||||||
}));
|
}));
|
||||||
}, [selectedToolKey, handleToolSelect]);
|
}, [selectedToolKey, actions]);
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,16 @@ import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
|
|||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
|
|
||||||
interface ToolManagementResult {
|
interface ToolManagementResult {
|
||||||
selectedToolKey: string | null;
|
|
||||||
selectedTool: ToolRegistryEntry | null;
|
selectedTool: ToolRegistryEntry | null;
|
||||||
toolSelectedFileIds: string[];
|
toolSelectedFileIds: string[];
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
selectTool: (toolKey: string) => void;
|
|
||||||
clearToolSelection: () => void;
|
|
||||||
setToolSelectedFileIds: (fileIds: string[]) => void;
|
setToolSelectedFileIds: (fileIds: string[]) => void;
|
||||||
|
getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useToolManagement = (): ToolManagementResult => {
|
export const useToolManagement = (): ToolManagementResult => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [selectedToolKey, setSelectedToolKey] = useState<string /* FIX ME: Should be ToolId */ | null>(null);
|
|
||||||
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Build endpoints list from registry entries with fallback to legacy mapping
|
// Build endpoints list from registry entries with fallback to legacy mapping
|
||||||
@ -56,35 +53,15 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
return availableToolRegistry;
|
return availableToolRegistry;
|
||||||
}, [isToolAvailable, t, baseRegistry]);
|
}, [isToolAvailable, t, baseRegistry]);
|
||||||
|
|
||||||
useEffect(() => {
|
const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => {
|
||||||
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
|
return toolKey ? toolRegistry[toolKey] || null : null;
|
||||||
const firstAvailableTool = Object.keys(toolRegistry)[0];
|
}, [toolRegistry]);
|
||||||
if (firstAvailableTool) {
|
|
||||||
setSelectedToolKey(firstAvailableTool);
|
|
||||||
} else {
|
|
||||||
setSelectedToolKey(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [endpointsLoading, selectedToolKey, toolRegistry]);
|
|
||||||
|
|
||||||
const selectTool = useCallback((toolKey: string) => {
|
|
||||||
setSelectedToolKey(toolKey);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearToolSelection = useCallback(() => {
|
|
||||||
setSelectedToolKey(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectedTool = selectedToolKey ? toolRegistry[selectedToolKey] : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedToolKey,
|
selectedTool: getSelectedTool(null), // This will be unused, kept for compatibility
|
||||||
selectedTool,
|
|
||||||
toolSelectedFileIds,
|
toolSelectedFileIds,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
selectTool,
|
|
||||||
clearToolSelection,
|
|
||||||
setToolSelectedFileIds,
|
setToolSelectedFileIds,
|
||||||
|
getSelectedTool,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { ModeType } from '../contexts/NavigationContext';
|
import { ModeType } from '../types/navigation';
|
||||||
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
|
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileActions, useFileSelection } from "../contexts/FileContext";
|
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
|
||||||
import { ToolWorkflowProvider, useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
|
||||||
import { Group } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext";
|
import { useSidebarContext } from "../contexts/SidebarContext";
|
||||||
import { useDocumentMeta } from "../hooks/useDocumentMeta";
|
import { useDocumentMeta } from "../hooks/useDocumentMeta";
|
||||||
import { getBaseUrl } from "../constants/app";
|
import { getBaseUrl } from "../constants/app";
|
||||||
|
|
||||||
@ -14,7 +12,7 @@ import QuickAccessBar from "../components/shared/QuickAccessBar";
|
|||||||
import FileManager from "../components/FileManager";
|
import FileManager from "../components/FileManager";
|
||||||
|
|
||||||
|
|
||||||
function HomePageContent() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
sidebarRefs,
|
sidebarRefs,
|
||||||
@ -22,8 +20,6 @@ function HomePageContent() {
|
|||||||
|
|
||||||
const { quickAccessRef } = sidebarRefs;
|
const { quickAccessRef } = sidebarRefs;
|
||||||
|
|
||||||
const { setSelectedFiles } = useFileSelection();
|
|
||||||
|
|
||||||
const { selectedTool, selectedToolKey } = useToolWorkflow();
|
const { selectedTool, selectedToolKey } = useToolWorkflow();
|
||||||
|
|
||||||
const baseUrl = getBaseUrl();
|
const baseUrl = getBaseUrl();
|
||||||
@ -54,24 +50,3 @@ function HomePageContent() {
|
|||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomePageWithProviders() {
|
|
||||||
const { actions } = useNavigationActions();
|
|
||||||
|
|
||||||
// Wrapper to convert string to ModeType
|
|
||||||
const handleViewChange = (view: string) => {
|
|
||||||
actions.setMode(view as any); // ToolWorkflowContext should validate this
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolWorkflowProvider onViewChange={handleViewChange}>
|
|
||||||
<SidebarProvider>
|
|
||||||
<HomePageContent />
|
|
||||||
</SidebarProvider>
|
|
||||||
</ToolWorkflowProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
return <HomePageWithProviders />;
|
|
||||||
}
|
|
||||||
|
183
frontend/src/services/automationStorage.ts
Normal file
183
frontend/src/services/automationStorage.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* Service for managing automation configurations in IndexedDB
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AutomationConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
operations: Array<{
|
||||||
|
operation: string;
|
||||||
|
parameters: any;
|
||||||
|
}>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutomationStorage {
|
||||||
|
private dbName = 'StirlingPDF_Automations';
|
||||||
|
private dbVersion = 1;
|
||||||
|
private storeName = 'automations';
|
||||||
|
private db: IDBDatabase | null = null;
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to open automation storage database'));
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
this.db = request.result;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
|
||||||
|
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||||
|
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||||
|
store.createIndex('name', 'name', { unique: false });
|
||||||
|
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureDB(): Promise<IDBDatabase> {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error('Database not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveAutomation(automation: Omit<AutomationConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<AutomationConfig> {
|
||||||
|
const db = await this.ensureDB();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
const automationWithMeta: AutomationConfig = {
|
||||||
|
id: `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
...automation,
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.add(automationWithMeta);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(automationWithMeta);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to save automation'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAutomation(automation: AutomationConfig): Promise<AutomationConfig> {
|
||||||
|
const db = await this.ensureDB();
|
||||||
|
|
||||||
|
const updatedAutomation: AutomationConfig = {
|
||||||
|
...automation,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.put(updatedAutomation);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(updatedAutomation);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to update automation'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAutomation(id: string): Promise<AutomationConfig | null> {
|
||||||
|
const db = await this.ensureDB();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readonly');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.get(id);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(request.result || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to get automation'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllAutomations(): Promise<AutomationConfig[]> {
|
||||||
|
const db = await this.ensureDB();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readonly');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.getAll();
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const automations = request.result || [];
|
||||||
|
// Sort by creation date, newest first
|
||||||
|
automations.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
|
resolve(automations);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to get automations'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAutomation(id: string): Promise<void> {
|
||||||
|
const db = await this.ensureDB();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.delete(id);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to delete automation'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAutomations(query: string): Promise<AutomationConfig[]> {
|
||||||
|
const automations = await this.getAllAutomations();
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
return automations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
return automations.filter(automation =>
|
||||||
|
automation.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
(automation.description && automation.description.toLowerCase().includes(lowerQuery)) ||
|
||||||
|
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const automationStorage = new AutomationStorage();
|
@ -409,7 +409,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
// Verify integration: utils validation prevents API call, hook shows error
|
// Verify integration: utils validation prevents API call, hook shows error
|
||||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||||
expect(result.current.errorMessage).toContain('errorNotSupported');
|
expect(result.current.errorMessage).toContain('Unsupported conversion format');
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
expect(result.current.downloadUrl).toBe(null);
|
expect(result.current.downloadUrl).toBe(null);
|
||||||
});
|
});
|
||||||
|
@ -9,11 +9,11 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||||
|
|
||||||
import { useAddPasswordParameters } from "../hooks/tools/addPassword/useAddPasswordParameters";
|
import { useAddPasswordParameters, defaultParameters } from "../hooks/tools/addPassword/useAddPasswordParameters";
|
||||||
import { useAddPasswordOperation } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
import { useAddPasswordOperation } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||||
import { useAddPasswordTips } from "../components/tooltips/useAddPasswordTips";
|
import { useAddPasswordTips } from "../components/tooltips/useAddPasswordTips";
|
||||||
import { useAddPasswordPermissionsTips } from "../components/tooltips/useAddPasswordPermissionsTips";
|
import { useAddPasswordPermissionsTips } from "../components/tooltips/useAddPasswordPermissionsTips";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -114,4 +114,4 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddPassword;
|
export default AddPassword as ToolComponent;
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
useWatermarkFileTips,
|
useWatermarkFileTips,
|
||||||
useWatermarkFormattingTips,
|
useWatermarkFormattingTips,
|
||||||
} from "../components/tooltips/useWatermarkTips";
|
} from "../components/tooltips/useWatermarkTips";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -208,4 +208,7 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddWatermark;
|
// Static method to get the operation hook for automation
|
||||||
|
AddWatermark.tool = () => useAddWatermarkOperation;
|
||||||
|
|
||||||
|
export default AddWatermark as ToolComponent;
|
||||||
|
168
frontend/src/tools/Automate.tsx
Normal file
168
frontend/src/tools/Automate.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
||||||
|
import AutomationSelection from "../components/tools/automate/AutomationSelection";
|
||||||
|
import AutomationCreation from "../components/tools/automate/AutomationCreation";
|
||||||
|
import AutomationRun from "../components/tools/automate/AutomationRun";
|
||||||
|
|
||||||
|
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
|
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||||
|
import { AutomationConfig, AutomationStepData, AutomationMode } from "../types/automation";
|
||||||
|
import { AUTOMATION_STEPS } from "../constants/automation";
|
||||||
|
|
||||||
|
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { selectedFiles } = useFileSelection();
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION);
|
||||||
|
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
|
|
||||||
|
const automateOperation = useAutomateOperation();
|
||||||
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
||||||
|
const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations();
|
||||||
|
|
||||||
|
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 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 '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}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case '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 '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,
|
||||||
|
placeholder: t('automate.files.placeholder', 'Select files to process with this automation')
|
||||||
|
});
|
||||||
|
|
||||||
|
const automationSteps = [
|
||||||
|
createStep(t('automate.selection.title', 'Automation Selection'), {
|
||||||
|
isVisible: true,
|
||||||
|
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
|
||||||
|
onCollapsedClick: () => setCurrentStep(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,
|
||||||
|
operation: automateOperation,
|
||||||
|
title: t('automate.reviewTitle', 'Automation Results')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Automate;
|
@ -11,7 +11,7 @@ import ChangePermissionsSettings from "../components/tools/changePermissions/Cha
|
|||||||
import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters";
|
import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters";
|
||||||
import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||||
import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips";
|
import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -95,4 +95,7 @@ const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChangePermissions;
|
// Static method to get the operation hook for automation
|
||||||
|
ChangePermissions.tool = () => useChangePermissionsOperation;
|
||||||
|
|
||||||
|
export default ChangePermissions as ToolComponent;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { use, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useFileSelection } from "../contexts/FileContext";
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
@ -10,7 +10,7 @@ import CompressSettings from "../components/tools/compress/CompressSettings";
|
|||||||
|
|
||||||
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
|
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
|
||||||
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
|
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
import { useCompressTips } from "../components/tooltips/useCompressTips";
|
import { useCompressTips } from "../components/tooltips/useCompressTips";
|
||||||
|
|
||||||
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
@ -94,4 +94,5 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Compress;
|
|
||||||
|
export default Compress as ToolComponent;
|
||||||
|
@ -10,7 +10,7 @@ import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
|||||||
|
|
||||||
import { useConvertParameters } from "../hooks/tools/convert/useConvertParameters";
|
import { useConvertParameters } from "../hooks/tools/convert/useConvertParameters";
|
||||||
import { useConvertOperation } from "../hooks/tools/convert/useConvertOperation";
|
import { useConvertOperation } from "../hooks/tools/convert/useConvertOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -133,4 +133,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Convert;
|
// Static method to get the operation hook for automation
|
||||||
|
Convert.tool = () => useConvertOperation;
|
||||||
|
|
||||||
|
export default Convert as ToolComponent;
|
||||||
|
@ -11,7 +11,7 @@ import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings";
|
|||||||
|
|
||||||
import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters";
|
import { useOCRParameters } from "../hooks/tools/ocr/useOCRParameters";
|
||||||
import { useOCROperation } from "../hooks/tools/ocr/useOCROperation";
|
import { useOCROperation } from "../hooks/tools/ocr/useOCROperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
import { useOCRTips } from "../components/tooltips/useOCRTips";
|
import { useOCRTips } from "../components/tooltips/useOCRTips";
|
||||||
|
|
||||||
const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
@ -134,4 +134,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OCR;
|
// Static method to get the operation hook for automation
|
||||||
|
OCR.tool = () => useOCROperation;
|
||||||
|
|
||||||
|
export default OCR as ToolComponent;
|
||||||
|
@ -9,7 +9,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|||||||
|
|
||||||
import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters";
|
import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters";
|
||||||
import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -77,4 +77,7 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RemoveCertificateSign;
|
// Static method to get the operation hook for automation
|
||||||
|
RemoveCertificateSign.tool = () => useRemoveCertificateSignOperation;
|
||||||
|
|
||||||
|
export default RemoveCertificateSign as ToolComponent;
|
@ -11,7 +11,7 @@ import RemovePasswordSettings from "../components/tools/removePassword/RemovePas
|
|||||||
import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters";
|
import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters";
|
||||||
import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation";
|
import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation";
|
||||||
import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips";
|
import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -94,4 +94,7 @@ const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RemovePassword;
|
// Static method to get the operation hook for automation
|
||||||
|
RemovePassword.tool = () => useRemovePasswordOperation;
|
||||||
|
|
||||||
|
export default RemovePassword as ToolComponent;
|
||||||
|
@ -9,7 +9,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|||||||
|
|
||||||
import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters";
|
import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters";
|
||||||
import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation";
|
import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -77,4 +77,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Repair;
|
// Static method to get the operation hook for automation
|
||||||
|
Repair.tool = () => useRepairOperation;
|
||||||
|
|
||||||
|
export default Repair as ToolComponent;
|
||||||
|
@ -2,20 +2,18 @@ import { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useFileSelection } from "../contexts/FileContext";
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
|
||||||
|
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||||
|
|
||||||
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
||||||
import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation";
|
import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
const { actions } = useNavigationActions();
|
|
||||||
|
|
||||||
const sanitizeParams = useSanitizeParameters();
|
const sanitizeParams = useSanitizeParameters();
|
||||||
const sanitizeOperation = useSanitizeOperation();
|
const sanitizeOperation = useSanitizeOperation();
|
||||||
@ -91,4 +89,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sanitize;
|
// Static method to get the operation hook for automation
|
||||||
|
Sanitize.tool = () => useSanitizeOperation;
|
||||||
|
|
||||||
|
export default Sanitize as ToolComponent;
|
||||||
|
@ -9,7 +9,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|||||||
|
|
||||||
import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters";
|
import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters";
|
||||||
import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -77,4 +77,7 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SingleLargePage;
|
// Static method to get the operation hook for automation
|
||||||
|
SingleLargePage.tool = () => useSingleLargePageOperation;
|
||||||
|
|
||||||
|
export default SingleLargePage as ToolComponent;
|
@ -9,7 +9,7 @@ import SplitSettings from "../components/tools/split/SplitSettings";
|
|||||||
|
|
||||||
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
||||||
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -90,4 +90,4 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Split;
|
export default Split as ToolComponent;
|
||||||
|
@ -9,7 +9,7 @@ import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|||||||
|
|
||||||
import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters";
|
import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters";
|
||||||
import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -77,4 +77,7 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnlockPdfForms;
|
// Static method to get the operation hook for automation
|
||||||
|
UnlockPdfForms.tool = () => useUnlockPdfFormsOperation;
|
||||||
|
|
||||||
|
export default UnlockPdfForms as ToolComponent;
|
70
frontend/src/types/automation.ts
Normal file
70
frontend/src/types/automation.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Types for automation functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AutomationOperation {
|
||||||
|
operation: string;
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
operations: AutomationOperation[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationTool {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
configured: boolean;
|
||||||
|
parameters?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationStepData {
|
||||||
|
step: 'selection' | 'creation' | 'run';
|
||||||
|
mode?: AutomationMode;
|
||||||
|
automation?: AutomationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionStep {
|
||||||
|
id: string;
|
||||||
|
operation: string;
|
||||||
|
name: string;
|
||||||
|
status: 'pending' | 'running' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationExecutionCallbacks {
|
||||||
|
onStepStart?: (stepIndex: number, operationName: string) => void;
|
||||||
|
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void;
|
||||||
|
onStepError?: (stepIndex: number, error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||||
|
automationConfig?: AutomationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutomationMode {
|
||||||
|
CREATE = 'create',
|
||||||
|
EDIT = 'edit',
|
||||||
|
SUGGESTED = 'suggested'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuggestedAutomation {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
operations: AutomationOperation[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
icon: any; // MUI Icon component
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the AutomateParameters interface that was previously defined inline
|
||||||
|
export interface AutomateParameters extends AutomationExecutionCallbacks {
|
||||||
|
automationConfig?: AutomationConfig;
|
||||||
|
}
|
42
frontend/src/types/navigation.ts
Normal file
42
frontend/src/types/navigation.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Shared navigation types to avoid circular dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Navigation mode types - complete list to match contexts
|
||||||
|
export type ModeType =
|
||||||
|
| 'viewer'
|
||||||
|
| 'pageEditor'
|
||||||
|
| 'fileEditor'
|
||||||
|
| 'merge'
|
||||||
|
| 'split'
|
||||||
|
| 'compress'
|
||||||
|
| 'ocr'
|
||||||
|
| 'convert'
|
||||||
|
| 'sanitize'
|
||||||
|
| 'addPassword'
|
||||||
|
| 'changePermissions'
|
||||||
|
| 'addWatermark'
|
||||||
|
| 'removePassword'
|
||||||
|
| 'single-large-page'
|
||||||
|
| 'repair'
|
||||||
|
| 'unlockPdfForms'
|
||||||
|
| 'removeCertificateSign';
|
||||||
|
|
||||||
|
// Utility functions for mode handling
|
||||||
|
export const isValidMode = (mode: string): mode is ModeType => {
|
||||||
|
const validModes: ModeType[] = [
|
||||||
|
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
|
||||||
|
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions',
|
||||||
|
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
|
||||||
|
'repair', 'unlockPdfForms', 'removeCertificateSign'
|
||||||
|
];
|
||||||
|
return validModes.includes(mode as ModeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultMode = (): ModeType => 'pageEditor';
|
||||||
|
|
||||||
|
// Route parsing result
|
||||||
|
export interface ToolRoute {
|
||||||
|
mode: ModeType;
|
||||||
|
toolKey: string | null;
|
||||||
|
}
|
21
frontend/src/types/navigationActions.ts
Normal file
21
frontend/src/types/navigationActions.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Navigation action interfaces to break circular dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ModeType } from './navigation';
|
||||||
|
|
||||||
|
export interface NavigationActions {
|
||||||
|
setMode: (mode: ModeType) => void;
|
||||||
|
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||||
|
showNavigationWarning: (show: boolean) => void;
|
||||||
|
requestNavigation: (navigationFn: () => void) => void;
|
||||||
|
confirmNavigation: () => void;
|
||||||
|
cancelNavigation: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavigationState {
|
||||||
|
currentMode: ModeType;
|
||||||
|
hasUnsavedChanges: boolean;
|
||||||
|
pendingNavigation: (() => void) | null;
|
||||||
|
showNavigationWarning: boolean;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ToolOperationHook } from '../hooks/tools/shared/useToolOperation';
|
||||||
|
|
||||||
export type MaxFiles = number; // 1=single, >1=limited, -1=unlimited
|
export type MaxFiles = number; // 1=single, >1=limited, -1=unlimited
|
||||||
export type ToolCategory = 'manipulation' | 'conversion' | 'analysis' | 'utility' | 'optimization' | 'security';
|
export type ToolCategory = 'manipulation' | 'conversion' | 'analysis' | 'utility' | 'optimization' | 'security';
|
||||||
@ -11,6 +12,29 @@ export interface BaseToolProps {
|
|||||||
onPreviewFile?: (file: File | null) => void;
|
onPreviewFile?: (file: File | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for tool components that support automation.
|
||||||
|
* Tools implementing this interface can be used in automation workflows.
|
||||||
|
*/
|
||||||
|
export interface AutomationCapableTool {
|
||||||
|
/**
|
||||||
|
* Static method that returns the operation hook for this tool.
|
||||||
|
* This enables automation to execute the tool programmatically.
|
||||||
|
*/
|
||||||
|
tool: () => () => ToolOperationHook<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static method that returns the default parameters for this tool.
|
||||||
|
* This enables automation creation to initialize tools with proper defaults.
|
||||||
|
*/
|
||||||
|
getDefaultParameters: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for tool components that can be used in automation
|
||||||
|
*/
|
||||||
|
export type ToolComponent = React.ComponentType<BaseToolProps> & AutomationCapableTool;
|
||||||
|
|
||||||
export interface ToolStepConfig {
|
export interface ToolStepConfig {
|
||||||
type: ToolStepType;
|
type: ToolStepType;
|
||||||
title: string;
|
title: string;
|
||||||
|
157
frontend/src/utils/automationExecutor.ts
Normal file
157
frontend/src/utils/automationExecutor.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||||
|
import { AutomationConfig, AutomationExecutionCallbacks } from '../types/automation';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
import { AutomationFileProcessor } from './automationFileProcessor';
|
||||||
|
import { ResourceManager } from './resourceManager';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a tool operation directly without using React hooks
|
||||||
|
*/
|
||||||
|
export const executeToolOperation = async (
|
||||||
|
operationName: string,
|
||||||
|
parameters: any,
|
||||||
|
files: File[],
|
||||||
|
toolRegistry: ToolRegistry
|
||||||
|
): Promise<File[]> => {
|
||||||
|
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
|
||||||
|
|
||||||
|
const config = toolRegistry[operationName]?.operationConfig;
|
||||||
|
if (!config) {
|
||||||
|
console.error(`❌ Tool operation not supported: ${operationName}`);
|
||||||
|
throw new Error(`Tool operation not supported: ${operationName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Using config:`, config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if tool uses custom processor (like Convert tool)
|
||||||
|
if (config.customProcessor) {
|
||||||
|
console.log(`🎯 Using custom processor for ${config.operationType}`);
|
||||||
|
const resultFiles = await config.customProcessor(parameters, files);
|
||||||
|
console.log(`✅ Custom processor returned ${resultFiles.length} files`);
|
||||||
|
return resultFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.multiFileEndpoint) {
|
||||||
|
// Multi-file processing - single API call with all files
|
||||||
|
const endpoint = typeof config.endpoint === 'function'
|
||||||
|
? config.endpoint(parameters)
|
||||||
|
: config.endpoint;
|
||||||
|
|
||||||
|
console.log(`🌐 Making multi-file request to: ${endpoint}`);
|
||||||
|
const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files);
|
||||||
|
console.log(`📤 FormData entries:`, Array.from(formData.entries()));
|
||||||
|
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
|
// Multi-file responses are typically ZIP files, but may be single files
|
||||||
|
const result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📁 Processed ${result.files.length} files from response`);
|
||||||
|
return result.files;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Single-file processing - separate API call per file
|
||||||
|
console.log(`🔄 Processing ${files.length} files individually`);
|
||||||
|
const resultFiles: File[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const endpoint = typeof config.endpoint === 'function'
|
||||||
|
? config.endpoint(parameters)
|
||||||
|
: config.endpoint;
|
||||||
|
|
||||||
|
console.log(`🌐 Making single-file request ${i+1}/${files.length} to: ${endpoint} for file: ${file.name}`);
|
||||||
|
const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file);
|
||||||
|
console.log(`📤 FormData entries:`, Array.from(formData.entries()));
|
||||||
|
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
|
// Create result file
|
||||||
|
const resultFile = ResourceManager.createResultFile(
|
||||||
|
response.data,
|
||||||
|
file.name,
|
||||||
|
AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
|
);
|
||||||
|
resultFiles.push(resultFile);
|
||||||
|
console.log(`✅ Created result file: ${resultFile.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`);
|
||||||
|
return resultFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Tool operation ${operationName} failed:`, error);
|
||||||
|
throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an entire automation sequence
|
||||||
|
*/
|
||||||
|
export const executeAutomationSequence = async (
|
||||||
|
automation: any,
|
||||||
|
initialFiles: File[],
|
||||||
|
toolRegistry: ToolRegistry,
|
||||||
|
onStepStart?: (stepIndex: number, operationName: string) => void,
|
||||||
|
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
|
||||||
|
onStepError?: (stepIndex: number, error: string) => void
|
||||||
|
): Promise<File[]> => {
|
||||||
|
console.log(`🚀 Starting automation sequence: ${automation.name || 'Unnamed'}`);
|
||||||
|
console.log(`📁 Initial files: ${initialFiles.length}`);
|
||||||
|
console.log(`🔧 Operations: ${automation.operations?.length || 0}`);
|
||||||
|
|
||||||
|
if (!automation?.operations || automation.operations.length === 0) {
|
||||||
|
throw new Error('No operations in automation');
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFiles = [...initialFiles];
|
||||||
|
|
||||||
|
for (let i = 0; i < automation.operations.length; i++) {
|
||||||
|
const operation = automation.operations[i];
|
||||||
|
|
||||||
|
console.log(`📋 Step ${i + 1}/${automation.operations.length}: ${operation.operation}`);
|
||||||
|
console.log(`📄 Input files: ${currentFiles.length}`);
|
||||||
|
console.log(`⚙️ Parameters:`, operation.parameters || {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
onStepStart?.(i, operation.operation);
|
||||||
|
|
||||||
|
const resultFiles = await executeToolOperation(
|
||||||
|
operation.operation,
|
||||||
|
operation.parameters || {},
|
||||||
|
currentFiles,
|
||||||
|
toolRegistry
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`);
|
||||||
|
currentFiles = resultFiles;
|
||||||
|
onStepComplete?.(i, resultFiles);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ Step ${i + 1} failed:`, error);
|
||||||
|
onStepError?.(i, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎉 Automation sequence completed: ${currentFiles.length} final files`);
|
||||||
|
return currentFiles;
|
||||||
|
};
|
186
frontend/src/utils/automationFileProcessor.ts
Normal file
186
frontend/src/utils/automationFileProcessor.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* File processing utilities specifically for automation workflows
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { zipFileService } from '../services/zipFileService';
|
||||||
|
import { ResourceManager } from './resourceManager';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
|
||||||
|
export interface AutomationProcessingOptions {
|
||||||
|
timeout?: number;
|
||||||
|
responseType?: 'blob' | 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationProcessingResult {
|
||||||
|
success: boolean;
|
||||||
|
files: File[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutomationFileProcessor {
|
||||||
|
/**
|
||||||
|
* Check if a blob is a ZIP file by examining its header
|
||||||
|
*/
|
||||||
|
static isZipFile(blob: Blob): boolean {
|
||||||
|
// This is a simple check - in a real implementation you might want to read the first few bytes
|
||||||
|
// For now, we'll rely on the extraction attempt and fallback
|
||||||
|
return blob.type === 'application/zip' || blob.type === 'application/x-zip-compressed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract files from a ZIP blob during automation execution, with fallback for non-ZIP files
|
||||||
|
*/
|
||||||
|
static async extractAutomationZipFiles(blob: Blob): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const zipFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESPONSE_ZIP_PREFIX,
|
||||||
|
'.zip',
|
||||||
|
'application/zip'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await zipFileService.extractPdfFiles(zipFile);
|
||||||
|
|
||||||
|
if (!result.success || result.extractedFiles.length === 0) {
|
||||||
|
// Fallback: treat as single PDF file
|
||||||
|
const fallbackFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESULT_FILE_PREFIX,
|
||||||
|
'.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [fallbackFile],
|
||||||
|
errors: [`ZIP extraction failed, treated as single file: ${result.errors?.join(', ') || 'Unknown error'}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: result.extractedFiles,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to extract automation ZIP files, falling back to single file:', error);
|
||||||
|
// Fallback: treat as single PDF file
|
||||||
|
const fallbackFile = ResourceManager.createTimestampedFile(
|
||||||
|
blob,
|
||||||
|
AUTOMATION_CONSTANTS.RESULT_FILE_PREFIX,
|
||||||
|
'.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [fallbackFile],
|
||||||
|
errors: [`ZIP extraction failed, treated as single file: ${error}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single file through an automation step
|
||||||
|
*/
|
||||||
|
static async processAutomationSingleFile(
|
||||||
|
endpoint: string,
|
||||||
|
formData: FormData,
|
||||||
|
originalFileName: string,
|
||||||
|
options: AutomationProcessingOptions = {}
|
||||||
|
): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: options.responseType || 'blob',
|
||||||
|
timeout: options.timeout || AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed - HTTP ${response.status}: ${response.statusText}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultFile = ResourceManager.createResultFile(
|
||||||
|
response.data,
|
||||||
|
originalFileName,
|
||||||
|
AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
files: [resultFile],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process multiple files through an automation step
|
||||||
|
*/
|
||||||
|
static async processAutomationMultipleFiles(
|
||||||
|
endpoint: string,
|
||||||
|
formData: FormData,
|
||||||
|
options: AutomationProcessingOptions = {}
|
||||||
|
): Promise<AutomationProcessingResult> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
responseType: options.responseType || 'blob',
|
||||||
|
timeout: options.timeout || AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed - HTTP ${response.status}: ${response.statusText}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-file responses are typically ZIP files
|
||||||
|
return await this.extractAutomationZipFiles(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
files: [],
|
||||||
|
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build form data for automation tool operations
|
||||||
|
*/
|
||||||
|
static buildAutomationFormData(
|
||||||
|
parameters: Record<string, any>,
|
||||||
|
files: File | File[],
|
||||||
|
fileFieldName: string = 'fileInput'
|
||||||
|
): FormData {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Add files
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
files.forEach(file => formData.append(fileFieldName, file));
|
||||||
|
} else {
|
||||||
|
formData.append(fileFieldName, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parameters
|
||||||
|
Object.entries(parameters).forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(item => formData.append(key, item));
|
||||||
|
} else if (value !== undefined && value !== null) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
CONVERSION_ENDPOINTS,
|
CONVERSION_ENDPOINTS,
|
||||||
ENDPOINT_NAMES,
|
ENDPOINT_NAMES,
|
||||||
EXTENSION_TO_ENDPOINT
|
EXTENSION_TO_ENDPOINT,
|
||||||
|
CONVERSION_MATRIX,
|
||||||
|
TO_FORMAT_OPTIONS
|
||||||
} from '../constants/convertConstants';
|
} from '../constants/convertConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,3 +59,32 @@ export const isImageFormat = (extension: string): boolean => {
|
|||||||
export const isWebFormat = (extension: string): boolean => {
|
export const isWebFormat = (extension: string): boolean => {
|
||||||
return ['html', 'zip'].includes(extension.toLowerCase());
|
return ['html', 'zip'].includes(extension.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets available target extensions for a given source extension
|
||||||
|
* Extracted from useConvertParameters to be reusable in automation settings
|
||||||
|
*/
|
||||||
|
export const getAvailableToExtensions = (fromExtension: string): Array<{value: string, label: string, group: string}> => {
|
||||||
|
if (!fromExtension) return [];
|
||||||
|
|
||||||
|
// Handle dynamic format identifiers (file-<extension>)
|
||||||
|
if (fromExtension.startsWith('file-')) {
|
||||||
|
// Dynamic format - use 'any' conversion options (file-to-pdf)
|
||||||
|
const supportedExtensions = CONVERSION_MATRIX['any'] || [];
|
||||||
|
return TO_FORMAT_OPTIONS.filter(option =>
|
||||||
|
supportedExtensions.includes(option.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
|
||||||
|
|
||||||
|
// If no explicit conversion exists, but file-to-pdf might be available,
|
||||||
|
// fall back to 'any' conversion (which converts unknown files to PDF via file-to-pdf)
|
||||||
|
if (supportedExtensions.length === 0 && fromExtension !== 'any') {
|
||||||
|
supportedExtensions = CONVERSION_MATRIX['any'] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return TO_FORMAT_OPTIONS.filter(option =>
|
||||||
|
supportedExtensions.includes(option.value)
|
||||||
|
);
|
||||||
|
};
|
71
frontend/src/utils/resourceManager.ts
Normal file
71
frontend/src/utils/resourceManager.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Utilities for managing file resources and blob URLs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
|
|
||||||
|
export class ResourceManager {
|
||||||
|
private static blobUrls = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blob URL and track it for cleanup
|
||||||
|
*/
|
||||||
|
static createBlobUrl(blob: Blob): string {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
this.blobUrls.add(url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a specific blob URL
|
||||||
|
*/
|
||||||
|
static revokeBlobUrl(url: string): void {
|
||||||
|
if (this.blobUrls.has(url)) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
this.blobUrls.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke all tracked blob URLs
|
||||||
|
*/
|
||||||
|
static revokeAllBlobUrls(): void {
|
||||||
|
this.blobUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
this.blobUrls.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a File with proper naming convention
|
||||||
|
*/
|
||||||
|
static createResultFile(
|
||||||
|
data: BlobPart,
|
||||||
|
originalName: string,
|
||||||
|
prefix: string = AUTOMATION_CONSTANTS.PROCESSED_FILE_PREFIX,
|
||||||
|
type: string = 'application/pdf'
|
||||||
|
): File {
|
||||||
|
return new File([data], `${prefix}${originalName}`, { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a timestamped file for responses
|
||||||
|
*/
|
||||||
|
static createTimestampedFile(
|
||||||
|
data: BlobPart,
|
||||||
|
prefix: string,
|
||||||
|
extension: string = '.pdf',
|
||||||
|
type: string = 'application/pdf'
|
||||||
|
): File {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return new File([data], `${prefix}${timestamp}${extension}`, { type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for automatic cleanup on component unmount
|
||||||
|
*/
|
||||||
|
export function useResourceCleanup(): () => void {
|
||||||
|
return useCallback(() => {
|
||||||
|
ResourceManager.revokeAllBlobUrls();
|
||||||
|
}, []);
|
||||||
|
}
|
@ -3,12 +3,7 @@
|
|||||||
* Provides clean URL routing for the V2 tool system
|
* Provides clean URL routing for the V2 tool system
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ModeType } from '../contexts/NavigationContext';
|
import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation';
|
||||||
|
|
||||||
export interface ToolRoute {
|
|
||||||
mode: ModeType;
|
|
||||||
toolKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the current URL to extract tool routing information
|
* Parse the current URL to extract tool routing information
|
||||||
@ -45,7 +40,7 @@ export function parseToolRoute(): ToolRoute {
|
|||||||
|
|
||||||
// Check for query parameter fallback (e.g., ?tool=split)
|
// Check for query parameter fallback (e.g., ?tool=split)
|
||||||
const toolParam = searchParams.get('tool');
|
const toolParam = searchParams.get('tool');
|
||||||
if (toolParam && isValidMode(toolParam)) {
|
if (toolParam && isValidModeType(toolParam)) {
|
||||||
return {
|
return {
|
||||||
mode: toolParam as ModeType,
|
mode: toolParam as ModeType,
|
||||||
toolKey: toolParam
|
toolKey: toolParam
|
||||||
@ -54,7 +49,8 @@ export function parseToolRoute(): ToolRoute {
|
|||||||
|
|
||||||
// Default to page editor for home page
|
// Default to page editor for home page
|
||||||
return {
|
return {
|
||||||
mode: 'pageEditor'
|
mode: getDefaultMode(),
|
||||||
|
toolKey: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,16 +133,7 @@ export function getToolDisplayName(toolKey: string): string {
|
|||||||
return displayNames[toolKey] || toolKey;
|
return displayNames[toolKey] || toolKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Note: isValidMode is now imported from types/navigation.ts
|
||||||
* Check if a mode is valid
|
|
||||||
*/
|
|
||||||
function isValidMode(mode: string): mode is ModeType {
|
|
||||||
const validModes: ModeType[] = [
|
|
||||||
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
|
|
||||||
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
|
|
||||||
];
|
|
||||||
return validModes.includes(mode as ModeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate shareable URL for current tool state
|
* Generate shareable URL for current tool state
|
||||||
|
Loading…
x
Reference in New Issue
Block a user