mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 22:29:24 +00:00
Redesign Merge around new major merges into V2
This commit is contained in:
parent
dd20a3c0a3
commit
5ed6cc3cfc
@ -2,10 +2,10 @@
|
|||||||
* FileContext reducer - Pure state management for file operations
|
* FileContext reducer - Pure state management for file operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextAction,
|
FileContextAction,
|
||||||
FileId,
|
FileId,
|
||||||
FileRecord
|
FileRecord
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
const { fileRecords } = action.payload;
|
const { fileRecords } = action.payload;
|
||||||
const newIds: FileId[] = [];
|
const newIds: FileId[] = [];
|
||||||
const newById: Record<FileId, FileRecord> = { ...state.files.byId };
|
const newById: Record<FileId, FileRecord> = { ...state.files.byId };
|
||||||
|
|
||||||
fileRecords.forEach(record => {
|
fileRecords.forEach(record => {
|
||||||
// Only add if not already present (dedupe by stable ID)
|
// Only add if not already present (dedupe by stable ID)
|
||||||
if (!newById[record.id]) {
|
if (!newById[record.id]) {
|
||||||
@ -40,7 +40,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
newById[record.id] = record;
|
newById[record.id] = record;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
@ -49,20 +49,20 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'REMOVE_FILES': {
|
case 'REMOVE_FILES': {
|
||||||
const { fileIds } = action.payload;
|
const { fileIds } = action.payload;
|
||||||
const remainingIds = state.files.ids.filter(id => !fileIds.includes(id));
|
const remainingIds = state.files.ids.filter(id => !fileIds.includes(id));
|
||||||
const newById = { ...state.files.byId };
|
const newById = { ...state.files.byId };
|
||||||
|
|
||||||
// Remove files from state (resource cleanup handled by lifecycle manager)
|
// Remove files from state (resource cleanup handled by lifecycle manager)
|
||||||
fileIds.forEach(id => {
|
fileIds.forEach(id => {
|
||||||
delete newById[id];
|
delete newById[id];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear selections that reference removed files
|
// Clear selections that reference removed files
|
||||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !fileIds.includes(id));
|
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !fileIds.includes(id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
@ -75,15 +75,15 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UPDATE_FILE_RECORD': {
|
case 'UPDATE_FILE_RECORD': {
|
||||||
const { id, updates } = action.payload;
|
const { id, updates } = action.payload;
|
||||||
const existingRecord = state.files.byId[id];
|
const existingRecord = state.files.byId[id];
|
||||||
|
|
||||||
if (!existingRecord) {
|
if (!existingRecord) {
|
||||||
return state; // File doesn't exist, no-op
|
return state; // File doesn't exist, no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
@ -98,22 +98,28 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'REORDER_FILES': {
|
case 'REORDER_FILES': {
|
||||||
const { orderedFileIds } = action.payload;
|
const { orderedFileIds } = action.payload;
|
||||||
|
|
||||||
// Validate that all IDs exist in current state
|
// Validate that all IDs exist in current state
|
||||||
const validIds = orderedFileIds.filter(id => state.files.byId[id]);
|
const validIds = orderedFileIds.filter(id => state.files.byId[id]);
|
||||||
|
// Reorder selected files by passed order
|
||||||
|
const selectedFileIds = orderedFileIds.filter(id => state.ui.selectedFileIds.includes(id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
...state.files,
|
...state.files,
|
||||||
ids: validIds
|
ids: validIds
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
...state.ui,
|
||||||
|
selectedFileIds,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_SELECTED_FILES': {
|
case 'SET_SELECTED_FILES': {
|
||||||
const { fileIds } = action.payload;
|
const { fileIds } = action.payload;
|
||||||
return {
|
return {
|
||||||
@ -124,7 +130,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_SELECTED_PAGES': {
|
case 'SET_SELECTED_PAGES': {
|
||||||
const { pageNumbers } = action.payload;
|
const { pageNumbers } = action.payload;
|
||||||
return {
|
return {
|
||||||
@ -135,7 +141,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'CLEAR_SELECTIONS': {
|
case 'CLEAR_SELECTIONS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -146,7 +152,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_PROCESSING': {
|
case 'SET_PROCESSING': {
|
||||||
const { isProcessing, progress } = action.payload;
|
const { isProcessing, progress } = action.payload;
|
||||||
return {
|
return {
|
||||||
@ -158,7 +164,7 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_UNSAVED_CHANGES': {
|
case 'SET_UNSAVED_CHANGES': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -168,42 +174,42 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'PIN_FILE': {
|
case 'PIN_FILE': {
|
||||||
const { fileId } = action.payload;
|
const { fileId } = action.payload;
|
||||||
const newPinnedFiles = new Set(state.pinnedFiles);
|
const newPinnedFiles = new Set(state.pinnedFiles);
|
||||||
newPinnedFiles.add(fileId);
|
newPinnedFiles.add(fileId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
pinnedFiles: newPinnedFiles
|
pinnedFiles: newPinnedFiles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UNPIN_FILE': {
|
case 'UNPIN_FILE': {
|
||||||
const { fileId } = action.payload;
|
const { fileId } = action.payload;
|
||||||
const newPinnedFiles = new Set(state.pinnedFiles);
|
const newPinnedFiles = new Set(state.pinnedFiles);
|
||||||
newPinnedFiles.delete(fileId);
|
newPinnedFiles.delete(fileId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
pinnedFiles: newPinnedFiles
|
pinnedFiles: newPinnedFiles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'CONSUME_FILES': {
|
case 'CONSUME_FILES': {
|
||||||
const { inputFileIds, outputFileRecords } = action.payload;
|
const { inputFileIds, outputFileRecords } = action.payload;
|
||||||
|
|
||||||
// Only remove unpinned input files
|
// Only remove unpinned input files
|
||||||
const unpinnedInputIds = inputFileIds.filter(id => !state.pinnedFiles.has(id));
|
const unpinnedInputIds = inputFileIds.filter(id => !state.pinnedFiles.has(id));
|
||||||
const remainingIds = state.files.ids.filter(id => !unpinnedInputIds.includes(id));
|
const remainingIds = state.files.ids.filter(id => !unpinnedInputIds.includes(id));
|
||||||
|
|
||||||
// Remove unpinned files from state
|
// Remove unpinned files from state
|
||||||
const newById = { ...state.files.byId };
|
const newById = { ...state.files.byId };
|
||||||
unpinnedInputIds.forEach(id => {
|
unpinnedInputIds.forEach(id => {
|
||||||
delete newById[id];
|
delete newById[id];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add output files
|
// Add output files
|
||||||
const outputIds: FileId[] = [];
|
const outputIds: FileId[] = [];
|
||||||
outputFileRecords.forEach(record => {
|
outputFileRecords.forEach(record => {
|
||||||
@ -212,10 +218,10 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
newById[record.id] = record;
|
newById[record.id] = record;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear selections that reference removed files
|
// Clear selections that reference removed files
|
||||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedInputIds.includes(id));
|
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedInputIds.includes(id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
files: {
|
files: {
|
||||||
@ -228,13 +234,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RESET_CONTEXT': {
|
case 'RESET_CONTEXT': {
|
||||||
// Reset UI state to clean slate (resource cleanup handled by lifecycle manager)
|
// Reset UI state to clean slate (resource cleanup handled by lifecycle manager)
|
||||||
return { ...initialFileContextState };
|
return { ...initialFileContextState };
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
FileStateContext,
|
FileStateContext,
|
||||||
FileActionsContext,
|
FileActionsContext,
|
||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue
|
FileContextActionsValue
|
||||||
} from './contexts';
|
} from './contexts';
|
||||||
import { FileId, FileRecord } from '../../types/fileContext';
|
import { FileId, FileRecord } from '../../types/fileContext';
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export function useFileActions(): FileContextActionsValue {
|
|||||||
*/
|
*/
|
||||||
export function useCurrentFile(): { file?: File; record?: FileRecord } {
|
export function useCurrentFile(): { file?: File; record?: FileRecord } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
const primaryFileId = state.files.ids[0];
|
const primaryFileId = state.files.ids[0];
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
||||||
@ -81,7 +81,7 @@ export function useFileSelection() {
|
|||||||
*/
|
*/
|
||||||
export function useFileManagement() {
|
export function useFileManagement() {
|
||||||
const { actions } = useFileActions();
|
const { actions } = useFileActions();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
addFiles: actions.addFiles,
|
addFiles: actions.addFiles,
|
||||||
removeFiles: actions.removeFiles,
|
removeFiles: actions.removeFiles,
|
||||||
@ -112,7 +112,7 @@ export function useFileUI() {
|
|||||||
*/
|
*/
|
||||||
export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } {
|
export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } {
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: selectors.getFile(fileId),
|
file: selectors.getFile(fileId),
|
||||||
record: selectors.getFileRecord(fileId)
|
record: selectors.getFileRecord(fileId)
|
||||||
@ -124,7 +124,7 @@ export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecor
|
|||||||
*/
|
*/
|
||||||
export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getFiles(),
|
files: selectors.getFiles(),
|
||||||
records: selectors.getFileRecords(),
|
records: selectors.getFileRecords(),
|
||||||
@ -135,13 +135,13 @@ export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds:
|
|||||||
/**
|
/**
|
||||||
* Hook for selected files (optimized for selection-based UI)
|
* Hook for selected files (optimized for selection-based UI)
|
||||||
*/
|
*/
|
||||||
export function useSelectedFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useSelectedFiles(): { selectedFiles: File[]; selectedRecords: FileRecord[]; selectedFileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getSelectedFiles(),
|
selectedFiles: selectors.getSelectedFiles(),
|
||||||
records: selectors.getSelectedFileRecords(),
|
selectedRecords: selectors.getSelectedFileRecords(),
|
||||||
fileIds: state.ui.selectedFileIds
|
selectedFileIds: state.ui.selectedFileIds
|
||||||
}), [state.ui.selectedFileIds, selectors]);
|
}), [state.ui.selectedFileIds, selectors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,31 +160,31 @@ export function useFileContext() {
|
|||||||
trackBlobUrl: actions.trackBlobUrl,
|
trackBlobUrl: actions.trackBlobUrl,
|
||||||
scheduleCleanup: actions.scheduleCleanup,
|
scheduleCleanup: actions.scheduleCleanup,
|
||||||
setUnsavedChanges: actions.setHasUnsavedChanges,
|
setUnsavedChanges: actions.setHasUnsavedChanges,
|
||||||
|
|
||||||
// File management
|
// File management
|
||||||
addFiles: actions.addFiles,
|
addFiles: actions.addFiles,
|
||||||
consumeFiles: actions.consumeFiles,
|
consumeFiles: actions.consumeFiles,
|
||||||
recordOperation: (fileId: string, operation: any) => {}, // Operation tracking not implemented
|
recordOperation: (fileId: string, operation: any) => {}, // Operation tracking not implemented
|
||||||
markOperationApplied: (fileId: string, operationId: string) => {}, // Operation tracking not implemented
|
markOperationApplied: (fileId: string, operationId: string) => {}, // Operation tracking not implemented
|
||||||
markOperationFailed: (fileId: string, operationId: string, error: string) => {}, // Operation tracking not implemented
|
markOperationFailed: (fileId: string, operationId: string, error: string) => {}, // Operation tracking not implemented
|
||||||
|
|
||||||
// File ID lookup
|
// File ID lookup
|
||||||
findFileId: (file: File) => {
|
findFileId: (file: File) => {
|
||||||
return state.files.ids.find(id => {
|
return state.files.ids.find(id => {
|
||||||
const record = state.files.byId[id];
|
const record = state.files.byId[id];
|
||||||
return record &&
|
return record &&
|
||||||
record.name === file.name &&
|
record.name === file.name &&
|
||||||
record.size === file.size &&
|
record.size === file.size &&
|
||||||
record.lastModified === file.lastModified;
|
record.lastModified === file.lastModified;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pinned files
|
// Pinned files
|
||||||
pinnedFiles: state.pinnedFiles,
|
pinnedFiles: state.pinnedFiles,
|
||||||
pinFile: actions.pinFile,
|
pinFile: actions.pinFile,
|
||||||
unpinFile: actions.unpinFile,
|
unpinFile: actions.unpinFile,
|
||||||
isFilePinned: selectors.isFilePinned,
|
isFilePinned: selectors.isFilePinned,
|
||||||
|
|
||||||
// Active files
|
// Active files
|
||||||
activeFiles: selectors.getFiles()
|
activeFiles: selectors.getFiles()
|
||||||
}), [state, selectors, actions]);
|
}), [state, selectors, actions]);
|
||||||
|
@ -28,6 +28,7 @@ import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation';
|
|||||||
import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation';
|
import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation';
|
||||||
import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation';
|
import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation';
|
||||||
import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation';
|
import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation';
|
||||||
|
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
|
||||||
import CompressSettings from '../components/tools/compress/CompressSettings';
|
import CompressSettings from '../components/tools/compress/CompressSettings';
|
||||||
import SplitSettings from '../components/tools/split/SplitSettings';
|
import SplitSettings from '../components/tools/split/SplitSettings';
|
||||||
import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings';
|
import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings';
|
||||||
@ -39,6 +40,7 @@ import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/Add
|
|||||||
import OCRSettings from '../components/tools/ocr/OCRSettings';
|
import OCRSettings from '../components/tools/ocr/OCRSettings';
|
||||||
import ConvertSettings from '../components/tools/convert/ConvertSettings';
|
import ConvertSettings from '../components/tools/convert/ConvertSettings';
|
||||||
import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings';
|
import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings';
|
||||||
|
import MergeSettings from '../components/tools/merge/MergeSettings';
|
||||||
|
|
||||||
const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented
|
const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented
|
||||||
|
|
||||||
@ -636,6 +638,8 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["merge-pdfs"],
|
endpoints: ["merge-pdfs"],
|
||||||
|
operationConfig: mergeOperationConfig,
|
||||||
|
settingsComponent: MergeSettings
|
||||||
},
|
},
|
||||||
"multi-tool": {
|
"multi-tool": {
|
||||||
icon: <span className="material-symbols-rounded">dashboard_customize</span>,
|
icon: <span className="material-symbols-rounded">dashboard_customize</span>,
|
||||||
|
@ -20,12 +20,12 @@ vi.mock('../../../utils/toolErrorHandler', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Import the mocked function
|
// Import the mocked function
|
||||||
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
|
import { ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
|
||||||
|
|
||||||
describe('useMergeOperation', () => {
|
describe('useMergeOperation', () => {
|
||||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
const mockUseToolOperation = vi.mocked(useToolOperation<MergeParameters>);
|
||||||
|
|
||||||
const getToolConfig = (): ToolOperationConfig<MergeParameters> => mockUseToolOperation.mock.calls[0][0];
|
const getToolConfig = () => mockUseToolOperation.mock.calls[0][0];
|
||||||
|
|
||||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||||
files: [],
|
files: [],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation, ResponseHandler } from '../shared/useToolOperation';
|
import { useToolOperation, ResponseHandler, ToolOperationConfig } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { MergeParameters } from './useMergeParameters';
|
import { MergeParameters } from './useMergeParameters';
|
||||||
|
|
||||||
@ -21,16 +21,21 @@ const mergeResponseHandler: ResponseHandler = (blob: Blob, originalFiles: File[]
|
|||||||
return [new File([blob], filename, { type: 'application/pdf' })];
|
return [new File([blob], filename, { type: 'application/pdf' })];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Operation configuration for automation
|
||||||
|
export const mergeOperationConfig: ToolOperationConfig<MergeParameters> = {
|
||||||
|
operationType: 'merge',
|
||||||
|
endpoint: '/api/v1/general/merge-pdfs',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: 'merged_',
|
||||||
|
multiFileEndpoint: true,
|
||||||
|
responseHandler: mergeResponseHandler,
|
||||||
|
};
|
||||||
|
|
||||||
export const useMergeOperation = () => {
|
export const useMergeOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<MergeParameters>({
|
return useToolOperation<MergeParameters>({
|
||||||
operationType: 'merge',
|
...mergeOperationConfig,
|
||||||
endpoint: '/api/v1/general/merge-pdfs',
|
|
||||||
buildFormData,
|
|
||||||
filePrefix: 'merged_',
|
|
||||||
multiFileEndpoint: true, // Single API call with all files
|
|
||||||
responseHandler: mergeResponseHandler, // Handle single PDF response
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('merge.error.failed', 'An error occurred while merging the PDFs.'))
|
getErrorMessage: createStandardErrorHandler(t('merge.error.failed', 'An error occurred while merging the PDFs.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileSelection, useFileManagement, useSelectedFiles, useAllFiles } from "../contexts/FileContext";
|
||||||
import { useToolFileSelection, useFileSelectionActions } from "../contexts/FileSelectionContext";
|
|
||||||
|
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import MergeSettings from "../components/tools/merge/MergeSettings";
|
import MergeSettings from "../components/tools/merge/MergeSettings";
|
||||||
@ -14,9 +13,10 @@ import { BaseToolProps } from "../types/tool";
|
|||||||
|
|
||||||
const Merge = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Merge = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setCurrentMode } = useFileContext();
|
const { selectedFiles, selectedFileIds } = useFileSelection();
|
||||||
const { selectedFiles } = useToolFileSelection();
|
const { fileIds } = useAllFiles()
|
||||||
const { setSelectedFiles } = useFileSelectionActions();
|
const { selectedRecords } = useSelectedFiles()
|
||||||
|
const { reorderFiles } = useFileManagement();
|
||||||
|
|
||||||
const mergeParams = useMergeParameters();
|
const mergeParams = useMergeParameters();
|
||||||
const mergeOperation = useMergeOperation();
|
const mergeOperation = useMergeOperation();
|
||||||
@ -45,36 +45,35 @@ const Merge = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
sessionStorage.setItem("previousMode", "merge");
|
sessionStorage.setItem("previousMode", "merge");
|
||||||
setCurrentMode("viewer");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsReset = () => {
|
const handleSettingsReset = () => {
|
||||||
mergeOperation.resetResults();
|
mergeOperation.resetResults();
|
||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
setCurrentMode("merge");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move to more general place so other tools can use it
|
// TODO: Move to more general place so other tools can use it
|
||||||
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
|
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
|
||||||
setSelectedFiles(((prevFiles: File[]) => {
|
// Sort the FileIds based on their corresponding File properties
|
||||||
const sortedFiles = [...prevFiles].sort((a, b) => {
|
const sortedRecords = [...selectedRecords].sort((recordA, recordB) => {
|
||||||
let comparison = 0;
|
let comparison = 0;
|
||||||
|
switch (sortType) {
|
||||||
|
case 'filename':
|
||||||
|
comparison = recordA.name.localeCompare(recordB.name);
|
||||||
|
break;
|
||||||
|
case 'dateModified':
|
||||||
|
comparison = recordA.lastModified - recordB.lastModified;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortType) {
|
return ascending ? comparison : -comparison;
|
||||||
case 'filename':
|
});
|
||||||
comparison = a.name.localeCompare(b.name);
|
|
||||||
break;
|
|
||||||
case 'dateModified':
|
|
||||||
comparison = a.lastModified - b.lastModified;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ascending ? comparison : -comparison;
|
const selectedIds = sortedRecords.map(record => record.id);
|
||||||
});
|
const deselectedIds = fileIds.filter(id => !selectedIds.includes(id));
|
||||||
|
|
||||||
return sortedFiles;
|
reorderFiles([...selectedIds, ...deselectedIds]); // Move all sorted IDs to the front of the workbench
|
||||||
}) as any /* FIX ME: Parameter type is wrong on setSelectedFiles */);
|
}, [selectedFiles, selectedFileIds, reorderFiles]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const minFiles = 2; // Merging one file doesn't make sense
|
const minFiles = 2; // Merging one file doesn't make sense
|
||||||
const hasFiles = selectedFiles.length >= minFiles;
|
const hasFiles = selectedFiles.length >= minFiles;
|
||||||
@ -83,7 +82,7 @@ const Merge = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
|
|
||||||
return createToolFlow({
|
return createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles,
|
selectedFiles: selectedFiles,
|
||||||
isCollapsed: hasFiles && !hasResults,
|
isCollapsed: hasFiles && !hasResults,
|
||||||
placeholder: "Select multiple PDF files to merge",
|
placeholder: "Select multiple PDF files to merge",
|
||||||
minFiles: minFiles,
|
minFiles: minFiles,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user