Update file usage to use filewithid

This commit is contained in:
Reece Browne 2025-09-03 10:44:28 +01:00
commit dad9f20879
28 changed files with 157 additions and 220 deletions

View File

@ -6,6 +6,7 @@ import { Dropzone } from '@mantine/dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import UploadFileIcon from '@mui/icons-material/UploadFile'; import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext'; import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext';
import { FileId } from '../../types/fileContext';
import { useNavigationActions } from '../../contexts/NavigationContext'; import { useNavigationActions } from '../../contexts/NavigationContext';
import { fileStorage } from '../../services/fileStorage'; import { fileStorage } from '../../services/fileStorage';
import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
@ -156,26 +157,6 @@ const FileEditor = ({
if (extractionResult.success) { if (extractionResult.success) {
allExtractedFiles.push(...extractionResult.extractedFiles); 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) { if (extractionResult.errors.length > 0) {
errors.push(...extractionResult.errors); errors.push(...extractionResult.errors);
@ -278,7 +259,7 @@ const FileEditor = ({
} }
// Update context (this automatically updates tool selection since they use the same action) // 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]); }, [setSelectedFiles, toolMode, setStatus, activeFileRecords]);
const toggleSelectionMode = useCallback(() => { const toggleSelectionMode = useCallback(() => {
@ -306,7 +287,7 @@ const FileEditor = ({
// Handle multi-file selection reordering // Handle multi-file selection reordering
const filesToMove = selectedFileIds.length > 1 const filesToMove = selectedFileIds.length > 1
? selectedFileIds.filter(id => currentIds.includes(id)) ? selectedFileIds.filter(id => currentIds.includes(id as any))
: [sourceFileId]; : [sourceFileId];
// Create new order // Create new order
@ -337,7 +318,7 @@ const FileEditor = ({
} }
// Insert files at the calculated position // Insert files at the calculated position
newOrder.splice(insertIndex, 0, ...filesToMove); newOrder.splice(insertIndex, 0, ...filesToMove.map(id => id as any));
// Update file order // Update file order
reorderFiles(newOrder); reorderFiles(newOrder);
@ -355,31 +336,11 @@ const FileEditor = ({
const file = record ? selectors.getFile(record.id) : null; const file = record ? selectors.getFile(record.id) : null;
if (record && file) { 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) // Remove file from context but keep in storage (close, don't delete)
removeFiles([contextFileId], false); removeFiles([record.id], false);
// Remove from context selections // Remove from context selections
const currentSelected = selectedFileIds.filter(id => id !== contextFileId); const currentSelected = selectedFileIds.filter(id => id !== record.id);
setSelectedFiles(currentSelected); setSelectedFiles(currentSelected);
} }
}, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]); }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
@ -388,7 +349,7 @@ const FileEditor = ({
const record = activeFileRecords.find(r => r.id === fileId); const record = activeFileRecords.find(r => r.id === fileId);
if (record) { if (record) {
// Set the file as selected in context and switch to viewer for preview // Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]); setSelectedFiles([fileId as FileId]);
navActions.setMode('viewer'); navActions.setMode('viewer');
} }
}, [activeFileRecords, setSelectedFiles, navActions.setMode]); }, [activeFileRecords, setSelectedFiles, navActions.setMode]);
@ -405,7 +366,7 @@ const FileEditor = ({
}, [activeFileRecords, selectors, onMergeFiles]); }, [activeFileRecords, selectors, onMergeFiles]);
const handleSplitFile = useCallback((fileId: string) => { const handleSplitFile = useCallback((fileId: string) => {
const file = selectors.getFile(fileId); const file = selectors.getFile(fileId as FileId);
if (file && onOpenPageEditor) { if (file && onOpenPageEditor) {
onOpenPageEditor(file); onOpenPageEditor(file);
} }

View File

@ -60,8 +60,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(() => {

View File

@ -60,8 +60,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(() => {

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useFileState } from '../../../contexts/FileContext'; import { useFileState } from '../../../contexts/FileContext';
import { PDFDocument, PDFPage } from '../../../types/pageEditor'; import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '../../../types/fileContext';
export interface PageDocumentHook { export interface PageDocumentHook {
document: PDFDocument | null; document: PDFDocument | null;
@ -70,7 +71,7 @@ export function usePageDocument(): PageDocumentHook {
let totalPageCount = 0; let totalPageCount = 0;
// Helper function to create pages from a file // 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); const fileRecord = selectors.getFileRecord(fileId);
if (!fileRecord) { if (!fileRecord) {
return []; return [];
@ -111,7 +112,7 @@ export function usePageDocument(): PageDocumentHook {
// Collect all pages from original files (without renumbering yet) // Collect all pages from original files (without renumbering yet)
const originalFilePages: PDFPage[] = []; const originalFilePages: PDFPage[] = [];
originalFileIds.forEach(fileId => { originalFileIds.forEach(fileId => {
const filePages = createPagesFromFile(fileId, 1); // Temporary numbering const filePages = createPagesFromFile(fileId as FileId, 1); // Temporary numbering
originalFilePages.push(...filePages); originalFilePages.push(...filePages);
}); });
@ -130,7 +131,7 @@ export function usePageDocument(): PageDocumentHook {
// Collect all pages to insert // Collect all pages to insert
const allNewPages: PDFPage[] = []; const allNewPages: PDFPage[] = [];
fileIds.forEach(fileId => { fileIds.forEach(fileId => {
const insertedPages = createPagesFromFile(fileId, 1); const insertedPages = createPagesFromFile(fileId as FileId, 1);
allNewPages.push(...insertedPages); allNewPages.push(...insertedPages);
}); });

View File

@ -123,10 +123,9 @@ const FileGrid = ({
style={{ overflowY: "auto", width: "100%" }} style={{ overflowY: "auto", width: "100%" }}
> >
{displayFiles.map((item, idx) => { {displayFiles.map((item, idx) => {
// Use record ID if available, otherwise throw error for missing FileRecord
if (!item.record?.id) { if (!item.record?.id) {
console.error('FileGrid: File missing FileRecord with proper ID:', item.file.name); 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 fileId = item.record.id;
const originalIdx = files.findIndex(f => f.record?.id === fileId); const originalIdx = files.findIndex(f => f.record?.id === fileId);

View File

@ -22,12 +22,13 @@ import {
OUTPUT_OPTIONS, OUTPUT_OPTIONS,
FIT_OPTIONS FIT_OPTIONS
} from "../../../constants/convertConstants"; } from "../../../constants/convertConstants";
import { FileWithId } from "../../../types/fileContext";
interface ConvertSettingsProps { interface ConvertSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
selectedFiles: File[]; selectedFiles: FileWithId[];
disabled?: boolean; disabled?: boolean;
} }
@ -128,7 +129,7 @@ const ConvertSettings = ({
}; };
const filterFilesByExtension = (extension: string) => { 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 => { return files.filter(file => {
const fileExtension = detectFileExtension(file.name); const fileExtension = detectFileExtension(file.name);
@ -142,21 +143,8 @@ const ConvertSettings = ({
}); });
}; };
const updateFileSelection = (files: File[]) => { const updateFileSelection = (files: FileWithId[]) => {
// Map File objects to their actual IDs in FileContext const fileIds = files.map(file => file.fileId);
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
setSelectedFiles(fileIds); setSelectedFiles(fileIds);
}; };

View File

@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters'; import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection'; import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
import { FileWithId } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps { interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
selectedFiles: File[]; selectedFiles: FileWithId[];
disabled?: boolean; disabled?: boolean;
} }

View File

@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload';
import { useFilesModalContext } from "../../../contexts/FilesModalContext"; import { useFilesModalContext } from "../../../contexts/FilesModalContext";
import { useAllFiles } from "../../../contexts/FileContext"; import { useAllFiles } from "../../../contexts/FileContext";
import { useFileManager } from "../../../hooks/useFileManager"; import { useFileManager } from "../../../hooks/useFileManager";
import { FileWithId } from "../../../types/fileContext";
export interface FileStatusIndicatorProps { export interface FileStatusIndicatorProps {
selectedFiles?: File[]; selectedFiles?: FileWithId[];
placeholder?: string; placeholder?: string;
} }

View File

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import FileStatusIndicator from './FileStatusIndicator'; import FileStatusIndicator from './FileStatusIndicator';
import { FileWithId } from '../../../types/fileContext';
export interface FilesToolStepProps { export interface FilesToolStepProps {
selectedFiles: File[]; selectedFiles: FileWithId[];
isCollapsed?: boolean; isCollapsed?: boolean;
onCollapsedClick?: () => void; onCollapsedClick?: () => void;
placeholder?: string; placeholder?: string;

View File

@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep';
import OperationButton from './OperationButton'; import OperationButton from './OperationButton';
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
import { FileWithId } from '../../../types/fileContext';
export interface FilesStepConfig { export interface FilesStepConfig {
selectedFiles: File[]; selectedFiles: FileWithId[];
isCollapsed?: boolean; isCollapsed?: boolean;
placeholder?: string; placeholder?: string;
onCollapsedClick?: () => void; onCollapsedClick?: () => void;

View File

@ -15,6 +15,7 @@ import { fileStorage } from "../../services/fileStorage";
import SkeletonLoader from '../shared/SkeletonLoader'; import SkeletonLoader from '../shared/SkeletonLoader';
import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext"; import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl"; import { useFileWithUrl } from "../../hooks/useFileWithUrl";
import { isFileObject } from "../../types/fileContext";
// Lazy loading page image component // Lazy loading page image component
@ -200,7 +201,7 @@ const Viewer = ({
const effectiveFile = React.useMemo(() => { const effectiveFile = React.useMemo(() => {
if (previewFile) { if (previewFile) {
// Validate the preview file // Validate the preview file
if (!(previewFile instanceof File)) { if (!isFileObject(previewFile)) {
return null; return null;
} }

View File

@ -89,19 +89,16 @@ function FileContextInner({
})); }));
} }
// Convert to FileWithId objects
return addedFilesWithIds.map(({ file, id }) => createFileWithId(file, id)); 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<FileWithId[]> => { 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);
// Convert to FileWithId objects
return result.map(({ file, id }) => createFileWithId(file, id)); return result.map(({ file, id }) => createFileWithId(file, id));
}, []); }, []);
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise<FileWithId[]> => { const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise<FileWithId[]> => {
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager); const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
// Convert to FileWithId objects
return result.map(({ file, id }) => createFileWithId(file, id)); return result.map(({ file, id }) => createFileWithId(file, id));
}, []); }, []);
@ -111,11 +108,9 @@ function FileContextInner({
// Helper functions for pinned files // Helper functions for pinned files
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileWithId[]> => { const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileWithId[]> => {
const result = await consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch); const result = await consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch);
// Convert results to FileWithId objects
return result.map(({ file, id }) => createFileWithId(file, id)); return result.map(({ file, id }) => createFileWithId(file, id));
}, []); }, []);
// File pinning functions - now use FileWithId directly
const pinFileWrapper = useCallback((file: FileWithId) => { const pinFileWrapper = useCallback((file: FileWithId) => {
baseActions.pinFile(file.fileId); baseActions.pinFile(file.fileId);
}, [baseActions]); }, [baseActions]);

View File

@ -361,7 +361,6 @@ export async function consumeFiles(
}) })
); );
// Extract records for dispatch
const outputFileRecords = processedOutputs.map(({ record }) => record); const outputFileRecords = processedOutputs.map(({ record }) => record);
// Dispatch the consume action // 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`); 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 })); return processedOutputs.map(({ file, id, thumbnail }) => ({ file, id, thumbnail }));
} }

View File

@ -9,7 +9,7 @@ import {
FileContextStateValue, FileContextStateValue,
FileContextActionsValue FileContextActionsValue
} from './contexts'; } 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) * 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) * 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(() => ({
@ -135,7 +135,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(() => ({
@ -164,11 +164,6 @@ export function useFileContext() {
// 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
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 // Pinned files
pinnedFiles: state.pinnedFiles, pinnedFiles: state.pinnedFiles,

View File

@ -35,10 +35,10 @@ export class FileLifecycleManager {
*/ */
cleanupFile = (fileId: string, stateRef?: React.MutableRefObject<any>): void => { cleanupFile = (fileId: string, stateRef?: React.MutableRefObject<any>): void => {
// Use comprehensive cleanup (same as removeFiles) // Use comprehensive cleanup (same as removeFiles)
this.cleanupAllResourcesForFile(fileId, stateRef); this.cleanupAllResourcesForFile(fileId as FileId, stateRef);
// Remove file from state // Remove file from state
this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds: [fileId] } }); this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds: [fileId as FileId] } });
}; };
/** /**

View File

@ -6,7 +6,6 @@ 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 } from '../../../types/fileContext'; import { FileWithId, extractFiles } from '../../../types/fileContext';
import { ResponseHandler } from '../../../utils/toolResponseProcessor'; import { ResponseHandler } from '../../../utils/toolResponseProcessor';
@ -108,7 +107,7 @@ export const useToolOperation = <TParams = void>(
config: ToolOperationConfig<TParams> config: ToolOperationConfig<TParams>
): ToolOperationHook<TParams> => { ): ToolOperationHook<TParams> => {
const { t } = useTranslation(); const { t } = useTranslation();
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles } = useFileContext(); const { addFiles, consumeFiles } = useFileContext();
// Composed hooks // Composed hooks
const { state, actions } = useToolState(); const { state, actions } = useToolState();
@ -131,9 +130,6 @@ export const useToolOperation = <TParams = void>(
return; return;
} }
// Setup operation tracking with proper FileWithId
const { operation, operationId, fileId } = createOperation(config.operationType, params, selectedFiles);
recordOperation(fileId, operation);
// Reset state // Reset state
actions.setLoading(true); actions.setLoading(true);
@ -144,7 +140,6 @@ export const useToolOperation = <TParams = void>(
try { try {
let processedFiles: File[]; let processedFiles: File[];
// Convert FileWithId to regular File objects for API processing
const validRegularFiles = extractFiles(validFiles); const validRegularFiles = extractFiles(validFiles);
if (config.customProcessor) { if (config.customProcessor) {
@ -214,20 +209,17 @@ export const useToolOperation = <TParams = void>(
// Replace input files with processed files (consumeFiles handles pinning) // Replace input files with processed files (consumeFiles handles pinning)
const inputFileIds = validFiles.map(file => file.fileId); const inputFileIds = validFiles.map(file => file.fileId);
await consumeFiles(inputFileIds, processedFiles); await consumeFiles(inputFileIds, processedFiles);
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, 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();

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useFileState, useFileActions } from '../contexts/FileContext'; import { useFileState, useFileActions } from '../contexts/FileContext';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { FileId } from '../types/fileContext';
export const useFileHandler = () => { export const useFileHandler = () => {
const { state } = useFileState(); // Still needed for addStoredFiles 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 }>) => { const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => {
// Filter out files that already exist with the same ID (exact match) // Filter out files that already exist with the same ID (exact match)
const newFiles = filesWithMetadata.filter(({ originalId }) => { const newFiles = filesWithMetadata.filter(({ originalId }) => {
return state.files.byId[originalId] === undefined; return state.files.byId[originalId as FileId] === undefined;
}); });
if (newFiles.length > 0) { 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)`); console.log(`📁 Added ${newFiles.length} stored files (${filesWithMetadata.length - newFiles.length} skipped as duplicates)`);

View File

@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
import { useIndexedDB } from '../contexts/IndexedDBContext'; import { useIndexedDB } from '../contexts/IndexedDBContext';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { generateThumbnailForFile } from '../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../utils/thumbnailUtils';
import { FileId } from '../types/fileContext';
export const useFileManager = () => { export const useFileManager = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -46,7 +47,7 @@ export const useFileManager = () => {
// Regular file loading // Regular file loading
if (fileMetadata.id) { if (fileMetadata.id) {
const file = await indexedDB.loadFile(fileMetadata.id); const file = await indexedDB.loadFile(fileMetadata.id as FileId);
if (file) { if (file) {
return file; return file;
} }
@ -86,7 +87,7 @@ export const useFileManager = () => {
throw new Error('IndexedDB context not available'); throw new Error('IndexedDB context not available');
} }
try { try {
await indexedDB.deleteFile(file.id); await indexedDB.deleteFile(file.id as FileId);
setFiles(files.filter((_, i) => i !== index)); setFiles(files.filter((_, i) => i !== index));
} catch (error) { } catch (error) {
console.error('Failed to remove file:', error); console.error('Failed to remove file:', error);
@ -100,7 +101,7 @@ export const useFileManager = () => {
} }
try { try {
// Store file with provided UUID from FileContext (thumbnail generated internally) // 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 // Convert file to ArrayBuffer for StoredFile interface compatibility
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
@ -176,7 +177,7 @@ export const useFileManager = () => {
try { try {
// Update access time - this will be handled by the cache in IndexedDBContext // 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 // 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) { } catch (error) {
console.error('Failed to touch file:', error); console.error('Failed to touch file:', error);
} }

View File

@ -1,4 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isFileObject } from '../types/fileContext';
/** /**
* Hook to convert a File object to { file: File; url: string } format * 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(() => { return useMemo(() => {
if (!file) return null; if (!file) return null;
// Validate that file is a proper File or Blob object // Validate that file is a proper File, FileWithId, or Blob object
if (!(file instanceof File) && !(file instanceof Blob)) { if (!isFileObject(file) && !(file instanceof Blob)) {
console.warn('useFileWithUrl: Expected File or Blob, got:', file); console.warn('useFileWithUrl: Expected File or Blob, got:', file);
return null; return null;
} }

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { FileMetadata } from "../types/file"; import { FileMetadata } from "../types/file";
import { useIndexedDB } from "../contexts/IndexedDBContext"; import { useIndexedDB } from "../contexts/IndexedDBContext";
import { generateThumbnailForFile } from "../utils/thumbnailUtils"; import { generateThumbnailForFile } from "../utils/thumbnailUtils";
import { FileId } from "../types/fileContext";
/** /**
* Calculate optimal scale for thumbnail generation * 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 // Try to load file from IndexedDB using new context
if (file.id && indexedDB) { if (file.id && indexedDB) {
const loadedFile = await indexedDB.loadFile(file.id); const loadedFile = await indexedDB.loadFile(file.id as FileId);
if (!loadedFile) { if (!loadedFile) {
throw new Error('File not found in IndexedDB'); 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 // Save thumbnail to IndexedDB for persistence
if (file.id && indexedDB && thumbnail) { if (file.id && indexedDB && thumbnail) {
try { try {
await indexedDB.updateThumbnail(file.id, thumbnail); await indexedDB.updateThumbnail(file.id as FileId, thumbnail);
} catch (error) { } catch (error) {
console.warn('Failed to save thumbnail to IndexedDB:', error); console.warn('Failed to save thumbnail to IndexedDB:', error);
} }

View File

@ -1,13 +1,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { pdfWorkerManager } from '../services/pdfWorkerManager'; import { pdfWorkerManager } from '../services/pdfWorkerManager';
import { FileWithId } from '../types/fileContext';
export interface PdfSignatureDetectionResult { export interface PdfSignatureDetectionResult {
hasDigitalSignatures: boolean; hasDigitalSignatures: boolean;
isChecking: boolean; isChecking: boolean;
} }
export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionResult => { export const usePdfSignatureDetection = (files: FileWithId[]): PdfSignatureDetectionResult => {
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false); const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);

View File

@ -71,7 +71,6 @@ 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 quickKey for PDF document caching (same metadata, consistent format)
const fileId = createQuickKey(file); const fileId = createQuickKey(file);
const results = await thumbnailGenerationService.generateThumbnails( const results = await thumbnailGenerationService.generateThumbnails(

View File

@ -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',

View File

@ -14,6 +14,8 @@ import i18n from '../../i18n/config';
import axios from 'axios'; import axios from 'axios';
import { detectFileExtension } from '../../utils/fileUtils'; import { detectFileExtension } from '../../utils/fileUtils';
import { FIT_OPTIONS } from '../../constants/convertConstants'; import { FIT_OPTIONS } from '../../constants/convertConstants';
import { createTestFileWithId, createTestFilesWithId } from '../utils/testFileHelpers';
import { FileWithId } from '../../types/fileContext';
// Mock axios // Mock axios
vi.mock('axios'); vi.mock('axios');
@ -81,7 +83,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
// Create mock DOCX file // 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 // Test auto-detection
act(() => { act(() => {
@ -117,7 +119,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
// Create mock unknown file // 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 // Test auto-detection
act(() => { act(() => {
@ -156,11 +158,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
// Create mock image files // Create mock image files
const imageFiles = [ const imageFiles = createTestFilesWithId([
new File(['jpg content'], 'photo1.jpg', { type: 'image/jpeg' }), { name: 'photo1.jpg', content: 'jpg content', type: 'image/jpeg' },
new File(['png content'], 'photo2.png', { type: 'image/png' }), { name: 'photo2.png', content: 'png content', type: 'image/png' },
new File(['gif content'], 'photo3.gif', { type: 'image/gif' }) { name: 'photo3.gif', content: 'gif content', type: 'image/gif' }
]; ]);
// Test smart detection for all images // Test smart detection for all images
act(() => { act(() => {
@ -202,11 +204,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
// Create mixed file types // Create mixed file types
const mixedFiles = [ const mixedFiles = createTestFilesWithId([
new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }), { name: 'document.pdf', content: 'pdf content', type: 'application/pdf' },
new File(['docx content'], 'spreadsheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), { name: 'spreadsheet.xlsx', content: 'docx content', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
new File(['pptx content'], 'presentation.pptx', { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }) { name: 'presentation.pptx', content: 'pptx content', type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
]; ]);
// Test smart detection for mixed types // Test smart detection for mixed types
act(() => { act(() => {
@ -243,10 +245,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}); });
// Create mock web files // Create mock web files
const webFiles = [ const webFiles = createTestFilesWithId([
new File(['<html>content</html>'], 'page1.html', { type: 'text/html' }), { name: 'page1.html', content: '<html>content</html>', type: 'text/html' },
new File(['zip content'], 'site.zip', { type: 'application/zip' }) { name: 'site.zip', content: 'zip content', type: 'application/zip' }
]; ]);
// Test smart detection for web files // Test smart detection for web files
act(() => { act(() => {
@ -288,7 +290,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
wrapper: TestWrapper wrapper: TestWrapper
}); });
const htmlFile = new File(['<html>content</html>'], 'page.html', { type: 'text/html' }); const htmlFile = createTestFileWithId('page.html', '<html>content</html>', 'text/html');
// Set up HTML conversion parameters // Set up HTML conversion parameters
act(() => { act(() => {
@ -318,7 +320,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
wrapper: TestWrapper 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 // Set up email conversion parameters
act(() => { act(() => {
@ -355,7 +357,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
wrapper: TestWrapper 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 // Set up PDF/A conversion parameters
act(() => { act(() => {
@ -392,10 +394,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
wrapper: TestWrapper wrapper: TestWrapper
}); });
const imageFiles = [ const imageFiles = createTestFilesWithId([
new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' }
]; ]);
// Set up image conversion parameters // Set up image conversion parameters
act(() => { act(() => {
@ -432,10 +434,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
wrapper: TestWrapper wrapper: TestWrapper
}); });
const imageFiles = [ const imageFiles = createTestFilesWithId([
new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' }
]; ]);
// Set up for separate processing // Set up for separate processing
act(() => { act(() => {
@ -477,10 +479,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
}) })
.mockRejectedValueOnce(new Error('File 2 failed')); .mockRejectedValueOnce(new Error('File 2 failed'));
const mixedFiles = [ const mixedFiles = createTestFilesWithId([
new File(['file1'], 'doc1.txt', { type: 'text/plain' }), { name: 'doc1.txt', content: 'file1', type: 'text/plain' },
new File(['file2'], 'doc2.xyz', { type: 'application/octet-stream' }) { name: 'doc2.xyz', content: 'file2', type: 'application/octet-stream' }
]; ]);
// Set up for separate processing (mixed smart detection) // Set up for separate processing (mixed smart detection)
act(() => { act(() => {

View 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)
);
}

View File

@ -69,14 +69,14 @@ export interface FileContextNormalizedFiles {
export function createFileId(): FileId { export function createFileId(): FileId {
// Use crypto.randomUUID for authoritative primary key // Use crypto.randomUUID for authoritative primary key
if (typeof window !== 'undefined' && window.crypto?.randomUUID) { if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
return window.crypto.randomUUID(); return window.crypto.randomUUID() as FileId;
} }
// Fallback for environments without randomUUID // Fallback for environments without randomUUID
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0; const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8); const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16); return v.toString(16);
}); }) as FileId;
} }
// Generate quick deduplication key from file metadata // Generate quick deduplication key from file metadata
@ -102,22 +102,26 @@ export function createFileWithId(file: File, id?: FileId): FileWithId {
const fileId = id || createFileId(); const fileId = id || createFileId();
const quickKey = createQuickKey(file); const quickKey = createQuickKey(file);
// Create new File-like object with embedded fileId and quickKey const newFile = new File([file], file.name, {
const fileWithId = Object.create(file); type: file.type,
Object.defineProperty(fileWithId, 'fileId', { lastModified: file.lastModified
});
Object.defineProperty(newFile, 'fileId', {
value: fileId, value: fileId,
writable: false, writable: false,
enumerable: true, enumerable: true,
configurable: false configurable: false
}); });
Object.defineProperty(fileWithId, 'quickKey', {
Object.defineProperty(newFile, 'quickKey', {
value: quickKey, value: quickKey,
writable: false, writable: false,
enumerable: true, enumerable: true,
configurable: false configurable: false
}); });
return fileWithId as FileWithId; return newFile as FileWithId;
} }
// Wrap array of Files with FileIds // Wrap array of Files with FileIds
@ -132,19 +136,22 @@ export function extractFileIds(files: FileWithId[]): FileId[] {
return files.map(file => file.fileId); return files.map(file => file.fileId);
} }
// Extract regular File objects from FileWithId array
export function extractFiles(files: FileWithId[]): File[] { export function extractFiles(files: FileWithId[]): File[] {
return files.map(file => { return files as File[];
// Create clean File object without the fileId property
return new File([file], file.name, {
type: file.type,
lastModified: file.lastModified
});
});
} }
// Type guards and validation functions // 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) // Validate that a string is a proper FileId (has UUID format)
export function isValidFileId(id: string): id is FileId { export function isValidFileId(id: string): id is FileId {
// Check UUID v4 format: 8-4-4-4-12 hex digits // Check UUID v4 format: 8-4-4-4-12 hex digits
@ -257,7 +264,7 @@ export interface FileOperation {
id: string; id: string;
type: OperationType; type: OperationType;
timestamp: number; timestamp: number;
fileIds: string[]; fileIds: FileId[];
status: 'pending' | 'applied' | 'failed'; status: 'pending' | 'applied' | 'failed';
data?: any; data?: any;
metadata?: { metadata?: {
@ -271,7 +278,7 @@ export interface FileOperation {
} }
export interface FileOperationHistory { export interface FileOperationHistory {
fileId: string; fileId: FileId;
fileName: string; fileName: string;
operations: (FileOperation | PageOperation)[]; operations: (FileOperation | PageOperation)[];
createdAt: number; createdAt: number;
@ -286,7 +293,7 @@ export interface ViewerConfig {
} }
export interface FileEditHistory { export interface FileEditHistory {
fileId: string; fileId: FileId;
pageOperations: PageOperation[]; pageOperations: PageOperation[];
lastModified: number; lastModified: number;
} }
@ -369,26 +376,19 @@ export interface FileContextActions {
// Resource management // Resource management
trackBlobUrl: (url: string) => void; trackBlobUrl: (url: string) => void;
scheduleCleanup: (fileId: string, delay?: number) => void; scheduleCleanup: (fileId: FileId, delay?: number) => void;
cleanupFile: (fileId: string) => void; cleanupFile: (fileId: FileId) => void;
} }
// 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 - now returns FileWithId for safer type checking
getFile: (id: FileId) => FileWithId | undefined; getFile: (id: FileId) => FileWithId | undefined;
getFiles: (ids?: FileId[]) => FileWithId[]; getFiles: (ids?: FileId[]) => FileWithId[];
// 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 - now return FileWithId
getAllFileIds: () => FileId[]; getAllFileIds: () => FileId[];
getSelectedFiles: () => FileWithId[]; getSelectedFiles: () => FileWithId[];
getSelectedFileRecords: () => FileRecord[]; getSelectedFileRecords: () => FileRecord[];
// Pinned files selectors - now return FileWithId
getPinnedFileIds: () => FileId[]; getPinnedFileIds: () => FileId[];
getPinnedFiles: () => FileWithId[]; getPinnedFiles: () => FileWithId[];
getPinnedFileRecords: () => FileRecord[]; getPinnedFileRecords: () => FileRecord[];

View File

@ -2,7 +2,7 @@
* Type safety declarations to prevent file.name/UUID confusion * Type safety declarations to prevent file.name/UUID confusion
*/ */
import { FileId, FileWithId } from './fileContext'; import { FileId, FileWithId, OperationType, FileOperation } from './fileContext';
declare global { declare global {
namespace FileIdSafety { namespace FileIdSafety {
@ -31,14 +31,8 @@ declare global {
} }
} }
// Module augmentation for stricter type checking on dangerous functions // Note: Module augmentation removed to prevent duplicate declaration
declare module '../utils/toolOperationTracker' { // The actual implementation in toolOperationTracker.ts enforces FileWithId usage
export const createOperation: <TParams = void>(
operationType: string,
params: TParams,
selectedFiles: FileWithId[] // Must be FileWithId, not File[]
) => { operation: FileOperation; operationId: string; fileId: string };
}
// Augment FileContext types to prevent bypassing FileWithId // Augment FileContext types to prevent bypassing FileWithId
declare module '../contexts/FileContext' { declare module '../contexts/FileContext' {

View File

@ -1,31 +0,0 @@
import { FileOperation, FileWithId, safeGetFileId, FileId } from '../types/fileContext';
/**
* Creates operation tracking data for FileContext integration
*/
export const createOperation = <TParams = void>(
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 };
};