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 { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressOperation";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
interface CompressSettingsProps {
parameters: CompressParameters;

View File

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

View File

@ -2,13 +2,7 @@ import React from 'react';
import { Stack, Select, Text, Divider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LanguagePicker from './LanguagePicker';
export interface OCRParameters {
languages: string[];
ocrType: string;
ocrRenderType: string;
additionalOptions: string[];
}
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
interface OCRSettingsProps {
parameters: OCRParameters;

View File

@ -1,5 +1,3 @@
import { AddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters";
export interface AlphabetOption {
value: string;
label: string;
@ -13,16 +11,3 @@ export const alphabetOptions: AlphabetOption[] = [
{ value: "chinese", 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 { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface AddPasswordParameters {
export interface AddPasswordParameters extends BaseParameters {
password: string;
ownerPassword: string;
keyLength: number;
@ -11,14 +12,9 @@ export interface AddPasswordFullParameters extends AddPasswordParameters {
permissions: ChangePermissionsParameters;
}
export interface AddPasswordParametersHook {
export interface AddPasswordParametersHook extends BaseParametersHook<AddPasswordParameters> {
fullParameters: AddPasswordFullParameters;
parameters: AddPasswordParameters;
permissions: ChangePermissionsParametersHook;
updateParameter: <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: AddPasswordParameters = {
@ -28,42 +24,31 @@ export const defaultParameters: AddPasswordParameters = {
};
export const useAddPasswordParameters = (): AddPasswordParametersHook => {
const [parameters, setParameters] = useState<AddPasswordParameters>(defaultParameters);
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 = {
...parameters,
...baseHook.parameters,
permissions: permissions.parameters,
};
const updateParameter = <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
baseHook.resetParameters();
permissions.resetParameters();
};
const validateParameters = () => {
// No required parameters for Add Password. Defer to permissions validation.
return permissions.validateParameters();
};
const getEndpointName = () => {
return 'add-password';
};
return {
...baseHook,
fullParameters,
parameters,
permissions,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
};

View File

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

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;
preventExtractContent: boolean;
preventExtractForAccessibility: boolean;
@ -11,14 +12,6 @@ export interface ChangePermissionsParameters {
preventPrintingFaithful: boolean;
}
export interface ChangePermissionsParametersHook {
parameters: ChangePermissionsParameters;
updateParameter: (parameter: keyof ChangePermissionsParameters, value: boolean) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: ChangePermissionsParameters = {
preventAssembly: false,
preventExtractContent: false,
@ -30,35 +23,11 @@ export const defaultParameters: ChangePermissionsParameters = {
preventPrintingFaithful: false,
};
export type ChangePermissionsParametersHook = BaseParametersHook<ChangePermissionsParameters>;
export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => {
const [parameters, setParameters] = useState<ChangePermissionsParameters>(defaultParameters);
const updateParameter = <K extends keyof ChangePermissionsParameters>(parameter: K, value: ChangePermissionsParameters[K]) => {
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,
};
return useBaseParameters({
defaultParameters,
endpointName: 'add-password', // Change Permissions is a fake endpoint for the Add Password tool
});
};

View File

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

View File

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

View File

@ -5,6 +5,7 @@
import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from './useConvertParameters';
import { FIT_OPTIONS } from '../../../constants/convertConstants';
describe('useConvertParameters', () => {
@ -44,13 +45,19 @@ describe('useConvertParameters', () => {
result.current.updateParameter('imageOptions', {
colorType: 'grayscale',
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.dpi).toBe(150);
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', () => {

View File

@ -1,4 +1,3 @@
import { useState, useEffect } from 'react';
import {
COLOR_TYPES,
OUTPUT_OPTIONS,
@ -11,8 +10,10 @@ import {
} from '../../../constants/convertConstants';
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
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;
toExtension: string;
imageOptions: {
@ -39,18 +40,13 @@ export interface ConvertParameters {
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
export interface ConvertParametersHook {
parameters: ConvertParameters;
updateParameter: (parameter: keyof ConvertParameters, value: any) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
export interface ConvertParametersHook extends BaseParametersHook<ConvertParameters> {
getEndpoint: () => string;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
analyzeFileTypes: (files: Array<{name: string}>) => void;
}
const initialParameters: ConvertParameters = {
const defaultParameters: ConvertParameters = {
fromExtension: '',
toExtension: '',
imageOptions: {
@ -77,66 +73,62 @@ const initialParameters: ConvertParameters = {
smartDetectionType: 'none',
};
export const useConvertParameters = (): ConvertParametersHook => {
const [parameters, setParameters] = useState<ConvertParameters>(initialParameters);
const validateParameters = (params: ConvertParameters): boolean => {
const { fromExtension, toExtension } = params;
const updateParameter = (parameter: keyof ConvertParameters, value: any) => {
setParameters(prev => ({ ...prev, [parameter]: value }));
};
if (!fromExtension || !toExtension) return false;
const resetParameters = () => {
setParameters(initialParameters);
};
// 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] || [];
}
const validateParameters = () => {
const { fromExtension, toExtension } = parameters;
if (!supportedToExtensions.includes(toExtension)) {
return false;
}
if (!fromExtension || !toExtension) return false;
return true;
};
// 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] || [];
}
const getEndpointName = (params: ConvertParameters): string => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = params;
if (!supportedToExtensions.includes(toExtension)) {
return false;
}
return true;
};
const getEndpointName = () => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint
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';
}
}
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint
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 { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = baseHook.parameters;
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
@ -189,7 +181,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
const analyzeFileTypes = (files: Array<{name: string}>) => {
if (files.length === 0) {
// No files - only reset smart detection, keep user's format choices
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: false,
smartDetectionType: 'none'
@ -215,7 +207,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
availableTargets = CONVERSION_MATRIX['any'] || [];
}
setParameters(prev => {
baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt);
@ -256,7 +248,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
availableTargets = CONVERSION_MATRIX['any'] || [];
}
setParameters(prev => {
baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt);
@ -285,7 +277,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
if (allImages) {
// All files are images - use image-to-pdf conversion
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'images',
@ -294,7 +286,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
}));
} else if (allWeb) {
// All files are web files - use html-to-pdf conversion
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'web',
@ -303,7 +295,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
}));
} else {
// Mixed non-image types - use file-to-pdf conversion
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'mixed',
@ -315,11 +307,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
...baseHook,
getEndpoint,
getAvailableToExtensions,
analyzeFileTypes,

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
import { OCRParameters } from './useOCRParameters';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { useToolResources } from '../shared/useToolResources';

View File

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

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;
}
export interface RemovePasswordParametersHook {
parameters: RemovePasswordParameters;
updateParameter: <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export type RemovePasswordParametersHook = BaseParametersHook<RemovePasswordParameters>;
export const defaultParameters: RemovePasswordParameters = {
password: '',
};
export const useRemovePasswordParameters = (): RemovePasswordParametersHook => {
const [parameters, setParameters] = useState<RemovePasswordParameters>(defaultParameters);
const updateParameter = <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
return parameters.password !== '';
};
const getEndpointName = () => {
return 'remove-password';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
return useBaseParameters({
defaultParameters,
endpointName: 'remove-password',
validateFn: (params) => {
return params.password !== '';
},
});
};

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;
removeEmbeddedFiles: boolean;
removeXMPMetadata: boolean;
@ -18,36 +19,14 @@ export const defaultParameters: SanitizeParameters = {
removeFonts: false,
};
export const useSanitizeParameters = () => {
const [parameters, setParameters] = useState<SanitizeParameters>(defaultParameters);
export type SanitizeParametersHook = BaseParametersHook<SanitizeParameters>;
const updateParameter = useCallback(<K extends keyof SanitizeParameters>(
key: K,
value: SanitizeParameters[K]
) => {
setParameters(prev => ({
...prev,
[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,
};
export const useSanitizeParameters = (): SanitizeParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'sanitize-pdf',
validateFn: (params) => {
return Object.values(params).some(value => value === true);
},
});
};

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../../i18n/config';
import axios from 'axios';
import { detectFileExtension } from '../../utils/fileUtils';
import { FIT_OPTIONS } from '../../constants/convertConstants';
// Mock axios
vi.mock('axios');
@ -403,7 +404,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
colorType: 'grayscale',
dpi: 150,
singleOrMultiple: 'single',
fitOption: 'fitToPage',
fitOption: FIT_OPTIONS.FIT_PAGE,
autoRotate: false,
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;
expect(formData.get('fitOption')).toBe('fitToPage');
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
expect(formData.get('colorType')).toBe('grayscale');
expect(formData.get('autoRotate')).toBe('false');
});