mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Add Merge UI to V2 (#4235)
# Description of Changes Add UI for Merge into V2.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useMergeOperation } from './useMergeOperation';
|
||||
import type { MergeParameters } from './useMergeParameters';
|
||||
|
||||
// Mock the useToolOperation hook
|
||||
vi.mock('../shared/useToolOperation', async () => {
|
||||
const actual = await vi.importActual('../shared/useToolOperation');
|
||||
return {
|
||||
...actual,
|
||||
useToolOperation: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the translation hook
|
||||
const mockT = vi.fn((key: string) => `translated-${key}`);
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
// Mock the error handler
|
||||
vi.mock('../../../utils/toolErrorHandler', () => ({
|
||||
createStandardErrorHandler: vi.fn(() => 'error-handler-function')
|
||||
}));
|
||||
|
||||
// Import the mocked function
|
||||
import { MultiFileToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
|
||||
|
||||
describe('useMergeOperation', () => {
|
||||
const mockUseToolOperation = vi.mocked(useToolOperation<MergeParameters>);
|
||||
|
||||
const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as MultiFileToolOperationConfig<MergeParameters>;
|
||||
|
||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||
files: [],
|
||||
thumbnails: [],
|
||||
downloadUrl: null,
|
||||
downloadFilename: '',
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
status: '',
|
||||
isGeneratingThumbnails: false,
|
||||
progress: null,
|
||||
executeOperation: vi.fn(),
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: function (): Promise<void> {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||
});
|
||||
|
||||
test('should build FormData correctly', () => {
|
||||
renderHook(() => useMergeOperation());
|
||||
|
||||
const config = getToolConfig();
|
||||
const mockFiles = [
|
||||
new File(['content1'], 'file1.pdf', { type: 'application/pdf' }),
|
||||
new File(['content2'], 'file2.pdf', { type: 'application/pdf' })
|
||||
];
|
||||
const parameters: MergeParameters = {
|
||||
removeDigitalSignature: true,
|
||||
generateTableOfContents: false
|
||||
};
|
||||
|
||||
const formData = config.buildFormData(parameters, mockFiles);
|
||||
|
||||
// Verify files are appended
|
||||
expect(formData.getAll('fileInput')).toHaveLength(2);
|
||||
expect(formData.getAll('fileInput')[0]).toBe(mockFiles[0]);
|
||||
expect(formData.getAll('fileInput')[1]).toBe(mockFiles[1]);
|
||||
|
||||
// Verify parameters are appended correctly
|
||||
expect(formData.get('sortType')).toBe('orderProvided');
|
||||
expect(formData.get('removeCertSign')).toBe('true');
|
||||
expect(formData.get('generateToc')).toBe('false');
|
||||
});
|
||||
|
||||
test('should handle response correctly', () => {
|
||||
renderHook(() => useMergeOperation());
|
||||
|
||||
const config = getToolConfig();
|
||||
const mockBlob = new Blob(['merged content'], { type: 'application/pdf' });
|
||||
const mockFiles = [
|
||||
new File(['content1'], 'file1.pdf', { type: 'application/pdf' }),
|
||||
new File(['content2'], 'file2.pdf', { type: 'application/pdf' })
|
||||
];
|
||||
|
||||
const result = config.responseHandler!(mockBlob, mockFiles) as File[];
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('merged_file1.pdf');
|
||||
expect(result[0].type).toBe('application/pdf');
|
||||
expect(result[0].size).toBe(mockBlob.size);
|
||||
});
|
||||
|
||||
test('should return the hook result from useToolOperation', () => {
|
||||
const { result } = renderHook(() => useMergeOperation());
|
||||
|
||||
expect(result.current).toBe(mockToolOperationReturn);
|
||||
});
|
||||
|
||||
test('should use correct translation keys for error handling', () => {
|
||||
renderHook(() => useMergeOperation());
|
||||
|
||||
expect(mockT).toHaveBeenCalledWith('merge.error.failed', 'An error occurred while merging the PDFs.');
|
||||
});
|
||||
|
||||
test('should build FormData with different parameter combinations', () => {
|
||||
renderHook(() => useMergeOperation());
|
||||
|
||||
const config = getToolConfig();
|
||||
const mockFiles = [new File(['test'], 'test.pdf', { type: 'application/pdf' })];
|
||||
|
||||
// Test case 1: All options disabled
|
||||
const params1: MergeParameters = {
|
||||
removeDigitalSignature: false,
|
||||
generateTableOfContents: false
|
||||
};
|
||||
const formData1 = config.buildFormData(params1, mockFiles);
|
||||
expect(formData1.get('removeCertSign')).toBe('false');
|
||||
expect(formData1.get('generateToc')).toBe('false');
|
||||
|
||||
// Test case 2: All options enabled
|
||||
const params2: MergeParameters = {
|
||||
removeDigitalSignature: true,
|
||||
generateTableOfContents: true
|
||||
};
|
||||
const formData2 = config.buildFormData(params2, mockFiles);
|
||||
expect(formData2.get('removeCertSign')).toBe('true');
|
||||
expect(formData2.get('generateToc')).toBe('true');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ResponseHandler, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { MergeParameters } from './useMergeParameters';
|
||||
|
||||
const buildFormData = (parameters: MergeParameters, files: File[]): FormData => {
|
||||
const formData = new FormData();
|
||||
|
||||
files.forEach((file) => {
|
||||
formData.append("fileInput", file);
|
||||
});
|
||||
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
|
||||
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
|
||||
formData.append("generateToc", parameters.generateTableOfContents.toString());
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const mergeResponseHandler: ResponseHandler = (blob: Blob, originalFiles: File[]): File[] => {
|
||||
const filename = `merged_${originalFiles[0].name}`
|
||||
return [new File([blob], filename, { type: 'application/pdf' })];
|
||||
};
|
||||
|
||||
// Operation configuration for automation
|
||||
export const mergeOperationConfig: ToolOperationConfig<MergeParameters> = {
|
||||
toolType: ToolType.multiFile,
|
||||
buildFormData,
|
||||
operationType: 'merge',
|
||||
endpoint: '/api/v1/general/merge-pdfs',
|
||||
filePrefix: 'merged_',
|
||||
responseHandler: mergeResponseHandler,
|
||||
};
|
||||
|
||||
export const useMergeOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<MergeParameters>({
|
||||
...mergeOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('merge.error.failed', 'An error occurred while merging the PDFs.'))
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useMergeParameters, defaultParameters } from './useMergeParameters';
|
||||
|
||||
describe('useMergeParameters', () => {
|
||||
test('should initialize with default parameters', () => {
|
||||
const { result } = renderHook(() => useMergeParameters());
|
||||
|
||||
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ paramName: 'removeDigitalSignature' as const, value: true },
|
||||
{ paramName: 'removeDigitalSignature' as const, value: false },
|
||||
{ paramName: 'generateTableOfContents' as const, value: true },
|
||||
{ paramName: 'generateTableOfContents' as const, value: false }
|
||||
])('should update parameter $paramName to $value', ({ paramName, value }) => {
|
||||
const { result } = renderHook(() => useMergeParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter(paramName, value);
|
||||
});
|
||||
|
||||
expect(result.current.parameters[paramName]).toBe(value);
|
||||
});
|
||||
|
||||
test('should reset parameters to defaults', () => {
|
||||
const { result } = renderHook(() => useMergeParameters());
|
||||
|
||||
// First, change some parameters
|
||||
act(() => {
|
||||
result.current.updateParameter('removeDigitalSignature', true);
|
||||
result.current.updateParameter('generateTableOfContents', true);
|
||||
});
|
||||
|
||||
expect(result.current.parameters.removeDigitalSignature).toBe(true);
|
||||
expect(result.current.parameters.generateTableOfContents).toBe(true);
|
||||
|
||||
// Then reset
|
||||
act(() => {
|
||||
result.current.resetParameters();
|
||||
});
|
||||
|
||||
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||
});
|
||||
|
||||
test('should validate parameters correctly - always returns true', () => {
|
||||
const { result } = renderHook(() => useMergeParameters());
|
||||
|
||||
// Default state should be valid
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
|
||||
// Change parameters and validate again
|
||||
act(() => {
|
||||
result.current.updateParameter('removeDigitalSignature', true);
|
||||
result.current.updateParameter('generateTableOfContents', true);
|
||||
});
|
||||
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
|
||||
// Reset and validate again
|
||||
act(() => {
|
||||
result.current.resetParameters();
|
||||
});
|
||||
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { BaseParametersHook, useBaseParameters } from '../shared/useBaseParameters';
|
||||
|
||||
export interface MergeParameters extends BaseParameters {
|
||||
removeDigitalSignature: boolean;
|
||||
generateTableOfContents: boolean;
|
||||
};
|
||||
|
||||
export const defaultParameters: MergeParameters = {
|
||||
removeDigitalSignature: false,
|
||||
generateTableOfContents: false,
|
||||
};
|
||||
|
||||
export type MergeParametersHook = BaseParametersHook<MergeParameters>;
|
||||
|
||||
export const useMergeParameters = (): MergeParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: "merge-pdfs",
|
||||
});
|
||||
};
|
||||
@@ -38,7 +38,9 @@ export function useBaseTool<TParams>(
|
||||
useParams: () => BaseParametersHook<TParams>,
|
||||
useOperation: () => ToolOperationHook<TParams>,
|
||||
props: BaseToolProps,
|
||||
options?: { minFiles?: number }
|
||||
): BaseToolReturn<TParams> {
|
||||
const minFiles = options?.minFiles ?? 1;
|
||||
const { onPreviewFile, onComplete, onError } = props;
|
||||
|
||||
// File selection
|
||||
@@ -96,7 +98,7 @@ export function useBaseTool<TParams>(
|
||||
}, [operation, onPreviewFile]);
|
||||
|
||||
// Standard computed state
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasFiles = selectedFiles.length >= minFiles;
|
||||
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user