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 { 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);
|
||||||
}
|
}
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
|
@ -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 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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] } });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
||||||
|
@ -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)`);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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',
|
||||||
|
@ -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(() => {
|
||||||
|
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 {
|
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[];
|
||||||
|
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
|
* 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' {
|
||||||
|
@ -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