diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index f2f76923e..cc76c74bf 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -6,6 +6,7 @@ import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; import UploadFileIcon from '@mui/icons-material/UploadFile'; import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext'; +import { FileId } from '../../types/fileContext'; import { useNavigationActions } from '../../contexts/NavigationContext'; import { fileStorage } from '../../services/fileStorage'; import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; @@ -156,26 +157,6 @@ const FileEditor = ({ if (extractionResult.success) { allExtractedFiles.push(...extractionResult.extractedFiles); - - // Record ZIP extraction operation - const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const operation: FileOperation = { - id: operationId, - type: 'convert', - timestamp: Date.now(), - fileIds: extractionResult.extractedFiles.map(f => f.name), - status: 'pending', - metadata: { - originalFileName: file.name, - outputFileNames: extractionResult.extractedFiles.map(f => f.name), - fileSize: file.size, - parameters: { - extractionType: 'zip', - extractedCount: extractionResult.extractedCount, - totalFiles: extractionResult.totalFiles - } - } - }; if (extractionResult.errors.length > 0) { errors.push(...extractionResult.errors); @@ -278,7 +259,7 @@ const FileEditor = ({ } // Update context (this automatically updates tool selection since they use the same action) - setSelectedFiles(newSelection); + setSelectedFiles(newSelection.map(id => id as FileId)); }, [setSelectedFiles, toolMode, setStatus, activeFileRecords]); const toggleSelectionMode = useCallback(() => { @@ -306,7 +287,7 @@ const FileEditor = ({ // Handle multi-file selection reordering const filesToMove = selectedFileIds.length > 1 - ? selectedFileIds.filter(id => currentIds.includes(id)) + ? selectedFileIds.filter(id => currentIds.includes(id as any)) : [sourceFileId]; // Create new order @@ -337,7 +318,7 @@ const FileEditor = ({ } // Insert files at the calculated position - newOrder.splice(insertIndex, 0, ...filesToMove); + newOrder.splice(insertIndex, 0, ...filesToMove.map(id => id as any)); // Update file order reorderFiles(newOrder); @@ -355,31 +336,11 @@ const FileEditor = ({ const file = record ? selectors.getFile(record.id) : null; if (record && file) { - // Record close operation - const fileName = file.name; - const contextFileId = record.id; - const operationId = `close-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const operation: FileOperation = { - id: operationId, - type: 'remove', - timestamp: Date.now(), - fileIds: [fileName], - status: 'pending', - metadata: { - originalFileName: fileName, - fileSize: record.size, - parameters: { - action: 'close', - reason: 'user_request' - } - } - }; - // Remove file from context but keep in storage (close, don't delete) - removeFiles([contextFileId], false); + removeFiles([record.id], false); // Remove from context selections - const currentSelected = selectedFileIds.filter(id => id !== contextFileId); + const currentSelected = selectedFileIds.filter(id => id !== record.id); setSelectedFiles(currentSelected); } }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]); @@ -388,7 +349,7 @@ const FileEditor = ({ const record = activeFileRecords.find(r => r.id === fileId); if (record) { // Set the file as selected in context and switch to viewer for preview - setSelectedFiles([fileId]); + setSelectedFiles([fileId as FileId]); navActions.setMode('viewer'); } }, [activeFileRecords, setSelectedFiles, navActions.setMode]); @@ -405,7 +366,7 @@ const FileEditor = ({ }, [activeFileRecords, selectors, onMergeFiles]); const handleSplitFile = useCallback((fileId: string) => { - const file = selectors.getFile(fileId); + const file = selectors.getFile(fileId as FileId); if (file && onOpenPageEditor) { onOpenPageEditor(file); } diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx index 2e441a882..bb6ed57f9 100644 --- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx @@ -60,8 +60,8 @@ const FileEditorThumbnail = ({ // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { - return activeFiles.find((f: File) => f.name === file.name && f.size === file.size); - }, [activeFiles, file.name, file.size]); + return activeFiles.find(f => f.fileId === file.id); + }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; const downloadSelectedFile = useCallback(() => { diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index a0a7d1795..35864f3ef 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -60,8 +60,8 @@ const FileThumbnail = ({ // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { - return activeFiles.find((f: File) => f.name === file.name && f.size === file.size); - }, [activeFiles, file.name, file.size]); + return activeFiles.find(f => f.fileId === file.id); + }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; const downloadSelectedFile = useCallback(() => { diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts index 5a9d13f9f..48b5e25a9 100644 --- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { useFileState } from '../../../contexts/FileContext'; import { PDFDocument, PDFPage } from '../../../types/pageEditor'; +import { FileId } from '../../../types/fileContext'; export interface PageDocumentHook { document: PDFDocument | null; @@ -70,7 +71,7 @@ export function usePageDocument(): PageDocumentHook { let totalPageCount = 0; // Helper function to create pages from a file - const createPagesFromFile = (fileId: string, startPageNumber: number): PDFPage[] => { + const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => { const fileRecord = selectors.getFileRecord(fileId); if (!fileRecord) { return []; @@ -111,7 +112,7 @@ export function usePageDocument(): PageDocumentHook { // Collect all pages from original files (without renumbering yet) const originalFilePages: PDFPage[] = []; originalFileIds.forEach(fileId => { - const filePages = createPagesFromFile(fileId, 1); // Temporary numbering + const filePages = createPagesFromFile(fileId as FileId, 1); // Temporary numbering originalFilePages.push(...filePages); }); @@ -130,7 +131,7 @@ export function usePageDocument(): PageDocumentHook { // Collect all pages to insert const allNewPages: PDFPage[] = []; fileIds.forEach(fileId => { - const insertedPages = createPagesFromFile(fileId, 1); + const insertedPages = createPagesFromFile(fileId as FileId, 1); allNewPages.push(...insertedPages); }); diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index ea0e70818..22d8382e5 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -123,10 +123,9 @@ const FileGrid = ({ style={{ overflowY: "auto", width: "100%" }} > {displayFiles.map((item, idx) => { - // Use record ID if available, otherwise throw error for missing FileRecord if (!item.record?.id) { console.error('FileGrid: File missing FileRecord with proper ID:', item.file.name); - return null; // Skip rendering files without proper IDs + return null; } const fileId = item.record.id; const originalIdx = files.findIndex(f => f.record?.id === fileId); diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx index bec6b272f..795e07472 100644 --- a/frontend/src/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertSettings.tsx @@ -22,12 +22,13 @@ import { OUTPUT_OPTIONS, FIT_OPTIONS } from "../../../constants/convertConstants"; +import { FileWithId } from "../../../types/fileContext"; interface ConvertSettingsProps { parameters: ConvertParameters; onParameterChange: (key: keyof ConvertParameters, value: any) => void; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; - selectedFiles: File[]; + selectedFiles: FileWithId[]; disabled?: boolean; } @@ -128,7 +129,7 @@ const ConvertSettings = ({ }; const filterFilesByExtension = (extension: string) => { - const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as File[]; + const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as FileWithId[]; return files.filter(file => { const fileExtension = detectFileExtension(file.name); @@ -142,21 +143,8 @@ const ConvertSettings = ({ }); }; - const updateFileSelection = (files: File[]) => { - // Map File objects to their actual IDs in FileContext - const fileIds = files.map(file => { - // Find the file ID by matching file properties - const fileRecord = state.files.ids - .map(id => selectors.getFileRecord(id)) - .find(record => - record && - record.name === file.name && - record.size === file.size && - record.lastModified === file.lastModified - ); - return fileRecord?.id; - }).filter((id): id is string => id !== undefined); // Type guard to ensure only strings - + const updateFileSelection = (files: FileWithId[]) => { + const fileIds = files.map(file => file.fileId); setSelectedFiles(fileIds); }; diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx index e1a662bd2..ccad7ce67 100644 --- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx @@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters'; import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection'; +import { FileWithId } from '../../../types/fileContext'; interface ConvertToPdfaSettingsProps { parameters: ConvertParameters; onParameterChange: (key: keyof ConvertParameters, value: any) => void; - selectedFiles: File[]; + selectedFiles: FileWithId[]; disabled?: boolean; } diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 28b8bf428..d8d8b6f51 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload'; import { useFilesModalContext } from "../../../contexts/FilesModalContext"; import { useAllFiles } from "../../../contexts/FileContext"; import { useFileManager } from "../../../hooks/useFileManager"; +import { FileWithId } from "../../../types/fileContext"; export interface FileStatusIndicatorProps { - selectedFiles?: File[]; + selectedFiles?: FileWithId[]; placeholder?: string; } diff --git a/frontend/src/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx index b062e9c02..11f7d4e02 100644 --- a/frontend/src/components/tools/shared/FilesToolStep.tsx +++ b/frontend/src/components/tools/shared/FilesToolStep.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import FileStatusIndicator from './FileStatusIndicator'; +import { FileWithId } from '../../../types/fileContext'; export interface FilesToolStepProps { - selectedFiles: File[]; + selectedFiles: FileWithId[]; isCollapsed?: boolean; onCollapsedClick?: () => void; placeholder?: string; diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index da2503b4e..4662f8eeb 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep'; import OperationButton from './OperationButton'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; +import { FileWithId } from '../../../types/fileContext'; export interface FilesStepConfig { - selectedFiles: File[]; + selectedFiles: FileWithId[]; isCollapsed?: boolean; placeholder?: string; onCollapsedClick?: () => void; diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index cdf831c06..f03d16d00 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -15,6 +15,7 @@ import { fileStorage } from "../../services/fileStorage"; import SkeletonLoader from '../shared/SkeletonLoader'; import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext"; import { useFileWithUrl } from "../../hooks/useFileWithUrl"; +import { isFileObject } from "../../types/fileContext"; // Lazy loading page image component @@ -200,7 +201,7 @@ const Viewer = ({ const effectiveFile = React.useMemo(() => { if (previewFile) { // Validate the preview file - if (!(previewFile instanceof File)) { + if (!isFileObject(previewFile)) { return null; } diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 9dc893d4d..3f157fe67 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -89,19 +89,16 @@ function FileContextInner({ })); } - // Convert to FileWithId objects return addedFilesWithIds.map(({ file, id }) => createFileWithId(file, id)); }, [indexedDB, enablePersistence]); const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => { const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager); - // Convert to FileWithId objects return result.map(({ file, id }) => createFileWithId(file, id)); }, []); const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise => { const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager); - // Convert to FileWithId objects return result.map(({ file, id }) => createFileWithId(file, id)); }, []); @@ -111,11 +108,9 @@ function FileContextInner({ // Helper functions for pinned files const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => { const result = await consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch); - // Convert results to FileWithId objects return result.map(({ file, id }) => createFileWithId(file, id)); }, []); - // File pinning functions - now use FileWithId directly const pinFileWrapper = useCallback((file: FileWithId) => { baseActions.pinFile(file.fileId); }, [baseActions]); diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index a623e1901..84fd732f6 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -361,7 +361,6 @@ export async function consumeFiles( }) ); - // Extract records for dispatch const outputFileRecords = processedOutputs.map(({ record }) => record); // Dispatch the consume action @@ -375,7 +374,6 @@ export async function consumeFiles( if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputFileRecords.length} outputs`); - // Return file data for FileWithId conversion return processedOutputs.map(({ file, id, thumbnail }) => ({ file, id, thumbnail })); } diff --git a/frontend/src/contexts/file/fileHooks.ts b/frontend/src/contexts/file/fileHooks.ts index 712a96e99..61022c92c 100644 --- a/frontend/src/contexts/file/fileHooks.ts +++ b/frontend/src/contexts/file/fileHooks.ts @@ -9,7 +9,7 @@ import { FileContextStateValue, FileContextActionsValue } from './contexts'; -import { FileId, FileRecord } from '../../types/fileContext'; +import { FileId, FileRecord, FileWithId } from '../../types/fileContext'; /** * Hook for accessing file state (will re-render on any state change) @@ -122,7 +122,7 @@ export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecor /** * 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(); return useMemo(() => ({ @@ -135,7 +135,7 @@ export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: /** * 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(); return useMemo(() => ({ @@ -164,11 +164,6 @@ export function useFileContext() { // File management addFiles: actions.addFiles, consumeFiles: actions.consumeFiles, - recordOperation: (fileId: string, operation: any) => {}, // Operation tracking not implemented - markOperationApplied: (fileId: string, operationId: string) => {}, // Operation tracking not implemented - markOperationFailed: (fileId: string, operationId: string, error: string) => {}, // Operation tracking not implemented - - // File ID lookup removed - use FileWithId.fileId directly for better performance and type safety // Pinned files pinnedFiles: state.pinnedFiles, diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 99b46495f..b3d6a5cad 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -35,10 +35,10 @@ export class FileLifecycleManager { */ cleanupFile = (fileId: string, stateRef?: React.MutableRefObject): void => { // Use comprehensive cleanup (same as removeFiles) - this.cleanupAllResourcesForFile(fileId, stateRef); + this.cleanupAllResourcesForFile(fileId as FileId, stateRef); // Remove file from state - this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds: [fileId] } }); + this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds: [fileId as FileId] } }); }; /** diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index a31d802e4..ae1e2ef26 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -6,7 +6,6 @@ import { useToolState, type ProcessingProgress } from './useToolState'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolResources } from './useToolResources'; import { extractErrorMessage } from '../../../utils/toolErrorHandler'; -import { createOperation } from '../../../utils/toolOperationTracker'; import { FileWithId, extractFiles } from '../../../types/fileContext'; import { ResponseHandler } from '../../../utils/toolResponseProcessor'; @@ -108,7 +107,7 @@ export const useToolOperation = ( config: ToolOperationConfig ): ToolOperationHook => { const { t } = useTranslation(); - const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles } = useFileContext(); + const { addFiles, consumeFiles } = useFileContext(); // Composed hooks const { state, actions } = useToolState(); @@ -131,9 +130,6 @@ export const useToolOperation = ( return; } - // Setup operation tracking with proper FileWithId - const { operation, operationId, fileId } = createOperation(config.operationType, params, selectedFiles); - recordOperation(fileId, operation); // Reset state actions.setLoading(true); @@ -144,7 +140,6 @@ export const useToolOperation = ( try { let processedFiles: File[]; - // Convert FileWithId to regular File objects for API processing const validRegularFiles = extractFiles(validFiles); if (config.customProcessor) { @@ -214,20 +209,17 @@ export const useToolOperation = ( // Replace input files with processed files (consumeFiles handles pinning) const inputFileIds = validFiles.map(file => file.fileId); await consumeFiles(inputFileIds, processedFiles); - - markOperationApplied(fileId, operationId); } } catch (error: any) { const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error); actions.setError(errorMessage); actions.setStatus(''); - markOperationFailed(fileId, operationId, errorMessage); } finally { actions.setLoading(false); actions.setProgress(null); } - }, [t, config, actions, recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]); + }, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]); const cancelOperation = useCallback(() => { cancelApiCalls(); diff --git a/frontend/src/hooks/useFileHandler.ts b/frontend/src/hooks/useFileHandler.ts index b42b4e94c..acfad9674 100644 --- a/frontend/src/hooks/useFileHandler.ts +++ b/frontend/src/hooks/useFileHandler.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { useFileState, useFileActions } from '../contexts/FileContext'; import { FileMetadata } from '../types/file'; +import { FileId } from '../types/fileContext'; export const useFileHandler = () => { const { state } = useFileState(); // Still needed for addStoredFiles @@ -20,11 +21,15 @@ export const useFileHandler = () => { const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => { // Filter out files that already exist with the same ID (exact match) const newFiles = filesWithMetadata.filter(({ originalId }) => { - return state.files.byId[originalId] === undefined; + return state.files.byId[originalId as FileId] === undefined; }); if (newFiles.length > 0) { - await actions.addStoredFiles(newFiles); + await actions.addStoredFiles(newFiles.map(({file, originalId, metadata}) => ({ + file, + originalId: originalId as FileId, + metadata + }))); } console.log(`📁 Added ${newFiles.length} stored files (${filesWithMetadata.length - newFiles.length} skipped as duplicates)`); diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index 59e8c1cdb..05816efd3 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react'; import { useIndexedDB } from '../contexts/IndexedDBContext'; import { FileMetadata } from '../types/file'; import { generateThumbnailForFile } from '../utils/thumbnailUtils'; +import { FileId } from '../types/fileContext'; export const useFileManager = () => { const [loading, setLoading] = useState(false); @@ -46,7 +47,7 @@ export const useFileManager = () => { // Regular file loading if (fileMetadata.id) { - const file = await indexedDB.loadFile(fileMetadata.id); + const file = await indexedDB.loadFile(fileMetadata.id as FileId); if (file) { return file; } @@ -86,7 +87,7 @@ export const useFileManager = () => { throw new Error('IndexedDB context not available'); } try { - await indexedDB.deleteFile(file.id); + await indexedDB.deleteFile(file.id as FileId); setFiles(files.filter((_, i) => i !== index)); } catch (error) { console.error('Failed to remove file:', error); @@ -100,7 +101,7 @@ export const useFileManager = () => { } try { // Store file with provided UUID from FileContext (thumbnail generated internally) - const metadata = await indexedDB.saveFile(file, fileId); + const metadata = await indexedDB.saveFile(file, fileId as FileId); // Convert file to ArrayBuffer for StoredFile interface compatibility const arrayBuffer = await file.arrayBuffer(); @@ -176,7 +177,7 @@ export const useFileManager = () => { try { // Update access time - this will be handled by the cache in IndexedDBContext // when the file is loaded, so we can just load it briefly to "touch" it - await indexedDB.loadFile(id); + await indexedDB.loadFile(id as FileId); } catch (error) { console.error('Failed to touch file:', error); } diff --git a/frontend/src/hooks/useFileWithUrl.ts b/frontend/src/hooks/useFileWithUrl.ts index fd2f2d604..84c06baa2 100644 --- a/frontend/src/hooks/useFileWithUrl.ts +++ b/frontend/src/hooks/useFileWithUrl.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { isFileObject } from '../types/fileContext'; /** * Hook to convert a File object to { file: File; url: string } format @@ -8,8 +9,8 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u return useMemo(() => { if (!file) return null; - // Validate that file is a proper File or Blob object - if (!(file instanceof File) && !(file instanceof Blob)) { + // Validate that file is a proper File, FileWithId, or Blob object + if (!isFileObject(file) && !(file instanceof Blob)) { console.warn('useFileWithUrl: Expected File or Blob, got:', file); return null; } diff --git a/frontend/src/hooks/useIndexedDBThumbnail.ts b/frontend/src/hooks/useIndexedDBThumbnail.ts index 4f0d77c0e..a6251db3c 100644 --- a/frontend/src/hooks/useIndexedDBThumbnail.ts +++ b/frontend/src/hooks/useIndexedDBThumbnail.ts @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { FileMetadata } from "../types/file"; import { useIndexedDB } from "../contexts/IndexedDBContext"; import { generateThumbnailForFile } from "../utils/thumbnailUtils"; +import { FileId } from "../types/fileContext"; /** * Calculate optimal scale for thumbnail generation @@ -53,7 +54,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): { // Try to load file from IndexedDB using new context if (file.id && indexedDB) { - const loadedFile = await indexedDB.loadFile(file.id); + const loadedFile = await indexedDB.loadFile(file.id as FileId); if (!loadedFile) { throw new Error('File not found in IndexedDB'); } @@ -70,7 +71,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): { // Save thumbnail to IndexedDB for persistence if (file.id && indexedDB && thumbnail) { try { - await indexedDB.updateThumbnail(file.id, thumbnail); + await indexedDB.updateThumbnail(file.id as FileId, thumbnail); } catch (error) { console.warn('Failed to save thumbnail to IndexedDB:', error); } diff --git a/frontend/src/hooks/usePdfSignatureDetection.ts b/frontend/src/hooks/usePdfSignatureDetection.ts index 17f90f2d9..84075f972 100644 --- a/frontend/src/hooks/usePdfSignatureDetection.ts +++ b/frontend/src/hooks/usePdfSignatureDetection.ts @@ -1,13 +1,14 @@ import { useState, useEffect } from 'react'; import * as pdfjsLib from 'pdfjs-dist'; import { pdfWorkerManager } from '../services/pdfWorkerManager'; +import { FileWithId } from '../types/fileContext'; export interface PdfSignatureDetectionResult { hasDigitalSignatures: boolean; isChecking: boolean; } -export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionResult => { +export const usePdfSignatureDetection = (files: FileWithId[]): PdfSignatureDetectionResult => { const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false); const [isChecking, setIsChecking] = useState(false); diff --git a/frontend/src/hooks/useThumbnailGeneration.ts b/frontend/src/hooks/useThumbnailGeneration.ts index bbb599c3b..6b0b6856f 100644 --- a/frontend/src/hooks/useThumbnailGeneration.ts +++ b/frontend/src/hooks/useThumbnailGeneration.ts @@ -71,7 +71,6 @@ async function processRequestQueue() { console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`); - // Use quickKey for PDF document caching (same metadata, consistent format) const fileId = createQuickKey(file); const results = await thumbnailGenerationService.generateThumbnails( diff --git a/frontend/src/tests/convert/ConvertIntegration.test.tsx b/frontend/src/tests/convert/ConvertIntegration.test.tsx index 1ce180b9f..baa7f1216 100644 --- a/frontend/src/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertIntegration.test.tsx @@ -18,6 +18,8 @@ import { FileContextProvider } from '../../contexts/FileContext'; import { I18nextProvider } from 'react-i18next'; import i18n from '../../i18n/config'; import axios from 'axios'; +import { createTestFileWithId } from '../utils/testFileHelpers'; +import { FileWithId } from '../../types/fileContext'; // Mock axios vi.mock('axios'); @@ -55,9 +57,9 @@ const createTestFile = (name: string, content: string, type: string): File => { 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'; - return createTestFile('test.pdf', pdfContent, 'application/pdf'); + return createTestFileWithId('test.pdf', pdfContent, 'application/pdf'); }; // Test wrapper component @@ -162,7 +164,7 @@ describe('Convert Tool Integration Tests', () => { 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 = { fromExtension: 'pdf', toExtension: 'png', @@ -426,7 +428,7 @@ describe('Convert Tool Integration Tests', () => { }); const files = [ createPDFFile(), - createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf') + createTestFileWithId('test2.pdf', '%PDF-1.4...', 'application/pdf') ] const parameters: ConvertParameters = { fromExtension: 'pdf', @@ -527,7 +529,7 @@ describe('Convert Tool Integration Tests', () => { 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 = { fromExtension: 'pdf', toExtension: 'png', diff --git a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx index 4038df63b..7c160f197 100644 --- a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx @@ -14,6 +14,8 @@ import i18n from '../../i18n/config'; import axios from 'axios'; import { detectFileExtension } from '../../utils/fileUtils'; import { FIT_OPTIONS } from '../../constants/convertConstants'; +import { createTestFileWithId, createTestFilesWithId } from '../utils/testFileHelpers'; +import { FileWithId } from '../../types/fileContext'; // Mock axios vi.mock('axios'); @@ -81,7 +83,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock DOCX file - const docxFile = new File(['docx content'], 'document.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); + const docxFile = createTestFileWithId('document.docx', 'docx content', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); // Test auto-detection act(() => { @@ -117,7 +119,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock unknown file - const unknownFile = new File(['unknown content'], 'document.xyz', { type: 'application/octet-stream' }); + const unknownFile = createTestFileWithId('document.xyz', 'unknown content', 'application/octet-stream'); // Test auto-detection act(() => { @@ -156,11 +158,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock image files - const imageFiles = [ - new File(['jpg content'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['png content'], 'photo2.png', { type: 'image/png' }), - new File(['gif content'], 'photo3.gif', { type: 'image/gif' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg content', type: 'image/jpeg' }, + { name: 'photo2.png', content: 'png content', type: 'image/png' }, + { name: 'photo3.gif', content: 'gif content', type: 'image/gif' } + ]); // Test smart detection for all images act(() => { @@ -202,11 +204,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mixed file types - const mixedFiles = [ - new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }), - new File(['docx content'], 'spreadsheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), - new File(['pptx content'], 'presentation.pptx', { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }) - ]; + const mixedFiles = createTestFilesWithId([ + { name: 'document.pdf', content: 'pdf content', type: 'application/pdf' }, + { name: 'spreadsheet.xlsx', content: 'docx content', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + { name: 'presentation.pptx', content: 'pptx content', type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' } + ]); // Test smart detection for mixed types act(() => { @@ -243,10 +245,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock web files - const webFiles = [ - new File(['content'], 'page1.html', { type: 'text/html' }), - new File(['zip content'], 'site.zip', { type: 'application/zip' }) - ]; + const webFiles = createTestFilesWithId([ + { name: 'page1.html', content: 'content', type: 'text/html' }, + { name: 'site.zip', content: 'zip content', type: 'application/zip' } + ]); // Test smart detection for web files act(() => { @@ -288,7 +290,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const htmlFile = new File(['content'], 'page.html', { type: 'text/html' }); + const htmlFile = createTestFileWithId('page.html', 'content', 'text/html'); // Set up HTML conversion parameters act(() => { @@ -318,7 +320,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const emlFile = new File(['email content'], 'email.eml', { type: 'message/rfc822' }); + const emlFile = createTestFileWithId('email.eml', 'email content', 'message/rfc822'); // Set up email conversion parameters act(() => { @@ -355,7 +357,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const pdfFile = new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }); + const pdfFile = createTestFileWithId('document.pdf', 'pdf content', 'application/pdf'); // Set up PDF/A conversion parameters act(() => { @@ -392,10 +394,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const imageFiles = [ - new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' }, + { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' } + ]); // Set up image conversion parameters act(() => { @@ -432,10 +434,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const imageFiles = [ - new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' }, + { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' } + ]); // Set up for separate processing act(() => { @@ -477,10 +479,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }) .mockRejectedValueOnce(new Error('File 2 failed')); - const mixedFiles = [ - new File(['file1'], 'doc1.txt', { type: 'text/plain' }), - new File(['file2'], 'doc2.xyz', { type: 'application/octet-stream' }) - ]; + const mixedFiles = createTestFilesWithId([ + { name: 'doc1.txt', content: 'file1', type: 'text/plain' }, + { name: 'doc2.xyz', content: 'file2', type: 'application/octet-stream' } + ]); // Set up for separate processing (mixed smart detection) act(() => { diff --git a/frontend/src/tests/utils/testFileHelpers.ts b/frontend/src/tests/utils/testFileHelpers.ts new file mode 100644 index 000000000..be8cd9774 --- /dev/null +++ b/frontend/src/tests/utils/testFileHelpers.ts @@ -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) + ); +} \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index aa12cbad7..103fae088 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -69,14 +69,14 @@ export interface FileContextNormalizedFiles { export function createFileId(): FileId { // Use crypto.randomUUID for authoritative primary key if (typeof window !== 'undefined' && window.crypto?.randomUUID) { - return window.crypto.randomUUID(); + return window.crypto.randomUUID() as FileId; } // Fallback for environments without randomUUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); - }); + }) as FileId; } // Generate quick deduplication key from file metadata @@ -102,22 +102,26 @@ export function createFileWithId(file: File, id?: FileId): FileWithId { const fileId = id || createFileId(); const quickKey = createQuickKey(file); - // Create new File-like object with embedded fileId and quickKey - const fileWithId = Object.create(file); - Object.defineProperty(fileWithId, 'fileId', { + const newFile = new File([file], file.name, { + type: file.type, + lastModified: file.lastModified + }); + + Object.defineProperty(newFile, 'fileId', { value: fileId, writable: false, enumerable: true, configurable: false }); - Object.defineProperty(fileWithId, 'quickKey', { + + Object.defineProperty(newFile, 'quickKey', { value: quickKey, writable: false, enumerable: true, configurable: false }); - return fileWithId as FileWithId; + return newFile as FileWithId; } // Wrap array of Files with FileIds @@ -132,19 +136,22 @@ 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.map(file => { - // Create clean File object without the fileId property - return new File([file], file.name, { - type: file.type, - lastModified: file.lastModified - }); - }); + return files as File[]; } // Type guards and validation functions +// 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'; +} + // Validate that a string is a proper FileId (has UUID format) export function isValidFileId(id: string): id is FileId { // Check UUID v4 format: 8-4-4-4-12 hex digits @@ -257,7 +264,7 @@ export interface FileOperation { id: string; type: OperationType; timestamp: number; - fileIds: string[]; + fileIds: FileId[]; status: 'pending' | 'applied' | 'failed'; data?: any; metadata?: { @@ -271,7 +278,7 @@ export interface FileOperation { } export interface FileOperationHistory { - fileId: string; + fileId: FileId; fileName: string; operations: (FileOperation | PageOperation)[]; createdAt: number; @@ -286,7 +293,7 @@ export interface ViewerConfig { } export interface FileEditHistory { - fileId: string; + fileId: FileId; pageOperations: PageOperation[]; lastModified: number; } @@ -369,26 +376,19 @@ export interface FileContextActions { // Resource management trackBlobUrl: (url: string) => void; - scheduleCleanup: (fileId: string, delay?: number) => void; - cleanupFile: (fileId: string) => void; + scheduleCleanup: (fileId: FileId, delay?: number) => void; + cleanupFile: (fileId: FileId) => void; } // File selectors (separate from actions to avoid re-renders) export interface FileContextSelectors { - // File access - now returns FileWithId for safer type checking getFile: (id: FileId) => FileWithId | undefined; getFiles: (ids?: FileId[]) => FileWithId[]; - - // Record access - uses normalized state getFileRecord: (id: FileId) => FileRecord | undefined; getFileRecords: (ids?: FileId[]) => FileRecord[]; - - // Derived selectors - now return FileWithId getAllFileIds: () => FileId[]; getSelectedFiles: () => FileWithId[]; getSelectedFileRecords: () => FileRecord[]; - - // Pinned files selectors - now return FileWithId getPinnedFileIds: () => FileId[]; getPinnedFiles: () => FileWithId[]; getPinnedFileRecords: () => FileRecord[]; diff --git a/frontend/src/types/fileIdSafety.d.ts b/frontend/src/types/fileIdSafety.d.ts index cbe41a56c..897dcf6c9 100644 --- a/frontend/src/types/fileIdSafety.d.ts +++ b/frontend/src/types/fileIdSafety.d.ts @@ -2,7 +2,7 @@ * Type safety declarations to prevent file.name/UUID confusion */ -import { FileId, FileWithId } from './fileContext'; +import { FileId, FileWithId, OperationType, FileOperation } from './fileContext'; declare global { namespace FileIdSafety { @@ -31,14 +31,8 @@ declare global { } } -// Module augmentation for stricter type checking on dangerous functions -declare module '../utils/toolOperationTracker' { - export const createOperation: ( - operationType: string, - params: TParams, - selectedFiles: FileWithId[] // Must be FileWithId, not File[] - ) => { operation: FileOperation; operationId: string; fileId: string }; -} +// Note: Module augmentation removed to prevent duplicate declaration +// The actual implementation in toolOperationTracker.ts enforces FileWithId usage // Augment FileContext types to prevent bypassing FileWithId declare module '../contexts/FileContext' { diff --git a/frontend/src/utils/toolOperationTracker.ts b/frontend/src/utils/toolOperationTracker.ts deleted file mode 100644 index c58e3bf57..000000000 --- a/frontend/src/utils/toolOperationTracker.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { FileOperation, FileWithId, safeGetFileId, FileId } from '../types/fileContext'; - -/** - * Creates operation tracking data for FileContext integration - */ -export const createOperation = ( - operationType: string, - params: TParams, - selectedFiles: FileWithId[] -): { operation: FileOperation; operationId: string; fileId: string } => { - const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - // Use proper FileIds instead of file.name - fixed dangerous pattern - const fileIds = selectedFiles.map(file => file.fileId); - const fileId = fileIds.join(','); - - const operation: FileOperation = { - id: operationId, - type: operationType, - timestamp: Date.now(), - fileIds, // Now properly uses FileId[] instead of file.name[] - status: 'pending', - metadata: { - originalFileName: selectedFiles[0]?.name, - parameters: params, - fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0) - } - }; - - return { operation, operationId, fileId }; -};