Redesgin ToolOperationConfig so types are known for single/multiple/custom tools (#4221)

# Description of Changes
Redesigns `ToolOperationConfig` so that the types of the functions are
always known depending on whether the tool runs on single files,
multiple files, or uses custom behaviour
This commit is contained in:
James Brunton 2025-08-27 14:51:52 +01:00 committed by GitHub
parent 0002043266
commit 2cac8e8edf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 195 additions and 162 deletions

View File

@ -4,9 +4,13 @@ import { useAddPasswordOperation } from './useAddPasswordOperation';
import type { AddPasswordFullParameters, AddPasswordParameters } from './useAddPasswordParameters'; import type { AddPasswordFullParameters, AddPasswordParameters } from './useAddPasswordParameters';
// Mock the useToolOperation hook // Mock the useToolOperation hook
vi.mock('../shared/useToolOperation', () => ({ vi.mock('../shared/useToolOperation', async () => {
const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc.
return {
...actual,
useToolOperation: vi.fn() useToolOperation: vi.fn()
})); };
});
// Mock the translation hook // Mock the translation hook
const mockT = vi.fn((key: string) => `translated-${key}`); const mockT = vi.fn((key: string) => `translated-${key}`);
@ -20,13 +24,13 @@ vi.mock('../../../utils/toolErrorHandler', () => ({
})); }));
// Import the mocked function // Import the mocked function
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation'; import { SingleFileToolOperationConfig, ToolOperationHook, ToolType, useToolOperation } from '../shared/useToolOperation';
describe('useAddPasswordOperation', () => { describe('useAddPasswordOperation', () => {
const mockUseToolOperation = vi.mocked(useToolOperation); const mockUseToolOperation = vi.mocked(useToolOperation);
const getToolConfig = (): ToolOperationConfig<AddPasswordFullParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<AddPasswordFullParameters>; const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as SingleFileToolOperationConfig<AddPasswordFullParameters>;
const mockToolOperationReturn: ToolOperationHook<unknown> = { const mockToolOperationReturn: ToolOperationHook<unknown> = {
files: [], files: [],
@ -91,7 +95,7 @@ describe('useAddPasswordOperation', () => {
}; };
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
const formData = buildFormData(testParameters, testFile as any /* FIX ME */); const formData = buildFormData(testParameters, testFile);
// Verify the form data contains the file // Verify the form data contains the file
expect(formData.get('fileInput')).toBe(testFile); expect(formData.get('fileInput')).toBe(testFile);
@ -112,7 +116,7 @@ describe('useAddPasswordOperation', () => {
}); });
test.each([ test.each([
{ property: 'multiFileEndpoint' as const, expectedValue: false }, { property: 'toolType' as const, expectedValue: ToolType.singleFile },
{ 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: 'translated-addPassword.filenamePrefix_' }, { property: 'filePrefix' as const, expectedValue: 'translated-addPassword.filenamePrefix_' },
{ property: 'operationType' as const, expectedValue: 'addPassword' } { property: 'operationType' as const, expectedValue: 'addPassword' }

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { AddPasswordFullParameters, defaultParameters } from './useAddPasswordParameters'; import { AddPasswordFullParameters, defaultParameters } from './useAddPasswordParameters';
import { defaultParameters as permissionsDefaults } from '../changePermissions/useChangePermissionsParameters'; import { defaultParameters as permissionsDefaults } from '../changePermissions/useChangePermissionsParameters';
@ -26,11 +26,11 @@ const fullDefaultParameters: AddPasswordFullParameters = {
// Static configuration object // Static configuration object
export const addPasswordOperationConfig = { export const addPasswordOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildAddPasswordFormData,
operationType: 'addPassword', operationType: 'addPassword',
endpoint: '/api/v1/security/add-password', endpoint: '/api/v1/security/add-password',
buildFormData: buildAddPasswordFormData,
filePrefix: 'encrypted_', // Will be overridden in hook with translation filePrefix: 'encrypted_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters: fullDefaultParameters, defaultParameters: fullDefaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { AddWatermarkParameters, defaultParameters } from './useAddWatermarkParameters'; import { AddWatermarkParameters, defaultParameters } from './useAddWatermarkParameters';
@ -35,11 +35,11 @@ export const buildAddWatermarkFormData = (parameters: AddWatermarkParameters, fi
// Static configuration object // Static configuration object
export const addWatermarkOperationConfig = { export const addWatermarkOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildAddWatermarkFormData,
operationType: 'watermark', operationType: 'watermark',
endpoint: '/api/v1/security/add-watermark', endpoint: '/api/v1/security/add-watermark',
buildFormData: buildAddWatermarkFormData,
filePrefix: 'watermarked_', // Will be overridden in hook with translation filePrefix: 'watermarked_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,4 +1,4 @@
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { executeAutomationSequence } from '../../../utils/automationExecutor'; import { executeAutomationSequence } from '../../../utils/automationExecutor';
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
@ -40,9 +40,8 @@ export function useAutomateOperation() {
}, [toolRegistry]); }, [toolRegistry]);
return useToolOperation<AutomateParameters>({ return useToolOperation<AutomateParameters>({
toolType: ToolType.custom,
operationType: 'automate', operationType: 'automate',
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
buildFormData: () => new FormData(), // Not used with customProcessor
customProcessor, customProcessor,
filePrefix: '' // No prefix needed since automation handles naming internally filePrefix: '' // No prefix needed since automation handles naming internally
}); });

View File

@ -4,9 +4,13 @@ import { useChangePermissionsOperation } from './useChangePermissionsOperation';
import type { ChangePermissionsParameters } from './useChangePermissionsParameters'; import type { ChangePermissionsParameters } from './useChangePermissionsParameters';
// Mock the useToolOperation hook // Mock the useToolOperation hook
vi.mock('../shared/useToolOperation', () => ({ vi.mock('../shared/useToolOperation', async () => {
const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc.
return {
...actual,
useToolOperation: vi.fn() useToolOperation: vi.fn()
})); };
});
// Mock the translation hook // Mock the translation hook
const mockT = vi.fn((key: string) => `translated-${key}`); const mockT = vi.fn((key: string) => `translated-${key}`);
@ -20,12 +24,12 @@ vi.mock('../../../utils/toolErrorHandler', () => ({
})); }));
// Import the mocked function // Import the mocked function
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation'; import { SingleFileToolOperationConfig, ToolOperationHook, ToolType, useToolOperation } from '../shared/useToolOperation';
describe('useChangePermissionsOperation', () => { describe('useChangePermissionsOperation', () => {
const mockUseToolOperation = vi.mocked(useToolOperation); const mockUseToolOperation = vi.mocked(useToolOperation);
const getToolConfig = (): ToolOperationConfig<ChangePermissionsParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<ChangePermissionsParameters>; const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as SingleFileToolOperationConfig<ChangePermissionsParameters>;
const mockToolOperationReturn: ToolOperationHook<unknown> = { const mockToolOperationReturn: ToolOperationHook<unknown> = {
files: [], files: [],
@ -86,7 +90,7 @@ describe('useChangePermissionsOperation', () => {
const buildFormData = callArgs.buildFormData; const buildFormData = callArgs.buildFormData;
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
const formData = buildFormData(testParameters, testFile as any /* FIX ME */); const formData = buildFormData(testParameters, testFile);
// Verify the form data contains the file // Verify the form data contains the file
expect(formData.get('fileInput')).toBe(testFile); expect(formData.get('fileInput')).toBe(testFile);
@ -106,7 +110,7 @@ describe('useChangePermissionsOperation', () => {
}); });
test.each([ test.each([
{ property: 'multiFileEndpoint' as const, expectedValue: false }, { property: 'toolType' as const, expectedValue: ToolType.singleFile },
{ 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: 'change-permissions' } { property: 'operationType' as const, expectedValue: 'change-permissions' }

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { ChangePermissionsParameters, defaultParameters } from './useChangePermissionsParameters'; import { ChangePermissionsParameters, defaultParameters } from './useChangePermissionsParameters';
@ -24,11 +24,11 @@ export const buildChangePermissionsFormData = (parameters: ChangePermissionsPara
// Static configuration object // Static configuration object
export const changePermissionsOperationConfig = { export const changePermissionsOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildChangePermissionsFormData,
operationType: 'change-permissions', operationType: 'change-permissions',
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
buildFormData: buildChangePermissionsFormData,
filePrefix: 'permissions_', filePrefix: 'permissions_',
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;
@ -39,6 +39,6 @@ export const useChangePermissionsOperation = () => {
...changePermissionsOperationConfig, ...changePermissionsOperationConfig,
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.')
) ),
}); });
}; };

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { CompressParameters, defaultParameters } from './useCompressParameters'; import { CompressParameters, defaultParameters } from './useCompressParameters';
@ -24,11 +24,11 @@ export const buildCompressFormData = (parameters: CompressParameters, file: File
// Static configuration object // Static configuration object
export const compressOperationConfig = { export const compressOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildCompressFormData,
operationType: 'compress', operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf', endpoint: '/api/v1/misc/compress-pdf',
buildFormData: buildCompressFormData,
filePrefix: 'compressed_', filePrefix: 'compressed_',
multiFileEndpoint: false, // Individual API calls per file
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ConvertParameters, defaultParameters } 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, ToolType } from '../shared/useToolOperation';
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils'; import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
// Static function that can be used by both the hook and automation executor // Static function that can be used by both the hook and automation executor
@ -129,11 +129,10 @@ export const convertProcessor = async (
// Static configuration object // Static configuration object
export const convertOperationConfig = { export const convertOperationConfig = {
toolType: ToolType.custom,
customProcessor: convertProcessor, // Can't use callback version here
operationType: 'convert', operationType: 'convert',
endpoint: '', // Not used with customProcessor but required
buildFormData: buildConvertFormData, // Not used with customProcessor but required
filePrefix: 'converted_', filePrefix: 'converted_',
customProcessor: convertProcessor,
defaultParameters, defaultParameters,
} as const; } as const;
@ -158,6 +157,6 @@ export const useConvertOperation = () => {
return error.message; return error.message;
} }
return t("convert.errorConversion", "An error occurred while converting the file."); return t("convert.errorConversion", "An error occurred while converting the file.");
} },
}); });
}; };

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OCRParameters, defaultParameters } from './useOCRParameters'; import { OCRParameters, defaultParameters } from './useOCRParameters';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { useToolResources } from '../shared/useToolResources'; import { useToolResources } from '../shared/useToolResources';
@ -94,11 +94,11 @@ export const ocrResponseHandler = async (blob: Blob, originalFiles: File[], extr
// Static configuration object (without t function dependencies) // Static configuration object (without t function dependencies)
export const ocrOperationConfig = { export const ocrOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildOCRFormData,
operationType: 'ocr', operationType: 'ocr',
endpoint: '/api/v1/misc/ocr-pdf', endpoint: '/api/v1/misc/ocr-pdf',
buildFormData: buildOCRFormData,
filePrefix: 'ocr_', filePrefix: 'ocr_',
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters'; import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters';
@ -12,11 +12,11 @@ export const buildRemoveCertificateSignFormData = (parameters: RemoveCertificate
// Static configuration object // Static configuration object
export const removeCertificateSignOperationConfig = { export const removeCertificateSignOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildRemoveCertificateSignFormData,
operationType: 'remove-certificate-sign', operationType: 'remove-certificate-sign',
endpoint: '/api/v1/security/remove-cert-sign', endpoint: '/api/v1/security/remove-cert-sign',
buildFormData: buildRemoveCertificateSignFormData,
filePrefix: 'unsigned_', // Will be overridden in hook with translation filePrefix: 'unsigned_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -3,10 +3,13 @@ import { renderHook } from '@testing-library/react';
import { useRemovePasswordOperation } from './useRemovePasswordOperation'; import { useRemovePasswordOperation } from './useRemovePasswordOperation';
import type { RemovePasswordParameters } from './useRemovePasswordParameters'; import type { RemovePasswordParameters } from './useRemovePasswordParameters';
// Mock the useToolOperation hook vi.mock('../shared/useToolOperation', async () => {
vi.mock('../shared/useToolOperation', () => ({ const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc.
return {
...actual,
useToolOperation: vi.fn() useToolOperation: vi.fn()
})); };
});
// Mock the translation hook // Mock the translation hook
const mockT = vi.fn((key: string) => `translated-${key}`); const mockT = vi.fn((key: string) => `translated-${key}`);
@ -20,12 +23,12 @@ vi.mock('../../../utils/toolErrorHandler', () => ({
})); }));
// Import the mocked function // Import the mocked function
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation'; import { SingleFileToolOperationConfig, ToolOperationHook, ToolType, useToolOperation } from '../shared/useToolOperation';
describe('useRemovePasswordOperation', () => { describe('useRemovePasswordOperation', () => {
const mockUseToolOperation = vi.mocked(useToolOperation); const mockUseToolOperation = vi.mocked(useToolOperation);
const getToolConfig = (): ToolOperationConfig<RemovePasswordParameters> => mockUseToolOperation.mock.calls[0][0] as ToolOperationConfig<RemovePasswordParameters>; const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as SingleFileToolOperationConfig<RemovePasswordParameters>;
const mockToolOperationReturn: ToolOperationHook<unknown> = { const mockToolOperationReturn: ToolOperationHook<unknown> = {
files: [], files: [],
@ -91,7 +94,7 @@ describe('useRemovePasswordOperation', () => {
}); });
test.each([ test.each([
{ property: 'multiFileEndpoint' as const, expectedValue: false }, { property: 'toolType' as const, expectedValue: ToolType.singleFile },
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/remove-password' }, { property: 'endpoint' as const, expectedValue: '/api/v1/security/remove-password' },
{ property: 'filePrefix' as const, expectedValue: 'translated-removePassword.filenamePrefix_' }, { property: 'filePrefix' as const, expectedValue: 'translated-removePassword.filenamePrefix_' },
{ property: 'operationType' as const, expectedValue: 'removePassword' } { property: 'operationType' as const, expectedValue: 'removePassword' }

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { RemovePasswordParameters, defaultParameters } from './useRemovePasswordParameters'; import { RemovePasswordParameters, defaultParameters } from './useRemovePasswordParameters';
@ -13,11 +13,11 @@ export const buildRemovePasswordFormData = (parameters: RemovePasswordParameters
// Static configuration object // Static configuration object
export const removePasswordOperationConfig = { export const removePasswordOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildRemovePasswordFormData,
operationType: 'removePassword', operationType: 'removePassword',
endpoint: '/api/v1/security/remove-password', endpoint: '/api/v1/security/remove-password',
buildFormData: buildRemovePasswordFormData,
filePrefix: 'decrypted_', // Will be overridden in hook with translation filePrefix: 'decrypted_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { RepairParameters, defaultParameters } from './useRepairParameters'; import { RepairParameters, defaultParameters } from './useRepairParameters';
@ -12,11 +12,11 @@ export const buildRepairFormData = (parameters: RepairParameters, file: File): F
// Static configuration object // Static configuration object
export const repairOperationConfig = { export const repairOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildRepairFormData,
operationType: 'repair', operationType: 'repair',
endpoint: '/api/v1/misc/repair', endpoint: '/api/v1/misc/repair',
buildFormData: buildRepairFormData,
filePrefix: 'repaired_', // Will be overridden in hook with translation filePrefix: 'repaired_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { SanitizeParameters, defaultParameters } from './useSanitizeParameters'; import { SanitizeParameters, defaultParameters } from './useSanitizeParameters';
@ -21,9 +21,10 @@ export const buildSanitizeFormData = (parameters: SanitizeParameters, file: File
// Static configuration object // Static configuration object
export const sanitizeOperationConfig = { export const sanitizeOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildSanitizeFormData,
operationType: 'sanitize', operationType: 'sanitize',
endpoint: '/api/v1/security/sanitize-pdf', endpoint: '/api/v1/security/sanitize-pdf',
buildFormData: buildSanitizeFormData,
filePrefix: 'sanitized_', // Will be overridden in hook with translation filePrefix: 'sanitized_', // Will be overridden in hook with translation
multiFileEndpoint: false, multiFileEndpoint: false,
defaultParameters, defaultParameters,

View File

@ -12,6 +12,12 @@ import { ResponseHandler } from '../../../utils/toolResponseProcessor';
// Re-export for backwards compatibility // Re-export for backwards compatibility
export type { ProcessingProgress, ResponseHandler }; export type { ProcessingProgress, ResponseHandler };
export enum ToolType {
singleFile,
multiFile,
custom,
}
/** /**
* Configuration for tool operations defining processing behavior and API integration. * Configuration for tool operations defining processing behavior and API integration.
* *
@ -20,45 +26,16 @@ export type { ProcessingProgress, ResponseHandler };
* 2. Multi-file tools: multiFileEndpoint: true, single API call with all files * 2. Multi-file tools: multiFileEndpoint: true, single API call with all files
* 3. Complex tools: customProcessor handles all processing logic * 3. Complex tools: customProcessor handles all processing logic
*/ */
export interface ToolOperationConfig<TParams = void> { interface BaseToolOperationConfig<TParams> {
/** Operation identifier for tracking and logging */ /** Operation identifier for tracking and logging */
operationType: string; operationType: string;
/**
* API endpoint for the operation. Can be static string or function for dynamic routing.
* Not used when customProcessor is provided.
*/
endpoint: string | ((params: TParams) => string);
/**
* Builds FormData for API request. Signature determines processing approach:
* - (params, file: File) => FormData: Single-file processing
* - (params, files: File[]) => FormData: Multi-file processing
* Not used when customProcessor is provided.
*/
buildFormData: ((params: TParams, file: File) => FormData) | ((params: TParams, files: File[]) => FormData); /* FIX ME */
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */ /** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string; filePrefix: string;
/**
* Whether this tool uses backends that accept MultipartFile[] arrays.
* - true: Single API call with all files (backend uses MultipartFile[])
* - false/undefined: Individual API calls per file (backend uses single MultipartFile)
* Ignored when customProcessor is provided.
*/
multiFileEndpoint?: boolean;
/** How to handle API responses (e.g., ZIP extraction, single file response) */ /** How to handle API responses (e.g., ZIP extraction, single file response) */
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
/**
* Custom processing logic that completely bypasses standard file processing.
* When provided, tool handles all API calls, response processing, and file creation.
* Use for tools with complex routing logic or non-standard processing requirements.
*/
customProcessor?: (params: TParams, files: File[]) => Promise<File[]>;
/** Extract user-friendly error messages from API errors */ /** Extract user-friendly error messages from API errors */
getErrorMessage?: (error: any) => string; getErrorMessage?: (error: any) => string;
@ -66,6 +43,49 @@ export interface ToolOperationConfig<TParams = void> {
defaultParameters?: TParams; defaultParameters?: TParams;
} }
export interface SingleFileToolOperationConfig<TParams> extends BaseToolOperationConfig<TParams> {
/** This tool processes one file at a time. */
toolType: ToolType.singleFile;
/** Builds FormData for API request. */
buildFormData: ((params: TParams, file: File) => FormData);
/** API endpoint for the operation. Can be static string or function for dynamic routing. */
endpoint: string | ((params: TParams) => string);
customProcessor?: undefined;
}
export interface MultiFileToolOperationConfig<TParams> extends BaseToolOperationConfig<TParams> {
/** This tool processes multiple files at once. */
toolType: ToolType.multiFile;
/** Builds FormData for API request. */
buildFormData: ((params: TParams, files: File[]) => FormData);
/** API endpoint for the operation. Can be static string or function for dynamic routing. */
endpoint: string | ((params: TParams) => string);
customProcessor?: undefined;
}
export interface CustomToolOperationConfig<TParams> extends BaseToolOperationConfig<TParams> {
/** This tool has custom behaviour. */
toolType: ToolType.custom;
buildFormData?: undefined;
endpoint?: undefined;
/**
* Custom processing logic that completely bypasses standard file processing.
* This tool handles all API calls, response processing, and file creation.
* Use for tools with complex routing logic or non-standard processing requirements.
*/
customProcessor: (params: TParams, files: File[]) => Promise<File[]>;
}
export type ToolOperationConfig<TParams = void> = SingleFileToolOperationConfig<TParams> | MultiFileToolOperationConfig<TParams> | CustomToolOperationConfig<TParams>;
/** /**
* Complete tool operation interface with execution capability * Complete tool operation interface with execution capability
*/ */
@ -103,7 +123,7 @@ export { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
* @param config - Tool operation configuration * @param config - Tool operation configuration
* @returns Hook interface with state and execution methods * @returns Hook interface with state and execution methods
*/ */
export const useToolOperation = <TParams = void>( export const useToolOperation = <TParams>(
config: ToolOperationConfig<TParams> config: ToolOperationConfig<TParams>
): ToolOperationHook<TParams> => { ): ToolOperationHook<TParams> => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -143,15 +163,28 @@ export const useToolOperation = <TParams = void>(
try { try {
let processedFiles: File[]; let processedFiles: File[];
if (config.customProcessor) { switch (config.toolType) {
actions.setStatus('Processing files...'); case ToolType.singleFile:
processedFiles = await config.customProcessor(params, validFiles); // Individual file processing - separate API call per file
} else { const apiCallsConfig: ApiCallsConfig<TParams> = {
// Use explicit multiFileEndpoint flag to determine processing approach endpoint: config.endpoint,
if (config.multiFileEndpoint) { buildFormData: config.buildFormData,
filePrefix: config.filePrefix,
responseHandler: config.responseHandler
};
processedFiles = await processFiles(
params,
validFiles,
apiCallsConfig,
actions.setProgress,
actions.setStatus
);
break;
case ToolType.multiFile:
// Multi-file processing - single API call with all files // Multi-file processing - single API call with all files
actions.setStatus('Processing files...'); actions.setStatus('Processing files...');
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles); const formData = config.buildFormData(params, validFiles);
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint; const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
const response = await axios.post(endpoint, formData, { responseType: 'blob' }); const response = await axios.post(endpoint, formData, { responseType: 'blob' });
@ -175,22 +208,12 @@ export const useToolOperation = <TParams = void>(
processedFiles = await extractAllZipFiles(response.data); processedFiles = await extractAllZipFiles(response.data);
} }
} }
} else { break;
// Individual file processing - separate API call per file
const apiCallsConfig: ApiCallsConfig<TParams> = { case ToolType.custom:
endpoint: config.endpoint, actions.setStatus('Processing files...');
buildFormData: config.buildFormData as (params: TParams, file: File) => FormData, processedFiles = await config.customProcessor(params, validFiles);
filePrefix: config.filePrefix, break;
responseHandler: config.responseHandler
};
processedFiles = await processFiles(
params,
validFiles,
apiCallsConfig,
actions.setProgress,
actions.setStatus
);
}
} }
if (processedFiles.length > 0) { if (processedFiles.length > 0) {

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters'; import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters';
@ -12,11 +12,11 @@ export const buildSingleLargePageFormData = (parameters: SingleLargePageParamete
// Static configuration object // Static configuration object
export const singleLargePageOperationConfig = { export const singleLargePageOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildSingleLargePageFormData,
operationType: 'single-large-page', operationType: 'single-large-page',
endpoint: '/api/v1/general/pdf-to-single-page', endpoint: '/api/v1/general/pdf-to-single-page',
buildFormData: buildSingleLargePageFormData,
filePrefix: 'single_page_', // Will be overridden in hook with translation filePrefix: 'single_page_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { SplitParameters, defaultParameters } from './useSplitParameters'; import { SplitParameters, defaultParameters } from './useSplitParameters';
import { SPLIT_MODES } from '../../../constants/splitConstants'; import { SPLIT_MODES } from '../../../constants/splitConstants';
@ -57,11 +57,11 @@ export const getSplitEndpoint = (parameters: SplitParameters): string => {
// Static configuration object // Static configuration object
export const splitOperationConfig = { export const splitOperationConfig = {
toolType: ToolType.multiFile,
buildFormData: buildSplitFormData,
operationType: 'splitPdf', operationType: 'splitPdf',
endpoint: getSplitEndpoint, endpoint: getSplitEndpoint,
buildFormData: buildSplitFormData,
filePrefix: 'split_', filePrefix: 'split_',
multiFileEndpoint: true, // Single API call with all files
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation'; import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters'; import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters';
@ -12,11 +12,11 @@ export const buildUnlockPdfFormsFormData = (parameters: UnlockPdfFormsParameters
// Static configuration object // Static configuration object
export const unlockPdfFormsOperationConfig = { export const unlockPdfFormsOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildUnlockPdfFormsFormData,
operationType: 'unlock-pdf-forms', operationType: 'unlock-pdf-forms',
endpoint: '/api/v1/misc/unlock-pdf-forms', endpoint: '/api/v1/misc/unlock-pdf-forms',
buildFormData: buildUnlockPdfFormsFormData,
filePrefix: 'unlocked_forms_', // Will be overridden in hook with translation filePrefix: 'unlocked_forms_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters, defaultParameters,
} as const; } as const;

View File

@ -4,6 +4,7 @@ import { AutomationConfig, AutomationExecutionCallbacks } from '../types/automat
import { AUTOMATION_CONSTANTS } from '../constants/automation'; import { AUTOMATION_CONSTANTS } from '../constants/automation';
import { AutomationFileProcessor } from './automationFileProcessor'; import { AutomationFileProcessor } from './automationFileProcessor';
import { ResourceManager } from './resourceManager'; import { ResourceManager } from './resourceManager';
import { ToolType } from '../hooks/tools/shared/useToolOperation';
/** /**
@ -47,7 +48,7 @@ export const executeToolOperationWithPrefix = async (
return resultFiles; return resultFiles;
} }
if (config.multiFileEndpoint) { if (config.toolType === ToolType.multiFile) {
// Multi-file processing - single API call with all files // Multi-file processing - single API call with all files
const endpoint = typeof config.endpoint === 'function' const endpoint = typeof config.endpoint === 'function'
? config.endpoint(parameters) ? config.endpoint(parameters)
@ -84,7 +85,6 @@ export const executeToolOperationWithPrefix = async (
if (result.errors.length > 0) { if (result.errors.length > 0) {
console.warn(`⚠️ File processing warnings:`, result.errors); console.warn(`⚠️ File processing warnings:`, result.errors);
} }
// Apply prefix to files, replacing any existing prefix // Apply prefix to files, replacing any existing prefix
const processedFiles = filePrefix const processedFiles = filePrefix
? result.files.map(file => { ? result.files.map(file => {