V2 reduce boilerplate in param hooks (#4246)

# Description of Changes
Extend the base params in all tools param hooks, reducing boilerplate
code.
This commit is contained in:
James Brunton 2025-08-21 08:48:25 +01:00 committed by GitHub
parent d06cbcaa91
commit a6706fcb0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 308 additions and 478 deletions

View File

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core"; import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressOperation"; import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
interface CompressSettingsProps { interface CompressSettingsProps {
parameters: CompressParameters; parameters: CompressParameters;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Stack, Text, Checkbox } from '@mantine/core'; import { Stack, Text, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OCRParameters } from './OCRSettings'; import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
export interface AdvancedOCRParameters { export interface AdvancedOCRParameters {
advancedOptions: string[]; advancedOptions: string[];

View File

@ -2,13 +2,7 @@ import React from 'react';
import { Stack, Select, Text, Divider } from '@mantine/core'; import { Stack, Select, Text, Divider } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
export interface OCRParameters {
languages: string[];
ocrType: string;
ocrRenderType: string;
additionalOptions: string[];
}
interface OCRSettingsProps { interface OCRSettingsProps {
parameters: OCRParameters; parameters: OCRParameters;
@ -25,7 +19,7 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
return ( return (
<Stack gap="md"> <Stack gap="md">
<Select <Select
label={t('ocr.settings.ocrMode.label', 'OCR Mode')} label={t('ocr.settings.ocrMode.label', 'OCR Mode')}
value={parameters.ocrType} value={parameters.ocrType}
@ -51,4 +45,4 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
); );
}; };
export default OCRSettings; export default OCRSettings;

View File

@ -1,5 +1,3 @@
import { AddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters";
export interface AlphabetOption { export interface AlphabetOption {
value: string; value: string;
label: string; label: string;
@ -13,16 +11,3 @@ export const alphabetOptions: AlphabetOption[] = [
{ value: "chinese", label: "简体中文" }, { value: "chinese", label: "简体中文" },
{ value: "thai", label: "ไทย" }, { value: "thai", label: "ไทย" },
]; ];
export const defaultWatermarkParameters: AddWatermarkParameters = {
watermarkType: undefined,
watermarkText: '',
fontSize: 12,
rotation: 0,
opacity: 50,
widthSpacer: 50,
heightSpacer: 50,
alphabet: 'roman',
customColor: '#d3d3d3',
convertPDFToImage: false
};

View File

@ -1,7 +1,8 @@
import { useState } from 'react';
import { ChangePermissionsParameters, ChangePermissionsParametersHook, useChangePermissionsParameters } from '../changePermissions/useChangePermissionsParameters'; import { ChangePermissionsParameters, ChangePermissionsParametersHook, useChangePermissionsParameters } from '../changePermissions/useChangePermissionsParameters';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface AddPasswordParameters { export interface AddPasswordParameters extends BaseParameters {
password: string; password: string;
ownerPassword: string; ownerPassword: string;
keyLength: number; keyLength: number;
@ -11,14 +12,9 @@ export interface AddPasswordFullParameters extends AddPasswordParameters {
permissions: ChangePermissionsParameters; permissions: ChangePermissionsParameters;
} }
export interface AddPasswordParametersHook { export interface AddPasswordParametersHook extends BaseParametersHook<AddPasswordParameters> {
fullParameters: AddPasswordFullParameters; fullParameters: AddPasswordFullParameters;
parameters: AddPasswordParameters;
permissions: ChangePermissionsParametersHook; permissions: ChangePermissionsParametersHook;
updateParameter: <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
} }
export const defaultParameters: AddPasswordParameters = { export const defaultParameters: AddPasswordParameters = {
@ -28,42 +24,31 @@ export const defaultParameters: AddPasswordParameters = {
}; };
export const useAddPasswordParameters = (): AddPasswordParametersHook => { export const useAddPasswordParameters = (): AddPasswordParametersHook => {
const [parameters, setParameters] = useState<AddPasswordParameters>(defaultParameters);
const permissions = useChangePermissionsParameters(); const permissions = useChangePermissionsParameters();
const baseHook = useBaseParameters({
defaultParameters,
endpointName: 'add-password',
validateFn: () => {
// No required parameters for Add Password. Defer to permissions validation.
return permissions.validateParameters();
},
});
const fullParameters: AddPasswordFullParameters = { const fullParameters: AddPasswordFullParameters = {
...parameters, ...baseHook.parameters,
permissions: permissions.parameters, permissions: permissions.parameters,
}; };
const updateParameter = <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => { const resetParameters = () => {
setParameters(defaultParameters); baseHook.resetParameters();
permissions.resetParameters(); permissions.resetParameters();
}; };
const validateParameters = () => {
// No required parameters for Add Password. Defer to permissions validation.
return permissions.validateParameters();
};
const getEndpointName = () => {
return 'add-password';
};
return { return {
...baseHook,
fullParameters, fullParameters,
parameters,
permissions, permissions,
updateParameter,
resetParameters, resetParameters,
validateParameters,
getEndpointName,
}; };
}; };

View File

@ -1,7 +1,7 @@
import { useState, useCallback } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { defaultWatermarkParameters } from '../../../constants/addWatermarkConstants'; import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface AddWatermarkParameters { export interface AddWatermarkParameters extends BaseParameters {
watermarkType?: 'text' | 'image'; watermarkType?: 'text' | 'image';
watermarkText: string; watermarkText: string;
watermarkImage?: File; watermarkImage?: File;
@ -15,36 +15,34 @@ export interface AddWatermarkParameters {
convertPDFToImage: boolean; convertPDFToImage: boolean;
} }
export const defaultParameters: AddWatermarkParameters = {
watermarkType: undefined,
watermarkText: '',
fontSize: 12,
rotation: 0,
opacity: 50,
widthSpacer: 50,
heightSpacer: 50,
alphabet: 'roman',
customColor: '#d3d3d3',
convertPDFToImage: false
};
export const useAddWatermarkParameters = () => { export type AddWatermarkParametersHook = BaseParametersHook<AddWatermarkParameters>;
const [parameters, setParameters] = useState<AddWatermarkParameters>(defaultWatermarkParameters);
const updateParameter = useCallback(<K extends keyof AddWatermarkParameters>( export const useAddWatermarkParameters = (): AddWatermarkParametersHook => {
key: K, return useBaseParameters({
value: AddWatermarkParameters[K] defaultParameters: defaultParameters,
) => { endpointName: 'add-watermark',
setParameters(prev => ({ ...prev, [key]: value })); validateFn: (params): boolean => {
}, []); if (!params.watermarkType) {
return false;
const resetParameters = useCallback(() => { }
setParameters(defaultWatermarkParameters); if (params.watermarkType === 'text') {
}, []); return params.watermarkText.trim().length > 0;
} else {
const validateParameters = useCallback((): boolean => { return params.watermarkImage !== undefined;
if (!parameters.watermarkType) { }
return false; },
} });
if (parameters.watermarkType === 'text') { };
return parameters.watermarkText.trim().length > 0;
} else {
return parameters.watermarkImage !== undefined;
}
}, [parameters]);
return {
parameters,
updateParameter,
resetParameters,
validateParameters
};
};

View File

@ -1,6 +1,7 @@
import { useState } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ChangePermissionsParameters { export interface ChangePermissionsParameters extends BaseParameters {
preventAssembly: boolean; preventAssembly: boolean;
preventExtractContent: boolean; preventExtractContent: boolean;
preventExtractForAccessibility: boolean; preventExtractForAccessibility: boolean;
@ -11,14 +12,6 @@ export interface ChangePermissionsParameters {
preventPrintingFaithful: boolean; preventPrintingFaithful: boolean;
} }
export interface ChangePermissionsParametersHook {
parameters: ChangePermissionsParameters;
updateParameter: (parameter: keyof ChangePermissionsParameters, value: boolean) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: ChangePermissionsParameters = { export const defaultParameters: ChangePermissionsParameters = {
preventAssembly: false, preventAssembly: false,
preventExtractContent: false, preventExtractContent: false,
@ -30,35 +23,11 @@ export const defaultParameters: ChangePermissionsParameters = {
preventPrintingFaithful: false, preventPrintingFaithful: false,
}; };
export type ChangePermissionsParametersHook = BaseParametersHook<ChangePermissionsParameters>;
export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => { export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => {
const [parameters, setParameters] = useState<ChangePermissionsParameters>(defaultParameters); return useBaseParameters({
defaultParameters,
const updateParameter = <K extends keyof ChangePermissionsParameters>(parameter: K, value: ChangePermissionsParameters[K]) => { endpointName: 'add-password', // Change Permissions is a fake endpoint for the Add Password tool
setParameters(prev => ({ });
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
// Always valid - any combination of permissions is allowed
return true;
};
const getEndpointName = () => {
return 'add-password'; // Change Permissions is a fake endpoint for the Add Password tool
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}; };

View File

@ -1,15 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { CompressParameters } from './useCompressParameters';
export interface CompressParameters {
compressionLevel: number;
grayscale: boolean;
expectedSize: string;
compressionMethod: 'quality' | 'filesize';
fileSizeValue: string;
fileSizeUnit: 'KB' | 'MB';
}
const buildFormData = (parameters: CompressParameters, file: File): FormData => { const buildFormData = (parameters: CompressParameters, file: File): FormData => {
const formData = new FormData(); const formData = new FormData();

View File

@ -1,15 +1,16 @@
import { useState } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { CompressParameters } from './useCompressOperation'; import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface CompressParametersHook { export interface CompressParameters extends BaseParameters {
parameters: CompressParameters; compressionLevel: number;
updateParameter: (parameter: keyof CompressParameters, value: string | boolean | number) => void; grayscale: boolean;
resetParameters: () => void; expectedSize: string;
validateParameters: () => boolean; compressionMethod: 'quality' | 'filesize';
getEndpointName: () => string; fileSizeValue: string;
fileSizeUnit: 'KB' | 'MB';
} }
const initialParameters: CompressParameters = { const defaultParameters: CompressParameters = {
compressionLevel: 5, compressionLevel: 5,
grayscale: false, grayscale: false,
expectedSize: '', expectedSize: '',
@ -18,32 +19,15 @@ const initialParameters: CompressParameters = {
fileSizeUnit: 'MB', fileSizeUnit: 'MB',
}; };
export type CompressParametersHook = BaseParametersHook<CompressParameters>;
export const useCompressParameters = (): CompressParametersHook => { export const useCompressParameters = (): CompressParametersHook => {
const [parameters, setParameters] = useState<CompressParameters>(initialParameters); return useBaseParameters({
defaultParameters,
const updateParameter = (parameter: keyof CompressParameters, value: string | boolean | number) => { endpointName: 'compress-pdf',
setParameters(prev => ({ ...prev, [parameter]: value })); validateFn: (params) => {
}; // For compression, we only need to validate that compression level is within range
return params.compressionLevel >= 1 && params.compressionLevel <= 9;
const resetParameters = () => { },
setParameters(initialParameters); });
}; };
const validateParameters = () => {
// For compression, we only need to validate that compression level is within range
// and that at least one file is selected (at least, I think that's all we need to do here)
return parameters.compressionLevel >= 1 && parameters.compressionLevel <= 9;
};
const getEndpointName = () => {
return 'compress-pdf';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
};

View File

@ -5,14 +5,15 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from './useConvertParameters'; import { useConvertParameters } from './useConvertParameters';
import { FIT_OPTIONS } from '../../../constants/convertConstants';
describe('useConvertParameters', () => { describe('useConvertParameters', () => {
describe('Parameter Management', () => { describe('Parameter Management', () => {
test('should initialize with default parameters', () => { test('should initialize with default parameters', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
expect(result.current.parameters.fromExtension).toBe(''); expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe(''); expect(result.current.parameters.toExtension).toBe('');
expect(result.current.parameters.imageOptions.colorType).toBe('color'); expect(result.current.parameters.imageOptions.colorType).toBe('color');
@ -28,46 +29,52 @@ describe('useConvertParameters', () => {
test('should update individual parameters', () => { test('should update individual parameters', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
}); });
expect(result.current.parameters.fromExtension).toBe('pdf'); expect(result.current.parameters.fromExtension).toBe('pdf');
expect(result.current.parameters.toExtension).toBe(''); // Should not affect other params expect(result.current.parameters.toExtension).toBe(''); // Should not affect other params
}); });
test('should update nested image options', () => { test('should update nested image options', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('imageOptions', { result.current.updateParameter('imageOptions', {
colorType: 'grayscale', colorType: 'grayscale',
dpi: 150, dpi: 150,
singleOrMultiple: 'single' singleOrMultiple: 'single',
fitOption: FIT_OPTIONS.FILL_PAGE,
autoRotate: false,
combineImages: false,
}); });
}); });
expect(result.current.parameters.imageOptions.colorType).toBe('grayscale'); expect(result.current.parameters.imageOptions.colorType).toBe('grayscale');
expect(result.current.parameters.imageOptions.dpi).toBe(150); expect(result.current.parameters.imageOptions.dpi).toBe(150);
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single'); expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single');
expect(result.current.parameters.imageOptions.fitOption).toBe(FIT_OPTIONS.FILL_PAGE);
expect(result.current.parameters.imageOptions.autoRotate).toBe(false);
expect(result.current.parameters.imageOptions.combineImages).toBe(false);
}); });
test('should update nested HTML options', () => { test('should update nested HTML options', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('htmlOptions', { result.current.updateParameter('htmlOptions', {
zoomLevel: 1.5 zoomLevel: 1.5
}); });
}); });
expect(result.current.parameters.htmlOptions.zoomLevel).toBe(1.5); expect(result.current.parameters.htmlOptions.zoomLevel).toBe(1.5);
}); });
test('should update nested email options', () => { test('should update nested email options', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('emailOptions', { result.current.updateParameter('emailOptions', {
includeAttachments: false, includeAttachments: false,
@ -76,7 +83,7 @@ describe('useConvertParameters', () => {
includeAllRecipients: true includeAllRecipients: true
}); });
}); });
expect(result.current.parameters.emailOptions.includeAttachments).toBe(false); expect(result.current.parameters.emailOptions.includeAttachments).toBe(false);
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(20); expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(20);
expect(result.current.parameters.emailOptions.downloadHtml).toBe(true); expect(result.current.parameters.emailOptions.downloadHtml).toBe(true);
@ -85,49 +92,49 @@ describe('useConvertParameters', () => {
test('should update nested PDF/A options', () => { test('should update nested PDF/A options', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('pdfaOptions', { result.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa' outputFormat: 'pdfa'
}); });
}); });
expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa'); expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa');
}); });
test('should reset parameters to defaults', () => { test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png'); result.current.updateParameter('toExtension', 'png');
}); });
expect(result.current.parameters.fromExtension).toBe('pdf'); expect(result.current.parameters.fromExtension).toBe('pdf');
act(() => { act(() => {
result.current.resetParameters(); result.current.resetParameters();
}); });
expect(result.current.parameters.fromExtension).toBe(''); expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe(''); expect(result.current.parameters.toExtension).toBe('');
}); });
}); });
describe('Parameter Validation', () => { describe('Parameter Validation', () => {
test('should validate parameters correctly', () => { test('should validate parameters correctly', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
// No parameters - should be invalid // No parameters - should be invalid
expect(result.current.validateParameters()).toBe(false); expect(result.current.validateParameters()).toBe(false);
// Only fromExtension - should be invalid // Only fromExtension - should be invalid
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
}); });
expect(result.current.validateParameters()).toBe(false); expect(result.current.validateParameters()).toBe(false);
// Both extensions with supported conversion - should be valid // Both extensions with supported conversion - should be valid
act(() => { act(() => {
result.current.updateParameter('toExtension', 'png'); result.current.updateParameter('toExtension', 'png');
@ -137,63 +144,63 @@ describe('useConvertParameters', () => {
test('should validate unsupported conversions', () => { test('should validate unsupported conversions', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'unsupported'); result.current.updateParameter('toExtension', 'unsupported');
}); });
expect(result.current.validateParameters()).toBe(false); expect(result.current.validateParameters()).toBe(false);
}); });
}); });
describe('Endpoint Generation', () => { describe('Endpoint Generation', () => {
test('should generate correct endpoint names', () => { test('should generate correct endpoint names', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png'); result.current.updateParameter('toExtension', 'png');
}); });
const endpointName = result.current.getEndpointName(); const endpointName = result.current.getEndpointName();
expect(endpointName).toBe('pdf-to-img'); expect(endpointName).toBe('pdf-to-img');
}); });
test('should generate correct endpoint URLs', () => { test('should generate correct endpoint URLs', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'pdf'); result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png'); result.current.updateParameter('toExtension', 'png');
}); });
const endpoint = result.current.getEndpoint(); const endpoint = result.current.getEndpoint();
expect(endpoint).toBe('/api/v1/convert/pdf/img'); expect(endpoint).toBe('/api/v1/convert/pdf/img');
}); });
test('should return empty strings for invalid conversions', () => { test('should return empty strings for invalid conversions', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
act(() => { act(() => {
result.current.updateParameter('fromExtension', 'invalid'); result.current.updateParameter('fromExtension', 'invalid');
result.current.updateParameter('toExtension', 'invalid'); result.current.updateParameter('toExtension', 'invalid');
}); });
expect(result.current.getEndpointName()).toBe(''); expect(result.current.getEndpointName()).toBe('');
expect(result.current.getEndpoint()).toBe(''); expect(result.current.getEndpoint()).toBe('');
}); });
}); });
describe('Available Extensions', () => { describe('Available Extensions', () => {
test('should return available extensions for valid source format', () => { test('should return available extensions for valid source format', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('pdf'); const availableExtensions = result.current.getAvailableToExtensions('pdf');
expect(availableExtensions.length).toBeGreaterThan(0); expect(availableExtensions.length).toBeGreaterThan(0);
expect(availableExtensions.some(ext => ext.value === 'png')).toBe(true); expect(availableExtensions.some(ext => ext.value === 'png')).toBe(true);
expect(availableExtensions.some(ext => ext.value === 'jpg')).toBe(true); expect(availableExtensions.some(ext => ext.value === 'jpg')).toBe(true);
@ -201,9 +208,9 @@ describe('useConvertParameters', () => {
test('should return empty array for invalid source format', () => { test('should return empty array for invalid source format', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('invalid'); const availableExtensions = result.current.getAvailableToExtensions('invalid');
expect(availableExtensions).toEqual([{ expect(availableExtensions).toEqual([{
"group": "Document", "group": "Document",
"label": "PDF", "label": "PDF",
@ -213,11 +220,11 @@ describe('useConvertParameters', () => {
test('should return empty array for empty source format', () => { test('should return empty array for empty source format', () => {
const { result } = renderHook(() => useConvertParameters()); const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions(''); const availableExtensions = result.current.getAvailableToExtensions('');
expect(availableExtensions).toEqual([]); expect(availableExtensions).toEqual([]);
}); });
}); });
}); });

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'; import {
import { COLOR_TYPES,
COLOR_TYPES,
OUTPUT_OPTIONS, OUTPUT_OPTIONS,
FIT_OPTIONS, FIT_OPTIONS,
TO_FORMAT_OPTIONS, TO_FORMAT_OPTIONS,
@ -11,8 +10,10 @@ import {
} from '../../../constants/convertConstants'; } from '../../../constants/convertConstants';
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils'; import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils'; import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ConvertParameters { export interface ConvertParameters extends BaseParameters {
fromExtension: string; fromExtension: string;
toExtension: string; toExtension: string;
imageOptions: { imageOptions: {
@ -39,18 +40,13 @@ export interface ConvertParameters {
smartDetectionType: 'mixed' | 'images' | 'web' | 'none'; smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
} }
export interface ConvertParametersHook { export interface ConvertParametersHook extends BaseParametersHook<ConvertParameters> {
parameters: ConvertParameters;
updateParameter: (parameter: keyof ConvertParameters, value: any) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
getEndpoint: () => string; getEndpoint: () => string;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
analyzeFileTypes: (files: Array<{name: string}>) => void; analyzeFileTypes: (files: Array<{name: string}>) => void;
} }
const initialParameters: ConvertParameters = { const defaultParameters: ConvertParameters = {
fromExtension: '', fromExtension: '',
toExtension: '', toExtension: '',
imageOptions: { imageOptions: {
@ -77,67 +73,63 @@ const initialParameters: ConvertParameters = {
smartDetectionType: 'none', smartDetectionType: 'none',
}; };
export const useConvertParameters = (): ConvertParametersHook => { const validateParameters = (params: ConvertParameters): boolean => {
const [parameters, setParameters] = useState<ConvertParameters>(initialParameters); const { fromExtension, toExtension } = params;
const updateParameter = (parameter: keyof ConvertParameters, value: any) => { if (!fromExtension || !toExtension) return false;
setParameters(prev => ({ ...prev, [parameter]: value }));
};
const resetParameters = () => { // Handle dynamic format identifiers (file-<extension>)
setParameters(initialParameters); let supportedToExtensions: string[] = [];
}; if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options
supportedToExtensions = CONVERSION_MATRIX['any'] || [];
} else {
// Regular format - check conversion matrix
supportedToExtensions = CONVERSION_MATRIX[fromExtension] || [];
}
const validateParameters = () => { if (!supportedToExtensions.includes(toExtension)) {
const { fromExtension, toExtension } = parameters; return false;
}
if (!fromExtension || !toExtension) return false;
// Handle dynamic format identifiers (file-<extension>)
let supportedToExtensions: string[] = [];
if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options
supportedToExtensions = CONVERSION_MATRIX['any'] || [];
} else {
// Regular format - check conversion matrix
supportedToExtensions = CONVERSION_MATRIX[fromExtension] || [];
}
if (!supportedToExtensions.includes(toExtension)) {
return false;
}
return true;
};
const getEndpointName = () => { return true;
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters; };
if (isSmartDetection) { const getEndpointName = (params: ConvertParameters): string => {
if (smartDetectionType === 'mixed') { const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = params;
// Mixed file types -> PDF using file-to-pdf endpoint
return 'file-to-pdf'; if (isSmartDetection) {
} else if (smartDetectionType === 'images') { if (smartDetectionType === 'mixed') {
// All images -> PDF using img-to-pdf endpoint // Mixed file types -> PDF using file-to-pdf endpoint
return 'img-to-pdf';
} else if (smartDetectionType === 'web') {
// All web files -> PDF using html-to-pdf endpoint
return 'html-to-pdf';
}
}
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return 'file-to-pdf'; return 'file-to-pdf';
} else if (smartDetectionType === 'images') {
// All images -> PDF using img-to-pdf endpoint
return 'img-to-pdf';
} else if (smartDetectionType === 'web') {
// All web files -> PDF using html-to-pdf endpoint
return 'html-to-pdf';
} }
}
return getEndpointNameUtil(fromExtension, toExtension);
}; // Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return 'file-to-pdf';
}
return getEndpointNameUtil(fromExtension, toExtension);
};
export const useConvertParameters = (): ConvertParametersHook => {
const baseHook = useBaseParameters({
defaultParameters,
endpointName: getEndpointName,
validateFn: validateParameters,
});
const getEndpoint = () => { const getEndpoint = () => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters; const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = baseHook.parameters;
if (isSmartDetection) { if (isSmartDetection) {
if (smartDetectionType === 'mixed') { if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint // Mixed file types -> PDF using file-to-pdf endpoint
@ -150,37 +142,37 @@ export const useConvertParameters = (): ConvertParametersHook => {
return '/api/v1/convert/html/pdf'; return '/api/v1/convert/html/pdf';
} }
} }
// Handle dynamic format identifiers (file-<extension>) // Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) { if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint // Dynamic format - use file-to-pdf endpoint
return '/api/v1/convert/file/pdf'; return '/api/v1/convert/file/pdf';
} }
return getEndpointUrl(fromExtension, toExtension); return getEndpointUrl(fromExtension, toExtension);
}; };
const getAvailableToExtensions = (fromExtension: string) => { const getAvailableToExtensions = (fromExtension: string) => {
if (!fromExtension) return []; if (!fromExtension) return [];
// Handle dynamic format identifiers (file-<extension>) // Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) { if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options (file-to-pdf) // Dynamic format - use 'any' conversion options (file-to-pdf)
const supportedExtensions = CONVERSION_MATRIX['any'] || []; const supportedExtensions = CONVERSION_MATRIX['any'] || [];
return TO_FORMAT_OPTIONS.filter(option => return TO_FORMAT_OPTIONS.filter(option =>
supportedExtensions.includes(option.value) supportedExtensions.includes(option.value)
); );
} }
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || []; let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
// If no explicit conversion exists, but file-to-pdf might be available, // 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) // fall back to 'any' conversion (which converts unknown files to PDF via file-to-pdf)
if (supportedExtensions.length === 0 && fromExtension !== 'any') { if (supportedExtensions.length === 0 && fromExtension !== 'any') {
supportedExtensions = CONVERSION_MATRIX['any'] || []; supportedExtensions = CONVERSION_MATRIX['any'] || [];
} }
return TO_FORMAT_OPTIONS.filter(option => return TO_FORMAT_OPTIONS.filter(option =>
supportedExtensions.includes(option.value) supportedExtensions.includes(option.value)
); );
}; };
@ -189,7 +181,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
const analyzeFileTypes = (files: Array<{name: string}>) => { const analyzeFileTypes = (files: Array<{name: string}>) => {
if (files.length === 0) { if (files.length === 0) {
// No files - only reset smart detection, keep user's format choices // No files - only reset smart detection, keep user's format choices
setParameters(prev => ({ baseHook.setParameters(prev => ({
...prev, ...prev,
isSmartDetection: false, isSmartDetection: false,
smartDetectionType: 'none' smartDetectionType: 'none'
@ -197,13 +189,13 @@ export const useConvertParameters = (): ConvertParametersHook => {
})); }));
return; return;
} }
if (files.length === 1) { if (files.length === 1) {
// Single file - use regular detection with smart target selection // Single file - use regular detection with smart target selection
const detectedExt = detectFileExtensionUtil(files[0].name); const detectedExt = detectFileExtensionUtil(files[0].name);
let fromExt = detectedExt; let fromExt = detectedExt;
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : []; let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
// If no explicit conversion exists for this file type, create a dynamic format entry // If no explicit conversion exists for this file type, create a dynamic format entry
// and fall back to 'any' conversion logic for the actual endpoint // and fall back to 'any' conversion logic for the actual endpoint
if (availableTargets.length === 0 && detectedExt) { if (availableTargets.length === 0 && detectedExt) {
@ -214,21 +206,21 @@ export const useConvertParameters = (): ConvertParametersHook => {
fromExt = 'any'; fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || []; availableTargets = CONVERSION_MATRIX['any'] || [];
} }
setParameters(prev => { baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension // Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension; const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt); const isCurrentToExtValid = availableTargets.includes(currentToExt);
// Auto-select target only if: // Auto-select target only if:
// 1. No current target is set, OR // 1. No current target is set, OR
// 2. Current target is invalid for new source type, OR // 2. Current target is invalid for new source type, OR
// 3. There's only one possible target (forced conversion) // 3. There's only one possible target (forced conversion)
let newToExtension = currentToExt; let newToExtension = currentToExt;
if (!currentToExt || !isCurrentToExtValid) { if (!currentToExt || !isCurrentToExtValid) {
newToExtension = availableTargets.length === 1 ? availableTargets[0] : ''; newToExtension = availableTargets.length === 1 ? availableTargets[0] : '';
} }
return { return {
...prev, ...prev,
isSmartDetection: false, isSmartDetection: false,
@ -249,27 +241,27 @@ export const useConvertParameters = (): ConvertParametersHook => {
const detectedExt = uniqueExtensions[0]; const detectedExt = uniqueExtensions[0];
let fromExt = detectedExt; let fromExt = detectedExt;
let availableTargets = CONVERSION_MATRIX[detectedExt] || []; let availableTargets = CONVERSION_MATRIX[detectedExt] || [];
// If no explicit conversion exists for this file type, fall back to 'any' // If no explicit conversion exists for this file type, fall back to 'any'
if (availableTargets.length === 0) { if (availableTargets.length === 0) {
fromExt = 'any'; fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || []; availableTargets = CONVERSION_MATRIX['any'] || [];
} }
setParameters(prev => { baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension // Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension; const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt); const isCurrentToExtValid = availableTargets.includes(currentToExt);
// Auto-select target only if: // Auto-select target only if:
// 1. No current target is set, OR // 1. No current target is set, OR
// 2. Current target is invalid for new source type, OR // 2. Current target is invalid for new source type, OR
// 3. There's only one possible target (forced conversion) // 3. There's only one possible target (forced conversion)
let newToExtension = currentToExt; let newToExtension = currentToExt;
if (!currentToExt || !isCurrentToExtValid) { if (!currentToExt || !isCurrentToExtValid) {
newToExtension = availableTargets.length === 1 ? availableTargets[0] : ''; newToExtension = availableTargets.length === 1 ? availableTargets[0] : '';
} }
return { return {
...prev, ...prev,
isSmartDetection: false, isSmartDetection: false,
@ -282,10 +274,10 @@ export const useConvertParameters = (): ConvertParametersHook => {
// Mixed file types // Mixed file types
const allImages = uniqueExtensions.every(ext => isImageFormat(ext)); const allImages = uniqueExtensions.every(ext => isImageFormat(ext));
const allWeb = uniqueExtensions.every(ext => isWebFormat(ext)); const allWeb = uniqueExtensions.every(ext => isWebFormat(ext));
if (allImages) { if (allImages) {
// All files are images - use image-to-pdf conversion // All files are images - use image-to-pdf conversion
setParameters(prev => ({ baseHook.setParameters(prev => ({
...prev, ...prev,
isSmartDetection: true, isSmartDetection: true,
smartDetectionType: 'images', smartDetectionType: 'images',
@ -294,7 +286,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
})); }));
} else if (allWeb) { } else if (allWeb) {
// All files are web files - use html-to-pdf conversion // All files are web files - use html-to-pdf conversion
setParameters(prev => ({ baseHook.setParameters(prev => ({
...prev, ...prev,
isSmartDetection: true, isSmartDetection: true,
smartDetectionType: 'web', smartDetectionType: 'web',
@ -302,8 +294,8 @@ export const useConvertParameters = (): ConvertParametersHook => {
toExtension: 'pdf' toExtension: 'pdf'
})); }));
} else { } else {
// Mixed non-image types - use file-to-pdf conversion // Mixed non-image types - use file-to-pdf conversion
setParameters(prev => ({ baseHook.setParameters(prev => ({
...prev, ...prev,
isSmartDetection: true, isSmartDetection: true,
smartDetectionType: 'mixed', smartDetectionType: 'mixed',
@ -315,13 +307,9 @@ export const useConvertParameters = (): ConvertParametersHook => {
}; };
return { return {
parameters, ...baseHook,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
getEndpoint, getEndpoint,
getAvailableToExtensions, getAvailableToExtensions,
analyzeFileTypes, analyzeFileTypes,
}; };
}; };

View File

@ -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 '../../../components/tools/ocr/OCRSettings'; import { OCRParameters } 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';

View File

@ -1,13 +1,15 @@
import { useState } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings'; import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface OCRParametersHook { export interface OCRParameters extends BaseParameters {
parameters: OCRParameters; languages: string[];
updateParameter: (key: keyof OCRParameters, value: any) => void; ocrType: string;
resetParameters: () => void; ocrRenderType: string;
validateParameters: () => boolean; additionalOptions: string[];
} }
export type OCRParametersHook = BaseParametersHook<OCRParameters>;
const defaultParameters: OCRParameters = { const defaultParameters: OCRParameters = {
languages: [], languages: [],
ocrType: 'skip-text', ocrType: 'skip-text',
@ -16,28 +18,12 @@ const defaultParameters: OCRParameters = {
}; };
export const useOCRParameters = (): OCRParametersHook => { export const useOCRParameters = (): OCRParametersHook => {
const [parameters, setParameters] = useState<OCRParameters>(defaultParameters); return useBaseParameters({
defaultParameters,
const updateParameter = (key: keyof OCRParameters, value: any) => { endpointName: 'ocr-pdf',
setParameters(prev => ({ validateFn: (params) => {
...prev, // At minimum, we need at least one language selected
[key]: value return params.languages.length > 0;
})); },
}; });
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
// At minimum, we need at least one language selected
return parameters.languages.length > 0;
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
};
};

View File

@ -1,49 +1,22 @@
import { useState } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface RemovePasswordParameters { export interface RemovePasswordParameters extends BaseParameters {
password: string; password: string;
} }
export interface RemovePasswordParametersHook { export type RemovePasswordParametersHook = BaseParametersHook<RemovePasswordParameters>;
parameters: RemovePasswordParameters;
updateParameter: <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: RemovePasswordParameters = { export const defaultParameters: RemovePasswordParameters = {
password: '', password: '',
}; };
export const useRemovePasswordParameters = (): RemovePasswordParametersHook => { export const useRemovePasswordParameters = (): RemovePasswordParametersHook => {
const [parameters, setParameters] = useState<RemovePasswordParameters>(defaultParameters); return useBaseParameters({
defaultParameters,
const updateParameter = <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => { endpointName: 'remove-password',
setParameters(prev => ({ validateFn: (params) => {
...prev, return params.password !== '';
[parameter]: value, },
}) });
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
return parameters.password !== '';
};
const getEndpointName = () => {
return 'remove-password';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}; };

View File

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react'; import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface SanitizeParameters { export interface SanitizeParameters extends BaseParameters {
removeJavaScript: boolean; removeJavaScript: boolean;
removeEmbeddedFiles: boolean; removeEmbeddedFiles: boolean;
removeXMPMetadata: boolean; removeXMPMetadata: boolean;
@ -18,36 +19,14 @@ export const defaultParameters: SanitizeParameters = {
removeFonts: false, removeFonts: false,
}; };
export const useSanitizeParameters = () => { export type SanitizeParametersHook = BaseParametersHook<SanitizeParameters>;
const [parameters, setParameters] = useState<SanitizeParameters>(defaultParameters);
const updateParameter = useCallback(<K extends keyof SanitizeParameters>( export const useSanitizeParameters = (): SanitizeParametersHook => {
key: K, return useBaseParameters({
value: SanitizeParameters[K] defaultParameters,
) => { endpointName: 'sanitize-pdf',
setParameters(prev => ({ validateFn: (params) => {
...prev, return Object.values(params).some(value => value === true);
[key]: value },
})); });
}, []);
const resetParameters = useCallback(() => {
setParameters(defaultParameters);
}, []);
const validateParameters = useCallback(() => {
return Object.values(parameters).some(value => value === true);
}, [parameters]);
const getEndpointName = () => {
return 'sanitize-pdf'
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}; };

View File

@ -1,7 +1,8 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface BaseParametersHook<T> { export interface BaseParametersHook<T> {
parameters: T; parameters: T;
setParameters: Dispatch<SetStateAction<T>>;
updateParameter: <K extends keyof T>(parameter: K, value: T[K]) => void; updateParameter: <K extends keyof T>(parameter: K, value: T[K]) => void;
resetParameters: () => void; resetParameters: () => void;
validateParameters: () => boolean; validateParameters: () => boolean;
@ -10,7 +11,7 @@ export interface BaseParametersHook<T> {
export interface BaseParametersConfig<T> { export interface BaseParametersConfig<T> {
defaultParameters: T; defaultParameters: T;
endpointName: string; endpointName: string | ((params: T) => string);
validateFn?: (params: T) => boolean; validateFn?: (params: T) => boolean;
} }
@ -32,15 +33,24 @@ export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParam
return config.validateFn ? config.validateFn(parameters) : true; return config.validateFn ? config.validateFn(parameters) : true;
}, [parameters, config.validateFn]); }, [parameters, config.validateFn]);
const getEndpointName = useCallback(() => { const endpointName = config.endpointName;
return config.endpointName; let getEndpointName: () => string;
}, [config.endpointName]); if (typeof endpointName === "string") {
getEndpointName = useCallback(() => {
return endpointName;
}, []);
} else {
getEndpointName = useCallback(() => {
return endpointName(parameters);
}, [parameters]);
}
return { return {
parameters, parameters,
setParameters,
updateParameter, updateParameter,
resetParameters, resetParameters,
validateParameters, validateParameters,
getEndpointName, getEndpointName,
}; };
} }

View File

@ -1,7 +1,8 @@
import { useState } from 'react';
import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, SplitType } from '../../../constants/splitConstants'; import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, SplitType } from '../../../constants/splitConstants';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface SplitParameters { export interface SplitParameters extends BaseParameters {
mode: SplitMode | ''; mode: SplitMode | '';
pages: string; pages: string;
hDiv: string; hDiv: string;
@ -14,15 +15,9 @@ export interface SplitParameters {
allowDuplicates: boolean; allowDuplicates: boolean;
} }
export interface SplitParametersHook { export type SplitParametersHook = BaseParametersHook<SplitParameters>;
parameters: SplitParameters;
updateParameter: (parameter: keyof SplitParameters, value: string | boolean) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
const initialParameters: SplitParameters = { const defaultParameters: SplitParameters = {
mode: '', mode: '',
pages: '', pages: '',
hDiv: '2', hDiv: '2',
@ -36,43 +31,27 @@ const initialParameters: SplitParameters = {
}; };
export const useSplitParameters = (): SplitParametersHook => { export const useSplitParameters = (): SplitParametersHook => {
const [parameters, setParameters] = useState<SplitParameters>(initialParameters); return useBaseParameters({
defaultParameters,
endpointName: (params) => {
if (!params.mode) return ENDPOINTS[SPLIT_MODES.BY_PAGES];
return ENDPOINTS[params.mode as SplitMode];
},
validateFn: (params) => {
if (!params.mode) return false;
const updateParameter = (parameter: keyof SplitParameters, value: string | boolean) => { switch (params.mode) {
setParameters(prev => ({ ...prev, [parameter]: value })); case SPLIT_MODES.BY_PAGES:
}; return params.pages.trim() !== "";
case SPLIT_MODES.BY_SECTIONS:
const resetParameters = () => { return params.hDiv !== "" && params.vDiv !== "";
setParameters(initialParameters); case SPLIT_MODES.BY_SIZE_OR_COUNT:
}; return params.splitValue.trim() !== "";
case SPLIT_MODES.BY_CHAPTERS:
const validateParameters = () => { return params.bookmarkLevel !== "";
if (!parameters.mode) return false; default:
return false;
switch (parameters.mode) { }
case SPLIT_MODES.BY_PAGES: },
return parameters.pages.trim() !== ""; });
case SPLIT_MODES.BY_SECTIONS:
return parameters.hDiv !== "" && parameters.vDiv !== "";
case SPLIT_MODES.BY_SIZE_OR_COUNT:
return parameters.splitValue.trim() !== "";
case SPLIT_MODES.BY_CHAPTERS:
return parameters.bookmarkLevel !== "";
default:
return false;
}
};
const getEndpointName = () => {
if (!parameters.mode) return ENDPOINTS[SPLIT_MODES.BY_PAGES];
return ENDPOINTS[parameters.mode as SplitMode];
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}; };

View File

@ -13,6 +13,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../../i18n/config'; import i18n from '../../i18n/config';
import axios from 'axios'; import axios from 'axios';
import { detectFileExtension } from '../../utils/fileUtils'; import { detectFileExtension } from '../../utils/fileUtils';
import { FIT_OPTIONS } from '../../constants/convertConstants';
// Mock axios // Mock axios
vi.mock('axios'); vi.mock('axios');
@ -23,8 +24,8 @@ vi.mock('../../services/fileStorage', () => ({
fileStorage: { fileStorage: {
init: vi.fn().mockResolvedValue(undefined), init: vi.fn().mockResolvedValue(undefined),
storeFile: vi.fn().mockImplementation((file, thumbnail) => { storeFile: vi.fn().mockImplementation((file, thumbnail) => {
return Promise.resolve({ return Promise.resolve({
id: `mock-id-${file.name}`, id: `mock-id-${file.name}`,
name: file.name, name: file.name,
size: file.size, size: file.size,
type: file.type, type: file.type,
@ -403,7 +404,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
colorType: 'grayscale', colorType: 'grayscale',
dpi: 150, dpi: 150,
singleOrMultiple: 'single', singleOrMultiple: 'single',
fitOption: 'fitToPage', fitOption: FIT_OPTIONS.FIT_PAGE,
autoRotate: false, autoRotate: false,
combineImages: true combineImages: true
}); });
@ -417,7 +418,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('fitOption')).toBe('fitToPage'); expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
expect(formData.get('colorType')).toBe('grayscale'); expect(formData.get('colorType')).toBe('grayscale');
expect(formData.get('autoRotate')).toBe('false'); expect(formData.get('autoRotate')).toBe('false');
}); });