mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Restore filewithid changes
This commit is contained in:
parent
1a3e8e7ecf
commit
0cb2161d33
@ -61,8 +61,8 @@ const FileEditorThumbnail = ({
|
|||||||
|
|
||||||
// Resolve the actual File object for pin/unpin operations
|
// Resolve the actual File object for pin/unpin operations
|
||||||
const actualFile = useMemo(() => {
|
const actualFile = useMemo(() => {
|
||||||
return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
|
return activeFiles.find(f => f.fileId === file.id);
|
||||||
}, [activeFiles, file.name, file.size]);
|
}, [activeFiles, file.id]);
|
||||||
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
||||||
|
|
||||||
const downloadSelectedFile = useCallback(() => {
|
const downloadSelectedFile = useCallback(() => {
|
||||||
|
@ -61,8 +61,8 @@ const FileThumbnail = ({
|
|||||||
|
|
||||||
// Resolve the actual File object for pin/unpin operations
|
// Resolve the actual File object for pin/unpin operations
|
||||||
const actualFile = useMemo(() => {
|
const actualFile = useMemo(() => {
|
||||||
return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
|
return activeFiles.find(f => f.fileId === file.id);
|
||||||
}, [activeFiles, file.name, file.size]);
|
}, [activeFiles, file.id]);
|
||||||
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
||||||
|
|
||||||
const downloadSelectedFile = useCallback(() => {
|
const downloadSelectedFile = useCallback(() => {
|
||||||
|
@ -19,7 +19,10 @@ import {
|
|||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue,
|
FileContextActionsValue,
|
||||||
FileContextActions,
|
FileContextActions,
|
||||||
FileRecord
|
FileId,
|
||||||
|
FileRecord,
|
||||||
|
FileWithId,
|
||||||
|
createFileWithId
|
||||||
} from '../types/fileContext';
|
} from '../types/fileContext';
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
@ -29,7 +32,6 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions
|
|||||||
import { FileLifecycleManager } from './file/lifecycle';
|
import { FileLifecycleManager } from './file/lifecycle';
|
||||||
import { FileStateContext, FileActionsContext } from './file/contexts';
|
import { FileStateContext, FileActionsContext } from './file/contexts';
|
||||||
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
||||||
import { FileId } from '../types/file';
|
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ function FileContextInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File operations using unified addFiles helper with persistence
|
// File operations using unified addFiles helper with persistence
|
||||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<File[]> => {
|
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<FileWithId[]> => {
|
||||||
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -98,15 +100,15 @@ function FileContextInner({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return addedFilesWithIds.map(({ file }) => file);
|
return addedFilesWithIds.map(({ file, id }) => createFileWithId(file, id));
|
||||||
}, [indexedDB, enablePersistence]);
|
}, [indexedDB, enablePersistence]);
|
||||||
|
|
||||||
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<File[]> => {
|
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<FileWithId[]> => {
|
||||||
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
return result.map(({ file }) => file);
|
return result.map(({ file, id }) => createFileWithId(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<File[]> => {
|
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<FileWithId[]> => {
|
||||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -114,7 +116,7 @@ function FileContextInner({
|
|||||||
selectFiles(result);
|
selectFiles(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map(({ file }) => file);
|
return result.map(({ file, id }) => createFileWithId(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Action creators
|
// Action creators
|
||||||
@ -140,24 +142,14 @@ function FileContextInner({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// File-to-ID wrapper functions for pinning
|
// File pinning functions - use FileWithId directly
|
||||||
const pinFileWrapper = useCallback((file: File) => {
|
const pinFileWrapper = useCallback((file: FileWithId) => {
|
||||||
const fileId = findFileId(file);
|
baseActions.pinFile(file.fileId);
|
||||||
if (fileId) {
|
}, [baseActions]);
|
||||||
baseActions.pinFile(fileId);
|
|
||||||
} else {
|
|
||||||
console.warn('File not found for pinning:', file.name);
|
|
||||||
}
|
|
||||||
}, [baseActions, findFileId]);
|
|
||||||
|
|
||||||
const unpinFileWrapper = useCallback((file: File) => {
|
const unpinFileWrapper = useCallback((file: FileWithId) => {
|
||||||
const fileId = findFileId(file);
|
baseActions.unpinFile(file.fileId);
|
||||||
if (fileId) {
|
}, [baseActions]);
|
||||||
baseActions.unpinFile(fileId);
|
|
||||||
} else {
|
|
||||||
console.warn('File not found for unpinning:', file.name);
|
|
||||||
}
|
|
||||||
}, [baseActions, findFileId]);
|
|
||||||
|
|
||||||
// Complete actions object
|
// Complete actions object
|
||||||
const actions = useMemo<FileContextActions>(() => ({
|
const actions = useMemo<FileContextActions>(() => ({
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue
|
FileContextActionsValue
|
||||||
} from './contexts';
|
} from './contexts';
|
||||||
import { FileRecord } from '../../types/fileContext';
|
import { FileRecord, FileWithId } from '../../types/fileContext';
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,7 +123,7 @@ export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecor
|
|||||||
/**
|
/**
|
||||||
* Hook for all files (use sparingly - causes re-renders on file list changes)
|
* Hook for all files (use sparingly - causes re-renders on file list changes)
|
||||||
*/
|
*/
|
||||||
export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useAllFiles(): { files: FileWithId[]; records: FileRecord[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
@ -136,7 +136,7 @@ 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(): { files: FileWithId[]; records: FileRecord[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
|
@ -6,7 +6,9 @@ import { FileId } from '../../types/file';
|
|||||||
import {
|
import {
|
||||||
FileRecord,
|
FileRecord,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextSelectors
|
FileContextSelectors,
|
||||||
|
FileWithId,
|
||||||
|
createFileWithId
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,11 +19,19 @@ export function createFileSelectors(
|
|||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): FileContextSelectors {
|
): FileContextSelectors {
|
||||||
return {
|
return {
|
||||||
getFile: (id: FileId) => filesRef.current.get(id),
|
getFile: (id: FileId) => {
|
||||||
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createFileWithId(file, id) : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
getFiles: (ids?: FileId[]) => {
|
getFiles: (ids?: FileId[]) => {
|
||||||
const currentIds = ids || stateRef.current.files.ids;
|
const currentIds = ids || stateRef.current.files.ids;
|
||||||
return currentIds.map(id => filesRef.current.get(id)).filter(Boolean) as File[];
|
return currentIds
|
||||||
|
.map(id => {
|
||||||
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createFileWithId(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as FileWithId[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getFileRecord: (id: FileId) => stateRef.current.files.byId[id],
|
getFileRecord: (id: FileId) => stateRef.current.files.byId[id],
|
||||||
@ -35,8 +45,11 @@ export function createFileSelectors(
|
|||||||
|
|
||||||
getSelectedFiles: () => {
|
getSelectedFiles: () => {
|
||||||
return stateRef.current.ui.selectedFileIds
|
return stateRef.current.ui.selectedFileIds
|
||||||
.map(id => filesRef.current.get(id))
|
.map(id => {
|
||||||
.filter(Boolean) as File[];
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createFileWithId(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as FileWithId[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getSelectedFileRecords: () => {
|
getSelectedFileRecords: () => {
|
||||||
@ -52,8 +65,11 @@ export function createFileSelectors(
|
|||||||
|
|
||||||
getPinnedFiles: () => {
|
getPinnedFiles: () => {
|
||||||
return Array.from(stateRef.current.pinnedFiles)
|
return Array.from(stateRef.current.pinnedFiles)
|
||||||
.map(id => filesRef.current.get(id))
|
.map(id => {
|
||||||
.filter(Boolean) as File[];
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createFileWithId(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as FileWithId[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getPinnedFileRecords: () => {
|
getPinnedFileRecords: () => {
|
||||||
@ -62,16 +78,8 @@ export function createFileSelectors(
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
isFilePinned: (file: File) => {
|
isFilePinned: (file: FileWithId) => {
|
||||||
// Find FileId by matching File object properties
|
return stateRef.current.pinnedFiles.has(file.fileId);
|
||||||
const fileId = (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
|
|
||||||
const storedFile = filesRef.current.get(id);
|
|
||||||
return storedFile &&
|
|
||||||
storedFile.name === file.name &&
|
|
||||||
storedFile.size === file.size &&
|
|
||||||
storedFile.lastModified === file.lastModified;
|
|
||||||
});
|
|
||||||
return fileId ? stateRef.current.pinnedFiles.has(fileId) : false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Stable signature for effects - prevents unnecessary re-renders
|
// Stable signature for effects - prevents unnecessary re-renders
|
||||||
|
@ -6,10 +6,8 @@ import { useToolState, type ProcessingProgress } from './useToolState';
|
|||||||
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
||||||
import { useToolResources } from './useToolResources';
|
import { useToolResources } from './useToolResources';
|
||||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||||
import { createOperation } from '../../../utils/toolOperationTracker';
|
import { FileWithId, extractFiles, FileId, FileRecord } from '../../../types/fileContext';
|
||||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
import { FileId } from '../../../types/file';
|
|
||||||
import { FileRecord } from '../../../types/fileContext';
|
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
export type { ProcessingProgress, ResponseHandler };
|
export type { ProcessingProgress, ResponseHandler };
|
||||||
@ -104,7 +102,7 @@ export interface ToolOperationHook<TParams = void> {
|
|||||||
progress: ProcessingProgress | null;
|
progress: ProcessingProgress | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
executeOperation: (params: TParams, selectedFiles: File[]) => Promise<void>;
|
executeOperation: (params: TParams, selectedFiles: FileWithId[]) => Promise<void>;
|
||||||
resetResults: () => void;
|
resetResults: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
cancelOperation: () => void;
|
cancelOperation: () => void;
|
||||||
@ -130,7 +128,7 @@ export const useToolOperation = <TParams>(
|
|||||||
config: ToolOperationConfig<TParams>
|
config: ToolOperationConfig<TParams>
|
||||||
): ToolOperationHook<TParams> => {
|
): ToolOperationHook<TParams> => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, undoConsumeFiles, findFileId, actions: fileActions, selectors } = useFileContext();
|
const { addFiles, consumeFiles, undoConsumeFiles, actions: fileActions, selectors } = useFileContext();
|
||||||
|
|
||||||
// Composed hooks
|
// Composed hooks
|
||||||
const { state, actions } = useToolState();
|
const { state, actions } = useToolState();
|
||||||
@ -146,7 +144,7 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
const executeOperation = useCallback(async (
|
const executeOperation = useCallback(async (
|
||||||
params: TParams,
|
params: TParams,
|
||||||
selectedFiles: File[]
|
selectedFiles: FileWithId[]
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Validation
|
// Validation
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
@ -160,9 +158,6 @@ export const useToolOperation = <TParams>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup operation tracking
|
|
||||||
const { operation, operationId, fileId } = createOperation(config.operationType, params, selectedFiles);
|
|
||||||
recordOperation(fileId, operation);
|
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
actions.setLoading(true);
|
actions.setLoading(true);
|
||||||
@ -173,6 +168,9 @@ export const useToolOperation = <TParams>(
|
|||||||
try {
|
try {
|
||||||
let processedFiles: File[];
|
let processedFiles: File[];
|
||||||
|
|
||||||
|
// Convert FileWithId to regular File objects for API processing
|
||||||
|
const validRegularFiles = extractFiles(validFiles);
|
||||||
|
|
||||||
switch (config.toolType) {
|
switch (config.toolType) {
|
||||||
case ToolType.singleFile:
|
case ToolType.singleFile:
|
||||||
// Individual file processing - separate API call per file
|
// Individual file processing - separate API call per file
|
||||||
@ -184,7 +182,7 @@ export const useToolOperation = <TParams>(
|
|||||||
};
|
};
|
||||||
processedFiles = await processFiles(
|
processedFiles = await processFiles(
|
||||||
params,
|
params,
|
||||||
validFiles,
|
validRegularFiles,
|
||||||
apiCallsConfig,
|
apiCallsConfig,
|
||||||
actions.setProgress,
|
actions.setProgress,
|
||||||
actions.setStatus
|
actions.setStatus
|
||||||
@ -194,7 +192,7 @@ export const useToolOperation = <TParams>(
|
|||||||
case ToolType.multiFile:
|
case ToolType.multiFile:
|
||||||
// Multi-file processing - single API call with all files
|
// Multi-file processing - single API call with all files
|
||||||
actions.setStatus('Processing files...');
|
actions.setStatus('Processing files...');
|
||||||
const formData = config.buildFormData(params, validFiles);
|
const formData = config.buildFormData(params, validRegularFiles);
|
||||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||||
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||||
@ -202,11 +200,11 @@ export const useToolOperation = <TParams>(
|
|||||||
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||||
if (config.responseHandler) {
|
if (config.responseHandler) {
|
||||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
processedFiles = await config.responseHandler(response.data, validRegularFiles);
|
||||||
} else if (response.data.type === 'application/pdf' ||
|
} else if (response.data.type === 'application/pdf' ||
|
||||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||||
// Single PDF response (e.g. split with merge option) - use original filename
|
// Single PDF response (e.g. split with merge option) - use original filename
|
||||||
const originalFileName = validFiles[0]?.name || 'document.pdf';
|
const originalFileName = validRegularFiles[0]?.name || 'document.pdf';
|
||||||
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||||
processedFiles = [singleFile];
|
processedFiles = [singleFile];
|
||||||
} else {
|
} else {
|
||||||
@ -222,7 +220,7 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
case ToolType.custom:
|
case ToolType.custom:
|
||||||
actions.setStatus('Processing files...');
|
actions.setStatus('Processing files...');
|
||||||
processedFiles = await config.customProcessor(params, validFiles);
|
processedFiles = await config.customProcessor(params, validRegularFiles);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,17 +244,13 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
// Build parallel arrays of IDs and records for undo tracking
|
// Build parallel arrays of IDs and records for undo tracking
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
const fileId = findFileId(file);
|
const fileId = file.fileId;
|
||||||
if (fileId) {
|
const record = selectors.getFileRecord(fileId);
|
||||||
const record = selectors.getFileRecord(fileId);
|
if (record) {
|
||||||
if (record) {
|
inputFileIds.push(fileId);
|
||||||
inputFileIds.push(fileId);
|
inputFileRecords.push(record);
|
||||||
inputFileRecords.push(record);
|
|
||||||
} else {
|
|
||||||
console.warn(`No file record found for file: ${file.name}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`No file ID found for file: ${file.name}`);
|
console.warn(`No file record found for file: ${file.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,24 +258,22 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||||
lastOperationRef.current = {
|
lastOperationRef.current = {
|
||||||
inputFiles: validFiles, // Keep original File objects for undo
|
inputFiles: extractFiles(validFiles), // Convert to File objects for undo
|
||||||
inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues
|
inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues
|
||||||
outputFileIds
|
outputFileIds
|
||||||
};
|
};
|
||||||
|
|
||||||
markOperationApplied(fileId, operationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
|
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
|
||||||
actions.setError(errorMessage);
|
actions.setError(errorMessage);
|
||||||
actions.setStatus('');
|
actions.setStatus('');
|
||||||
markOperationFailed(fileId, operationId, errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
actions.setLoading(false);
|
actions.setLoading(false);
|
||||||
actions.setProgress(null);
|
actions.setProgress(null);
|
||||||
}
|
}
|
||||||
}, [t, config, actions, recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, findFileId, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]);
|
}, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]);
|
||||||
|
|
||||||
const cancelOperation = useCallback(() => {
|
const cancelOperation = useCallback(() => {
|
||||||
cancelApiCalls();
|
cancelApiCalls();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
||||||
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
|
|
||||||
export function usePDFProcessor() {
|
export function usePDFProcessor() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -75,7 +76,7 @@ export function usePDFProcessor() {
|
|||||||
// Create pages without thumbnails initially - load them lazily
|
// Create pages without thumbnails initially - load them lazily
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
originalPageNumber: i,
|
originalPageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
// Request queue to handle concurrent thumbnail requests
|
// Request queue to handle concurrent thumbnail requests
|
||||||
@ -71,8 +72,8 @@ async function processRequestQueue() {
|
|||||||
|
|
||||||
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
|
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
|
||||||
|
|
||||||
// Use file name as fileId for PDF document caching
|
// Use quickKey for PDF document caching (same metadata, consistent format)
|
||||||
const fileId = file.name + '_' + file.size + '_' + file.lastModified as FileId;
|
const fileId = createQuickKey(file) as FileId;
|
||||||
|
|
||||||
const results = await thumbnailGenerationService.generateThumbnails(
|
const results = await thumbnailGenerationService.generateThumbnails(
|
||||||
fileId,
|
fileId,
|
||||||
|
@ -5,6 +5,7 @@ import { FileHasher } from '../utils/fileHash';
|
|||||||
import { FileAnalyzer } from './fileAnalyzer';
|
import { FileAnalyzer } from './fileAnalyzer';
|
||||||
import { ProcessingErrorHandler } from './processingErrorHandler';
|
import { ProcessingErrorHandler } from './processingErrorHandler';
|
||||||
import { pdfWorkerManager } from './pdfWorkerManager';
|
import { pdfWorkerManager } from './pdfWorkerManager';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
|
|
||||||
export class EnhancedPDFProcessingService {
|
export class EnhancedPDFProcessingService {
|
||||||
private static instance: EnhancedPDFProcessingService;
|
private static instance: EnhancedPDFProcessingService;
|
||||||
@ -201,7 +202,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -251,7 +252,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -266,7 +267,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholder pages for remaining pages
|
// Create placeholder pages for remaining pages
|
||||||
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -313,7 +314,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -334,7 +335,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholders for remaining pages
|
// Create placeholders for remaining pages
|
||||||
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -368,7 +369,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const pages: PDFPage[] = [];
|
const pages: PDFPage[] = [];
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ProcessedFile, ProcessingState, PDFPage } from '../types/processing';
|
import { ProcessedFile, ProcessingState, PDFPage } from '../types/processing';
|
||||||
import { ProcessingCache } from './processingCache';
|
import { ProcessingCache } from './processingCache';
|
||||||
import { pdfWorkerManager } from './pdfWorkerManager';
|
import { pdfWorkerManager } from './pdfWorkerManager';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
|
|
||||||
export class PDFProcessingService {
|
export class PDFProcessingService {
|
||||||
private static instance: PDFProcessingService;
|
private static instance: PDFProcessingService;
|
||||||
@ -113,7 +114,7 @@ export class PDFProcessingService {
|
|||||||
const thumbnail = canvas.toDataURL();
|
const thumbnail = canvas.toDataURL();
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
@ -18,6 +18,8 @@ import { FileContextProvider } from '../../contexts/FileContext';
|
|||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../../i18n/config';
|
import i18n from '../../i18n/config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { createTestFileWithId } from '../utils/testFileHelpers';
|
||||||
|
import { FileWithId } from '../../types/fileContext';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
@ -55,9 +57,9 @@ const createTestFile = (name: string, content: string, type: string): File => {
|
|||||||
return new File([content], name, { type });
|
return new File([content], name, { type });
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPDFFile = (): File => {
|
const createPDFFile = (): FileWithId => {
|
||||||
const pdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\ntrailer\n<<\n/Size 2\n/Root 1 0 R\n>>\nstartxref\n0\n%%EOF';
|
const pdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\ntrailer\n<<\n/Size 2\n/Root 1 0 R\n>>\nstartxref\n0\n%%EOF';
|
||||||
return createTestFile('test.pdf', pdfContent, 'application/pdf');
|
return createTestFileWithId('test.pdf', pdfContent, 'application/pdf');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test wrapper component
|
// Test wrapper component
|
||||||
@ -162,7 +164,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
});
|
});
|
||||||
|
|
||||||
const testFile = createTestFile('invalid.txt', 'not a pdf', 'text/plain');
|
const testFile = createTestFileWithId('invalid.txt', 'not a pdf', 'text/plain');
|
||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
toExtension: 'png',
|
||||||
@ -426,7 +428,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
const files = [
|
const files = [
|
||||||
createPDFFile(),
|
createPDFFile(),
|
||||||
createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
|
createTestFileWithId('test2.pdf', '%PDF-1.4...', 'application/pdf')
|
||||||
]
|
]
|
||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
@ -527,7 +529,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
});
|
});
|
||||||
|
|
||||||
const corruptedFile = createTestFile('corrupted.pdf', 'not-a-pdf', 'application/pdf');
|
const corruptedFile = createTestFileWithId('corrupted.pdf', 'not-a-pdf', 'application/pdf');
|
||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
toExtension: 'png',
|
||||||
|
28
frontend/src/tests/utils/testFileHelpers.ts
Normal file
28
frontend/src/tests/utils/testFileHelpers.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Test utilities for creating FileWithId objects in tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileWithId, createFileWithId } from '../../types/fileContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a FileWithId object for testing purposes
|
||||||
|
*/
|
||||||
|
export function createTestFileWithId(
|
||||||
|
name: string,
|
||||||
|
content: string = 'test content',
|
||||||
|
type: string = 'application/pdf'
|
||||||
|
): FileWithId {
|
||||||
|
const file = new File([content], name, { type });
|
||||||
|
return createFileWithId(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create multiple FileWithId objects for testing
|
||||||
|
*/
|
||||||
|
export function createTestFilesWithId(
|
||||||
|
files: Array<{ name: string; content?: string; type?: string }>
|
||||||
|
): FileWithId[] {
|
||||||
|
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
|
||||||
|
createTestFileWithId(name, content, type)
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,9 @@
|
|||||||
import { PageOperation } from './pageEditor';
|
import { PageOperation } from './pageEditor';
|
||||||
import { FileId, FileMetadata } from './file';
|
import { FileId, FileMetadata } from './file';
|
||||||
|
|
||||||
|
// Re-export FileId for convenience
|
||||||
|
export type { FileId };
|
||||||
|
|
||||||
export type ModeType =
|
export type ModeType =
|
||||||
| 'viewer'
|
| 'viewer'
|
||||||
| 'pageEditor'
|
| 'pageEditor'
|
||||||
@ -82,6 +85,67 @@ export function createQuickKey(file: File): string {
|
|||||||
return `${file.name}|${file.size}|${file.lastModified}`;
|
return `${file.name}|${file.size}|${file.lastModified}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File with embedded UUID - replaces loose File + FileId parameter passing
|
||||||
|
export interface FileWithId extends File {
|
||||||
|
readonly fileId: FileId;
|
||||||
|
readonly quickKey: string; // Fast deduplication key: name|size|lastModified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to check if a File object has an embedded fileId
|
||||||
|
export function isFileWithId(file: File): file is FileWithId {
|
||||||
|
return 'fileId' in file && typeof (file as any).fileId === 'string' &&
|
||||||
|
'quickKey' in file && typeof (file as any).quickKey === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a FileWithId from a regular File object
|
||||||
|
export function createFileWithId(file: File, id?: FileId): FileWithId {
|
||||||
|
const fileId = id || createFileId();
|
||||||
|
const quickKey = createQuickKey(file);
|
||||||
|
|
||||||
|
// File properties are not enumerable, so we need to copy them explicitly
|
||||||
|
// This avoids prototype chain issues while preserving all File functionality
|
||||||
|
const fileWithId = {
|
||||||
|
// Explicitly copy File properties (they're not enumerable)
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
lastModified: file.lastModified,
|
||||||
|
webkitRelativePath: file.webkitRelativePath,
|
||||||
|
|
||||||
|
// Add our custom properties
|
||||||
|
fileId: fileId,
|
||||||
|
quickKey: quickKey,
|
||||||
|
|
||||||
|
// Preserve File prototype methods by binding them to the original file
|
||||||
|
arrayBuffer: file.arrayBuffer.bind(file),
|
||||||
|
slice: file.slice.bind(file),
|
||||||
|
stream: file.stream.bind(file),
|
||||||
|
text: file.text.bind(file)
|
||||||
|
} as FileWithId;
|
||||||
|
|
||||||
|
return fileWithId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract FileIds from FileWithId array
|
||||||
|
export function extractFileIds(files: FileWithId[]): FileId[] {
|
||||||
|
return files.map(file => file.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract regular File objects from FileWithId array
|
||||||
|
export function extractFiles(files: FileWithId[]): File[] {
|
||||||
|
return files as File[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an object is a File or FileWithId (replaces instanceof File checks)
|
||||||
|
export function isFileObject(obj: any): obj is File | FileWithId {
|
||||||
|
return obj &&
|
||||||
|
typeof obj.name === 'string' &&
|
||||||
|
typeof obj.size === 'number' &&
|
||||||
|
typeof obj.type === 'string' &&
|
||||||
|
typeof obj.lastModified === 'number' &&
|
||||||
|
typeof obj.arrayBuffer === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function toFileRecord(file: File, id?: FileId): FileRecord {
|
export function toFileRecord(file: File, id?: FileId): FileRecord {
|
||||||
@ -215,18 +279,18 @@ export type FileContextAction =
|
|||||||
|
|
||||||
export interface FileContextActions {
|
export interface FileContextActions {
|
||||||
// File management - lightweight actions only
|
// File management - lightweight actions only
|
||||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<File[]>;
|
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<FileWithId[]>;
|
||||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
|
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<FileWithId[]>;
|
||||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<File[]>;
|
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<FileWithId[]>;
|
||||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
||||||
reorderFiles: (orderedFileIds: FileId[]) => void;
|
reorderFiles: (orderedFileIds: FileId[]) => void;
|
||||||
clearAllFiles: () => Promise<void>;
|
clearAllFiles: () => Promise<void>;
|
||||||
clearAllData: () => Promise<void>;
|
clearAllData: () => Promise<void>;
|
||||||
|
|
||||||
// File pinning
|
// File pinning - accepts FileWithId for safer type checking
|
||||||
pinFile: (file: File) => void;
|
pinFile: (file: FileWithId) => void;
|
||||||
unpinFile: (file: File) => void;
|
unpinFile: (file: FileWithId) => void;
|
||||||
|
|
||||||
// File consumption (replace unpinned files with outputs)
|
// File consumption (replace unpinned files with outputs)
|
||||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
||||||
@ -253,26 +317,17 @@ export interface FileContextActions {
|
|||||||
|
|
||||||
// File selectors (separate from actions to avoid re-renders)
|
// File selectors (separate from actions to avoid re-renders)
|
||||||
export interface FileContextSelectors {
|
export interface FileContextSelectors {
|
||||||
// File access - no state dependency, uses ref
|
getFile: (id: FileId) => FileWithId | undefined;
|
||||||
getFile: (id: FileId) => File | undefined;
|
getFiles: (ids?: FileId[]) => FileWithId[];
|
||||||
getFiles: (ids?: FileId[]) => File[];
|
|
||||||
|
|
||||||
// Record access - uses normalized state
|
|
||||||
getFileRecord: (id: FileId) => FileRecord | undefined;
|
getFileRecord: (id: FileId) => FileRecord | undefined;
|
||||||
getFileRecords: (ids?: FileId[]) => FileRecord[];
|
getFileRecords: (ids?: FileId[]) => FileRecord[];
|
||||||
|
|
||||||
// Derived selectors
|
|
||||||
getAllFileIds: () => FileId[];
|
getAllFileIds: () => FileId[];
|
||||||
getSelectedFiles: () => File[];
|
getSelectedFiles: () => FileWithId[];
|
||||||
getSelectedFileRecords: () => FileRecord[];
|
getSelectedFileRecords: () => FileRecord[];
|
||||||
|
|
||||||
// Pinned files selectors
|
|
||||||
getPinnedFileIds: () => FileId[];
|
getPinnedFileIds: () => FileId[];
|
||||||
getPinnedFiles: () => File[];
|
getPinnedFiles: () => FileWithId[];
|
||||||
getPinnedFileRecords: () => FileRecord[];
|
getPinnedFileRecords: () => FileRecord[];
|
||||||
isFilePinned: (file: File) => boolean;
|
isFilePinned: (file: FileWithId) => boolean;
|
||||||
|
|
||||||
// Stable signature for effect dependencies
|
|
||||||
getFilesSignature: () => string;
|
getFilesSignature: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user