Initial working version of automation support

This commit is contained in:
James Brunton 2025-09-05 16:05:18 +01:00
parent b9b8e6e4e1
commit e929d7e349
11 changed files with 990 additions and 49 deletions

View File

@ -12,7 +12,7 @@ import {
} from '@mantine/core';
import CheckIcon from '@mui/icons-material/Check';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import ToolConfigurationModal from './ToolConfigurationModal';
import EnhancedToolConfigurationModal from './EnhancedToolConfigurationModal';
import ToolList from './ToolList';
import IconSelector from './IconSelector';
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
@ -216,7 +216,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
{/* Tool Configuration Modal */}
{currentConfigTool && (
<ToolConfigurationModal
<EnhancedToolConfigurationModal
opened={configModalOpen}
tool={currentConfigTool}
onSave={handleToolConfigSave}

View File

@ -0,0 +1,244 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
Text,
Stack,
Group,
TextInput,
Textarea,
Divider,
Badge,
Alert
} from '@mantine/core';
import CheckIcon from '@mui/icons-material/Check';
import InfoIcon from '@mui/icons-material/Info';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import EnhancedToolConfigurationModal from './EnhancedToolConfigurationModal';
import ToolList from './ToolList';
import IconSelector from './IconSelector';
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
import { useEnhancedAutomationForm } from '../../../hooks/tools/automate/useEnhancedAutomationForm';
interface AutomationCreationEnhancedProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
onBack: () => void;
onComplete: (automation: AutomationConfig) => void;
toolRegistry: ToolRegistry;
}
/**
* Enhanced automation creation component that works with both definition-based and legacy tools
*/
export default function AutomationCreationEnhanced({
mode,
existingAutomation,
onBack,
onComplete,
toolRegistry
}: AutomationCreationEnhancedProps) {
const { t } = useTranslation();
const {
automationName,
setAutomationName,
automationDescription,
setAutomationDescription,
automationIcon,
setAutomationIcon,
selectedTools,
addTool,
removeTool,
updateTool,
hasUnsavedChanges,
canSaveAutomation,
getToolName,
getAutomatableTools,
isToolAutomatable
} = useEnhancedAutomationForm({ mode, existingAutomation, toolRegistry });
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configTool, setConfigTool] = useState<AutomationTool | null>(null);
const automatableTools = getAutomatableTools();
const definitionBasedCount = automatableTools.filter(tool => tool.hasDefinition).length;
const legacyCount = automatableTools.filter(tool => tool.hasLegacySettings).length;
const handleToolConfig = (tool: AutomationTool) => {
setConfigTool(tool);
setConfigModalOpen(true);
};
const handleConfigSave = (parameters: unknown) => {
if (configTool) {
updateTool(configTool.id, {
parameters: parameters as Record<string, unknown>,
configured: true
});
}
setConfigModalOpen(false);
setConfigTool(null);
};
const handleConfigCancel = () => {
setConfigModalOpen(false);
setConfigTool(null);
};
const handleSave = () => {
const automation: AutomationConfig = {
id: existingAutomation?.id || `automation-${Date.now()}`,
name: automationName,
description: automationDescription,
icon: automationIcon,
operations: selectedTools.map(tool => ({
operation: tool.operation,
parameters: tool.parameters
})),
createdAt: existingAutomation?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString()
};
onComplete(automation);
};
return (
<>
<Stack gap="lg">
{/* Info about tool types */}
<Alert icon={<InfoIcon />} color="blue">
<Text size="sm">
{t('automate.enhanced.info',
'Enhanced automation now supports {{definitionCount}} definition-based tools and {{legacyCount}} legacy tools.',
{ definitionCount: definitionBasedCount, legacyCount: legacyCount }
)}
</Text>
</Alert>
{/* Automation Details */}
<Stack gap="md">
<Text size="lg" fw={600}>
{mode === AutomationMode.EDIT
? t('automate.edit.title', 'Edit Automation')
: t('automate.create.title', 'Create New Automation')
}
</Text>
<Group grow>
<TextInput
label={t('automate.name.label', 'Automation Name')}
placeholder={t('automate.name.placeholder', 'Enter automation name')}
value={automationName}
onChange={(e) => setAutomationName(e.target.value)}
required
/>
<IconSelector
value={automationIcon}
onChange={setAutomationIcon}
/>
</Group>
<Textarea
label={t('automate.description.label', 'Description')}
placeholder={t('automate.description.placeholder', 'Describe what this automation does')}
value={automationDescription}
onChange={(e) => setAutomationDescription(e.target.value)}
rows={3}
/>
</Stack>
<Divider />
{/* Tool Selection and Configuration */}
<Stack gap="md">
<Text size="md" fw={600}>
{t('automate.tools.title', 'Selected Tools')}
</Text>
{selectedTools.length > 0 && (
<Stack gap="sm">
{selectedTools.map((tool, index) => {
const toolEntry = toolRegistry[tool.operation as keyof ToolRegistry];
const hasDefinition = !!toolEntry?.definition;
return (
<Group key={tool.id} justify="space-between" p="sm" style={{ border: '1px solid #e0e0e0', borderRadius: '4px' }}>
<Group>
<Text fw={500}>{index + 1}.</Text>
<Text>{tool.name}</Text>
{hasDefinition && (
<Badge size="xs" color="blue">Definition-based</Badge>
)}
{!hasDefinition && toolEntry?.settingsComponent && (
<Badge size="xs" color="orange">Legacy</Badge>
)}
{tool.configured && (
<Badge size="xs" color="green">Configured</Badge>
)}
</Group>
<Group gap="xs">
<Button
size="xs"
variant="outline"
onClick={() => handleToolConfig(tool)}
>
{tool.configured
? t('automate.tool.reconfigure', 'Reconfigure')
: t('automate.tool.configure', 'Configure')
}
</Button>
<Button
size="xs"
variant="outline"
color="red"
onClick={() => removeTool(tool.id)}
>
{t('common.remove', 'Remove')}
</Button>
</Group>
</Group>
);
})}
</Stack>
)}
<ToolList
toolRegistry={toolRegistry}
onToolSelect={addTool}
selectedToolIds={selectedTools.map(t => t.operation)}
showOnlyAutomatable={true}
automatableToolsFilter={isToolAutomatable}
/>
</Stack>
{/* Action Buttons */}
<Group justify="flex-end" gap="sm">
<Button variant="outline" onClick={onBack}>
{t('common.back', 'Back')}
</Button>
<Button
leftSection={<CheckIcon />}
onClick={handleSave}
disabled={!canSaveAutomation}
>
{mode === AutomationMode.EDIT
? t('automate.update', 'Update Automation')
: t('automate.create', 'Create Automation')
}
</Button>
</Group>
</Stack>
{/* Configuration Modal */}
{configModalOpen && configTool && (
<EnhancedToolConfigurationModal
opened={configModalOpen}
tool={configTool}
onSave={handleConfigSave}
onCancel={handleConfigCancel}
toolRegistry={toolRegistry}
/>
)}
</>
);
}

View File

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Text, Stack } from '@mantine/core';
import WarningIcon from '@mui/icons-material/Warning';
import { ToolDefinition } from '../shared/toolDefinition';
interface DefinitionBasedToolConfigProps {
definition: ToolDefinition<unknown>;
parameters: Record<string, unknown>;
onParameterChange: (key: string, value: unknown) => void;
disabled?: boolean;
}
/**
* Component that renders tool settings from a ToolDefinition
* This allows automation to work with definition-based tools automatically
*/
export default function DefinitionBasedToolConfig({
definition,
parameters,
onParameterChange,
disabled = false
}: DefinitionBasedToolConfigProps) {
const { t } = useTranslation();
// Get steps from definition (handle both static and dynamic)
const stepDefinitions = typeof definition.steps === 'function'
? definition.steps(parameters, true, false) // hasFiles = true, hasResults = false (automation context)
: definition.steps;
// Show all steps that aren't explicitly hidden
const visibleSteps = stepDefinitions.filter((stepDef) => {
return stepDef.isVisible !== false; // Show unless explicitly set to false (not a function)
});
if (visibleSteps.length === 0) {
return (
<Alert icon={<WarningIcon />} color="orange">
<Text size="sm">
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
</Text>
</Alert>
);
}
return (
<Stack gap="md">
{visibleSteps.map((stepDef) => (
<div key={stepDef.key}>
<Text size="sm" fw={500} mb="xs">
{stepDef.title(t)}
</Text>
<stepDef.component
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</div>
))}
</Stack>
);
}

View File

@ -0,0 +1,176 @@
import React, { useState, useEffect } 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 { ToolDefinition } from '../shared/toolDefinition';
import { getAvailableToExtensions } from '../../../utils/convertUtils';
import DefinitionBasedToolConfig from './DefinitionBasedToolConfig';
interface EnhancedToolConfigurationModalProps {
opened: boolean;
tool: {
id: string;
operation: string;
name: string;
parameters?: unknown;
};
onSave: (parameters: unknown) => void;
onCancel: () => void;
toolRegistry: ToolRegistry;
}
export default function EnhancedToolConfigurationModal({
opened,
tool,
onSave,
onCancel,
toolRegistry
}: EnhancedToolConfigurationModalProps) {
const { t } = useTranslation();
const [legacyParameters, setLegacyParameters] = useState<Record<string, unknown>>({});
const [isValid, setIsValid] = useState(true);
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const hasDefinition = !!toolInfo?.definition;
const SettingsComponent = toolInfo?.settingsComponent;
// For definition-based tools, use the actual hook
const definitionParams = hasDefinition ? (toolInfo.definition as ToolDefinition<unknown>).useParameters() : null;
// Initialize legacy parameters
useEffect(() => {
if (!hasDefinition) {
if (tool.parameters) {
setLegacyParameters(tool.parameters as Record<string, unknown>);
} else {
setLegacyParameters({});
}
}
}, [tool.parameters, tool.operation, hasDefinition]);
// Handle parameter changes
const handleParameterChange = (key: string, value: unknown) => {
if (hasDefinition && definitionParams) {
definitionParams.updateParameter(key, value);
} else {
setLegacyParameters((prev) => ({ ...prev, [key]: value }));
}
};
const parameters = hasDefinition && definitionParams ? definitionParams.parameters : legacyParameters;
// Render the settings component
const renderToolSettings = () => {
if (hasDefinition) {
// Use definition-based rendering
const definition = toolInfo.definition as ToolDefinition<unknown>;
return (
<DefinitionBasedToolConfig
definition={definition}
parameters={parameters}
onParameterChange={handleParameterChange}
disabled={false}
/>
);
}
if (!SettingsComponent) {
return (
<Alert icon={<WarningIcon />} color="orange">
<Text size="sm">
{t('automate.config.noSettings', 'This tool does not have configurable settings.')}
</Text>
</Alert>
);
}
// Legacy settings component rendering
if (tool.operation === 'convert') {
return (
<SettingsComponent
parameters={parameters}
onParameterChange={handleParameterChange}
getAvailableToExtensions={getAvailableToExtensions}
selectedFiles={[]}
disabled={false}
/>
);
}
return (
<SettingsComponent
parameters={parameters}
onParameterChange={handleParameterChange}
disabled={false}
/>
);
};
const handleSave = () => {
if (isValid) {
const finalParameters = hasDefinition && definitionParams ? definitionParams.parameters : legacyParameters;
onSave(finalParameters);
}
};
return (
<Modal
opened={opened}
onClose={onCancel}
title={
<Group gap="xs">
<SettingsIcon />
<Title order={4}>
{t('automate.config.title', 'Configure {{toolName}}', { toolName: tool.name })}
</Title>
</Group>
}
size="lg"
scrollAreaComponent="div"
>
<Stack gap="md">
<Text size="sm" c="dimmed">
{hasDefinition
? t('automate.config.descriptionDefinition', 'Configure settings for this tool. These settings will be applied when the automation runs.')
: t('automate.config.descriptionLegacy', 'Configure settings for this tool using the legacy interface.')
}
</Text>
<div style={{ minHeight: '200px' }}>
{renderToolSettings()}
</div>
<Group justify="flex-end" gap="sm">
<Button
variant="subtle"
leftSection={<CloseIcon />}
onClick={onCancel}
>
{t('common.cancel', 'Cancel')}
</Button>
<Button
leftSection={<CheckIcon />}
onClick={handleSave}
disabled={!isValid}
>
{t('common.save', 'Save')}
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@ -4,6 +4,7 @@ import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
import { WorkbenchType } from '../types/workbench';
import { ToolId } from '../types/toolId';
import { ToolDefinition } from '../components/tools/shared/toolDefinition';
export enum SubcategoryId {
SIGNING = 'signing',
@ -41,6 +42,8 @@ export type ToolRegistryEntry = {
urlPath?: string;
// Workbench type for navigation
workbench?: WorkbenchType;
// Tool definition for definition-based tools (cast to specific type at point of use)
definition?: ToolDefinition<unknown>;
// Operation configuration for automation
operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration

View File

@ -9,37 +9,31 @@ import Sanitize from "../tools/Sanitize";
import AddPassword from "../tools/AddPassword";
import ChangePermissions from "../tools/ChangePermissions";
import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import { SubcategoryId, ToolCategoryId, ToolRegistry, ToolRegistryEntry } from "./toolsTaxonomy";
import AddWatermark from "../tools/AddWatermark";
import Repair from "../tools/Repair";
import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
// Tool definitions
import { compressDefinition } from "../tools/definitions/compressDefinition";
import { splitDefinition } from "../tools/definitions/splitDefinition";
import { addWatermarkDefinition } from "../tools/definitions/addWatermarkDefinition";
import { repairDefinition } from "../tools/definitions/repairDefinition";
import { sanitizeDefinition } from "../tools/definitions/sanitizeDefinition";
import { removePasswordDefinition } from "../tools/definitions/removePasswordDefinition";
import { unlockPdfFormsDefinition } from "../tools/definitions/unlockPdfFormsDefinition";
import { singleLargePageDefinition } from "../tools/definitions/singleLargePageDefinition";
import { removeCertificateSignDefinition } from "../tools/definitions/removeCertificateSignDefinition";
import { changePermissionsDefinition } from "../tools/definitions/changePermissionsDefinition";
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";
import { ToolId } from "../types/toolId";
import { ToolDefinition } from "../components/tools/shared/toolDefinition";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -167,13 +161,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
definition: addWatermarkDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
maxFiles: -1,
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig,
settingsComponent: AddWatermarkSingleStepSettings,
},
"add-stamp": {
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
@ -187,13 +180,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sanitize.title", "Sanitize"),
component: Sanitize,
definition: sanitizeDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
maxFiles: -1,
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig,
settingsComponent: SanitizeSettings,
},
flatten: {
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
@ -207,13 +199,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms,
definition: unlockPdfFormsDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["unlock-pdf-forms"],
operationConfig: unlockPdfFormsOperationConfig,
settingsComponent: UnlockPdfFormsSettings,
},
"manage-certificates": {
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
@ -230,13 +221,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions,
definition: changePermissionsDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig,
settingsComponent: ChangePermissionsSettings,
},
// Verification
@ -301,11 +291,10 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.split.title", "Split"),
component: SplitPdfPanel,
definition: splitDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig,
settingsComponent: SplitSettings,
},
"reorganize-pages": {
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
@ -350,13 +339,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage,
definition: singleLargePageDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["pdf-to-single-page"],
operationConfig: singleLargePageOperationConfig,
},
"add-attachments": {
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
@ -425,24 +413,23 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword,
definition: removePasswordDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
endpoints: ["remove-password"],
maxFiles: -1,
operationConfig: removePasswordOperationConfig,
settingsComponent: RemovePasswordSettings,
},
"remove-certificate-sign": {
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign,
definition: removeCertificateSignDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1,
endpoints: ["remove-certificate-sign"],
operationConfig: removeCertificateSignOperationConfig,
},
// Automation
@ -500,13 +487,12 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.repair.title", "Repair"),
component: Repair,
definition: repairDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1,
endpoints: ["repair"],
operationConfig: repairOperationConfig,
settingsComponent: RepairSettings,
},
"detect-split-scanned-photos": {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
@ -617,12 +603,11 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compress.title", "Compress"),
component: CompressPdfPanel,
definition: compressDefinition as ToolDefinition<unknown>, // Somewhat ugly hack for the sake of prototyping
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
operationConfig: compressOperationConfig,
settingsComponent: CompressSettings,
},
convert: {
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,

View File

@ -15,9 +15,11 @@ export function useAutomateOperation() {
throw new Error('No automation configuration provided');
}
// Execute the automation sequence and return the final results
console.log('🔍 Full automation config:', params.automationConfig);
// Execute the automation sequence using the regular executor
const finalResults = await executeAutomationSequence(
params.automationConfig!,
params.automationConfig,
files,
toolRegistry,
(stepIndex: number, operationName: string) => {
@ -31,7 +33,6 @@ export function useAutomateOperation() {
(stepIndex: number, error: string) => {
console.error(`Step ${stepIndex + 1} failed:`, error);
params.onStepError?.(stepIndex, error);
throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`);
}
);

View File

@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import { AutomationTool } from '../../../types/automation';
import { ToolDefinition } from '../../../components/tools/shared/toolDefinition';
export function useAutomationExecutor(toolRegistry: ToolRegistry) {
const executeStep = useCallback(async (tool: AutomationTool, files: File[]): Promise<File[]> => {
const toolEntry = toolRegistry[tool.operation as keyof ToolRegistry];
if (!toolEntry) {
throw new Error(`Tool ${tool.operation} not found in registry`);
}
// Handle definition-based tools
if (toolEntry.definition) {
const definition = toolEntry.definition as ToolDefinition<unknown>;
console.log(`🎯 Using definition-based tool: ${definition.id}`);
const operation = definition.useOperation();
const result = await operation.executeOperation(tool.parameters, files);
if (!result.success) {
throw new Error(result.error || 'Operation failed');
}
console.log(`✅ Definition-based tool returned ${result.files.length} files`);
return result.files;
}
// Handle legacy tools with operationConfig
if (toolEntry.operationConfig) {
// Import the legacy executor function and use it
const { executeToolOperationWithPrefix } = await import('../../../utils/automationExecutor');
return executeToolOperationWithPrefix(
tool.operation,
tool.parameters || {},
files,
toolRegistry
);
}
throw new Error(`Tool ${tool.operation} has no execution method available`);
}, [toolRegistry]);
const executeSequence = useCallback(async (
tools: AutomationTool[],
initialFiles: File[],
onStepStart?: (stepIndex: number, operationName: string) => void,
onStepComplete?: (stepIndex: number, resultFiles: File[]) => void,
onStepError?: (stepIndex: number, error: string) => void
): Promise<File[]> => {
let currentFiles = initialFiles;
for (let i = 0; i < tools.length; i++) {
const tool = tools[i];
try {
onStepStart?.(i, tool.operation);
console.log(`🔄 Executing step ${i + 1}/${tools.length}: ${tool.operation}`);
const resultFiles = await executeStep(tool, currentFiles);
currentFiles = resultFiles;
onStepComplete?.(i, resultFiles);
console.log(`✅ Step ${i + 1} completed with ${resultFiles.length} files`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(`❌ Step ${i + 1} failed:`, error);
onStepError?.(i, errorMessage);
throw new Error(`Step ${i + 1} failed: ${errorMessage}`);
}
}
return currentFiles;
}, [executeStep]);
return {
executeStep,
executeSequence
};
}

View File

@ -0,0 +1,159 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import { ToolDefinition } from '../../../components/tools/shared/toolDefinition';
interface UseEnhancedAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
toolRegistry: ToolRegistry;
}
/**
* Enhanced automation form hook that works with both definition-based and legacy tools
*/
export function useEnhancedAutomationForm({ mode, existingAutomation, toolRegistry }: UseEnhancedAutomationFormProps) {
const { t } = useTranslation();
const [automationName, setAutomationName] = useState('');
const [automationDescription, setAutomationDescription] = useState('');
const [automationIcon, setAutomationIcon] = useState<string>('');
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, unknown> => {
const toolEntry = toolRegistry[operation as keyof ToolRegistry];
if (!toolEntry) return {};
// Check if it's a definition-based tool
if (toolEntry.definition) {
const definition = toolEntry.definition as ToolDefinition<unknown>;
// For definition-based tools, we need to get defaults from the parameters hook
// This is tricky because we can't call hooks here, but we can provide sensible defaults
// TODO: Consider creating a static defaultParameters method on definitions
// For now, return empty object - the definition components should handle their own defaults
return {};
}
// Legacy operationConfig approach
const config = toolEntry.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}
return {};
}, [toolRegistry]);
/**
* Get list of automatable tools from the registry
* Includes both definition-based and legacy tools with settingsComponent
*/
const getAutomatableTools = useCallback(() => {
return Object.entries(toolRegistry)
.filter(([_, toolEntry]) => {
// Include definition-based tools OR legacy tools with settings
return toolEntry.definition || toolEntry.settingsComponent;
})
.map(([toolId, toolEntry]) => ({
id: toolId,
name: toolEntry.name,
hasDefinition: !!toolEntry.definition,
hasLegacySettings: !!toolEntry.settingsComponent
}));
}, [toolRegistry]);
/**
* Check if a tool supports automation
*/
const isToolAutomatable = useCallback((operation: string) => {
const toolEntry = toolRegistry[operation as keyof ToolRegistry];
return !!(toolEntry?.definition || toolEntry?.settingsComponent);
}, [toolRegistry]);
// Initialize based on mode and existing automation
useEffect(() => {
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
setAutomationName(existingAutomation.name || '');
setAutomationDescription(existingAutomation.description || '');
setAutomationIcon(existingAutomation.icon || '');
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 {
// Creating new automation
setAutomationName('');
setAutomationDescription('');
setAutomationIcon('');
setSelectedTools([]);
}
}, [mode, existingAutomation, getToolName]);
const addTool = useCallback((operation: string) => {
if (!isToolAutomatable(operation)) {
console.warn(`Tool ${operation} is not automatable`);
return;
}
const newTool: AutomationTool = {
id: `${operation}-${Date.now()}`,
operation,
name: getToolName(operation),
configured: false,
parameters: getToolDefaultParameters(operation)
};
setSelectedTools(prev => [...prev, newTool]);
}, [getToolName, getToolDefaultParameters, isToolAutomatable]);
const removeTool = useCallback((toolId: string) => {
setSelectedTools(prev => prev.filter(tool => tool.id !== toolId));
}, []);
const updateTool = useCallback((toolId: string, updates: Partial<AutomationTool>) => {
setSelectedTools(prev => prev.map(tool =>
tool.id === toolId ? { ...tool, ...updates } : tool
));
}, []);
const hasUnsavedChanges = selectedTools.length > 0 || automationName.trim() !== '';
const canSaveAutomation = automationName.trim() !== '' &&
selectedTools.length > 0 &&
selectedTools.every(tool => tool.configured);
return {
automationName,
setAutomationName,
automationDescription,
setAutomationDescription,
automationIcon,
setAutomationIcon,
selectedTools,
addTool,
removeTool,
updateTool,
hasUnsavedChanges,
canSaveAutomation,
getToolName,
getToolDefaultParameters,
getAutomatableTools,
isToolAutomatable
};
}

View File

@ -1,11 +1,65 @@
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';
import { ToolType } from '../hooks/tools/shared/useToolOperation';
/**
* Execute operation using a static config (for definition-based tools)
*/
const executeWithStaticConfig = async (
config: any,
parameters: any,
files: File[]
): Promise<File[]> => {
if (config.toolType === ToolType.singleFile) {
// Single file processing - process each file individually
const processedFiles: File[] = [];
for (const file of files) {
const endpoint = typeof config.endpoint === 'function'
? config.endpoint(parameters)
: config.endpoint;
const formData = config.buildFormData(parameters, file);
const response = await axios.post(endpoint, formData, {
responseType: 'blob',
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
});
const prefix = config.filePrefix || '';
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
const processedFile = new File([response.data], `${prefix}${fileName}`, { type: 'application/pdf' });
processedFiles.push(processedFile);
}
return processedFiles;
} else if (config.toolType === ToolType.multiFile) {
// Multi-file processing - single API call with all files
const endpoint = typeof config.endpoint === 'function'
? config.endpoint(parameters)
: config.endpoint;
const formData = config.buildFormData(parameters, files);
const response = await axios.post(endpoint, formData, {
responseType: 'blob',
timeout: AUTOMATION_CONSTANTS.OPERATION_TIMEOUT
});
// Handle multi-file response (usually ZIP)
if (response.data.type === 'application/pdf') {
const fileName = files[0]?.name || 'document.pdf';
return [new File([response.data], fileName, { type: 'application/pdf' })];
} else {
// Extract ZIP files
const { extractZipFiles } = await import('./automationFileProcessor');
return await extractZipFiles(response.data);
}
}
throw new Error(`Unsupported tool type: ${config.toolType}`);
};
/**
* Execute a tool operation directly without using React hooks
@ -31,17 +85,44 @@ export const executeToolOperationWithPrefix = async (
): Promise<File[]> => {
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
if (!config) {
console.error(`❌ Tool operation not supported: ${operationName}`);
const toolInfo = toolRegistry[operationName as keyof ToolRegistry];
const config = toolInfo?.operationConfig;
const definition = toolInfo?.definition;
if (!config && !definition) {
console.error(`❌ Tool operation not supported: ${operationName}. Missing both operationConfig and definition.`);
throw new Error(`Tool operation not supported: ${operationName}`);
}
console.log(`📋 Using config:`, config);
console.log(`📋 Using config:`, config, `definition:`, definition);
try {
// Handle definition-based tools by extracting their static execution logic
if (definition && !config) {
console.log(`🎯 Using definition-based tool: ${definition.id}`);
// Get the static operation config from the tool's operation hook
// This is exported as a static config object from each tool
const operationModule = await import(`../hooks/tools/${definition.id}/use${definition.id.charAt(0).toUpperCase() + definition.id.slice(1)}Operation`);
const staticConfig = operationModule[`${definition.id}OperationConfig`];
if (staticConfig) {
console.log(`📋 Using static config for ${definition.id}:`, staticConfig);
// Use the static config to execute the operation
if (staticConfig.customProcessor) {
const resultFiles = await staticConfig.customProcessor(parameters, files);
return resultFiles;
} else {
// Handle single/multi-file operations using the static config
return await executeWithStaticConfig(staticConfig, parameters, files);
}
} else {
throw new Error(`Definition-based tool ${definition.id} does not have a static config`);
}
}
// Check if tool uses custom processor (like Convert tool)
if (config.customProcessor) {
if (config && 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`);

View File

@ -0,0 +1,151 @@
import { ToolRegistry } from '../data/toolsTaxonomy';
import { ToolDefinition } from '../components/tools/shared/toolDefinition';
import { AutomationTool } from '../types/automation';
/**
* Enhanced automation executor that works with both definition-based and legacy tools
*/
export class EnhancedAutomationExecutor {
constructor(private toolRegistry: ToolRegistry) {}
/**
* Execute a single automation step
*/
async executeStep(tool: AutomationTool, files: File[]): Promise<File[]> {
const toolEntry = this.toolRegistry[tool.operation as keyof ToolRegistry];
if (!toolEntry) {
throw new Error(`Tool ${tool.operation} not found in registry`);
}
// Check if it's a definition-based tool
if (toolEntry.definition) {
return this.executeDefinitionBasedStep(toolEntry.definition as ToolDefinition<unknown>, tool, files);
}
// Check if it has legacy operationConfig
if (toolEntry.operationConfig) {
return this.executeLegacyStep(toolEntry.operationConfig, tool, files);
}
throw new Error(`Tool ${tool.operation} has no execution method available`);
}
/**
* Execute a step using a tool definition
*/
private async executeDefinitionBasedStep(
definition: ToolDefinition<unknown>,
tool: AutomationTool,
files: File[]
): Promise<File[]> {
// Create the operation hook instance
const operationHook = definition.useOperation();
// Execute the operation with the tool's parameters and files
await operationHook.executeOperation(tool.parameters, files);
// Return the resulting files
return operationHook.files;
}
/**
* Execute a step using legacy operation config
*/
private async executeLegacyStep(
operationConfig: any,
tool: AutomationTool,
files: File[]
): Promise<File[]> {
// This would use the existing legacy execution logic
// Implementation depends on the current operationConfig structure
console.log('Executing legacy step:', tool.operation, tool.parameters);
// Placeholder - return files unchanged for now
return files;
}
/**
* Execute a complete automation workflow
*/
async executeWorkflow(tools: AutomationTool[], initialFiles: File[]): Promise<File[]> {
let currentFiles = initialFiles;
for (const tool of tools) {
try {
currentFiles = await this.executeStep(tool, currentFiles);
console.log(`Step ${tool.operation} completed, ${currentFiles.length} files`);
} catch (error) {
console.error(`Step ${tool.operation} failed:`, error);
throw new Error(`Automation failed at step: ${tool.operation}`);
}
}
return currentFiles;
}
/**
* Get tool information for automation UI
*/
getToolInfo(operation: string) {
const toolEntry = this.toolRegistry[operation as keyof ToolRegistry];
if (!toolEntry) return null;
return {
name: toolEntry.name,
hasDefinition: !!toolEntry.definition,
hasLegacyConfig: !!toolEntry.operationConfig,
isAutomatable: !!(toolEntry.definition || toolEntry.operationConfig)
};
}
/**
* Get default parameters for a tool
*/
getDefaultParameters(operation: string): Record<string, unknown> {
const toolEntry = this.toolRegistry[operation as keyof ToolRegistry];
if (!toolEntry) return {};
if (toolEntry.definition) {
// For definition-based tools, we'd need to instantiate the parameters hook
// This is complex in a static context, so for now return empty object
return {};
}
if (toolEntry.operationConfig?.defaultParameters) {
return { ...toolEntry.operationConfig.defaultParameters };
}
return {};
}
/**
* Validate that an automation workflow is executable
*/
validateWorkflow(tools: AutomationTool[]): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
for (const tool of tools) {
const toolInfo = this.getToolInfo(tool.operation);
if (!toolInfo) {
errors.push(`Tool '${tool.operation}' not found`);
continue;
}
if (!toolInfo.isAutomatable) {
errors.push(`Tool '${tool.operation}' is not automatable`);
continue;
}
if (!tool.configured) {
errors.push(`Tool '${tool.operation}' is not configured`);
continue;
}
}
return {
isValid: errors.length === 0,
errors
};
}
}