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;

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) {
const resetParameters = useCallback(() => {
setParameters(defaultWatermarkParameters);
}, []);
const validateParameters = useCallback((): boolean => {
if (!parameters.watermarkType) {
return false; return false;
} }
if (parameters.watermarkType === 'text') { if (params.watermarkType === 'text') {
return parameters.watermarkText.trim().length > 0; return params.watermarkText.trim().length > 0;
} else { } else {
return parameters.watermarkImage !== undefined; return params.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) => {
};
const resetParameters = () => {
setParameters(initialParameters);
};
const validateParameters = () => {
// For compression, we only need to validate that compression level is within range // 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 params.compressionLevel >= 1 && params.compressionLevel <= 9;
return parameters.compressionLevel >= 1 && parameters.compressionLevel <= 9; },
}; });
const getEndpointName = () => {
return 'compress-pdf';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}; };

View File

@ -5,6 +5,7 @@
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', () => {
@ -44,13 +45,19 @@ describe('useConvertParameters', () => {
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', () => {

View File

@ -1,4 +1,3 @@
import { useState, useEffect } from 'react';
import { import {
COLOR_TYPES, COLOR_TYPES,
OUTPUT_OPTIONS, OUTPUT_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,19 +73,8 @@ 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) => {
setParameters(prev => ({ ...prev, [parameter]: value }));
};
const resetParameters = () => {
setParameters(initialParameters);
};
const validateParameters = () => {
const { fromExtension, toExtension } = parameters;
if (!fromExtension || !toExtension) return false; if (!fromExtension || !toExtension) return false;
@ -108,10 +93,10 @@ export const useConvertParameters = (): ConvertParametersHook => {
} }
return true; return true;
}; };
const getEndpointName = () => { const getEndpointName = (params: ConvertParameters): string => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters; const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = params;
if (isSmartDetection) { if (isSmartDetection) {
if (smartDetectionType === 'mixed') { if (smartDetectionType === 'mixed') {
@ -133,10 +118,17 @@ export const useConvertParameters = (): ConvertParametersHook => {
} }
return getEndpointNameUtil(fromExtension, toExtension); 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') {
@ -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'
@ -215,7 +207,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
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);
@ -256,7 +248,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
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);
@ -285,7 +277,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
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',
@ -303,7 +295,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
})); }));
} 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,11 +307,7 @@ 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,
[key]: value
}));
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
// At minimum, we need at least one language selected // At minimum, we need at least one language selected
return parameters.languages.length > 0; return params.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,12 +33,21 @@ 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,

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 }));
};
const resetParameters = () => {
setParameters(initialParameters);
};
const validateParameters = () => {
if (!parameters.mode) return false;
switch (parameters.mode) {
case SPLIT_MODES.BY_PAGES: case SPLIT_MODES.BY_PAGES:
return parameters.pages.trim() !== ""; return params.pages.trim() !== "";
case SPLIT_MODES.BY_SECTIONS: case SPLIT_MODES.BY_SECTIONS:
return parameters.hDiv !== "" && parameters.vDiv !== ""; return params.hDiv !== "" && params.vDiv !== "";
case SPLIT_MODES.BY_SIZE_OR_COUNT: case SPLIT_MODES.BY_SIZE_OR_COUNT:
return parameters.splitValue.trim() !== ""; return params.splitValue.trim() !== "";
case SPLIT_MODES.BY_CHAPTERS: case SPLIT_MODES.BY_CHAPTERS:
return parameters.bookmarkLevel !== ""; return params.bookmarkLevel !== "";
default: default:
return false; 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');
@ -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');
}); });