mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Update file usage to use filewithid
This commit is contained in:
commit
dad9f20879
@ -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';
|
||||
@ -157,26 +158,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);
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<FileWithId[]> => {
|
||||
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<FileWithId[]> => {
|
||||
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<FileWithId[]> => {
|
||||
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]);
|
||||
|
@ -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 }));
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -35,10 +35,10 @@ export class FileLifecycleManager {
|
||||
*/
|
||||
cleanupFile = (fileId: string, stateRef?: React.MutableRefObject<any>): 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] } });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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 = <TParams = void>(
|
||||
config: ToolOperationConfig<TParams>
|
||||
): ToolOperationHook<TParams> => {
|
||||
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 = <TParams = void>(
|
||||
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 = <TParams = void>(
|
||||
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 = <TParams = void>(
|
||||
// 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();
|
||||
|
@ -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)`);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
|
@ -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(['<html>content</html>'], 'page1.html', { type: 'text/html' }),
|
||||
new File(['zip content'], 'site.zip', { type: 'application/zip' })
|
||||
];
|
||||
const webFiles = createTestFilesWithId([
|
||||
{ name: 'page1.html', content: '<html>content</html>', 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(['<html>content</html>'], 'page.html', { type: 'text/html' });
|
||||
const htmlFile = createTestFileWithId('page.html', '<html>content</html>', '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(() => {
|
||||
|
28
frontend/src/tests/utils/testFileHelpers.ts
Normal file
28
frontend/src/tests/utils/testFileHelpers.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Test utilities for creating FileWithId objects in tests
|
||||
*/
|
||||
|
||||
import { FileWithId, createFileWithId } from '../../types/fileContext';
|
||||
|
||||
/**
|
||||
* Create a FileWithId object for testing purposes
|
||||
*/
|
||||
export function createTestFileWithId(
|
||||
name: string,
|
||||
content: string = 'test content',
|
||||
type: string = 'application/pdf'
|
||||
): FileWithId {
|
||||
const file = new File([content], name, { type });
|
||||
return createFileWithId(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple FileWithId objects for testing
|
||||
*/
|
||||
export function createTestFilesWithId(
|
||||
files: Array<{ name: string; content?: string; type?: string }>
|
||||
): FileWithId[] {
|
||||
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
|
||||
createTestFileWithId(name, content, type)
|
||||
);
|
||||
}
|
@ -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[];
|
||||
|
12
frontend/src/types/fileIdSafety.d.ts
vendored
12
frontend/src/types/fileIdSafety.d.ts
vendored
@ -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: <TParams = void>(
|
||||
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' {
|
||||
|
@ -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 };
|
||||
};
|
Loading…
Reference in New Issue
Block a user