diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js deleted file mode 100644 index cd779cd72..000000000 --- a/frontend/.eslintrc.js +++ /dev/null @@ -1,55 +0,0 @@ -module.exports = { - extends: [ - 'react-app', - 'react-app/jest' - ], - rules: { - // Custom rules to prevent dangerous file.name as ID patterns - 'no-file-name-as-id': 'error', - 'prefer-file-with-id': 'warn' - }, - overrides: [ - { - files: ['**/*.ts', '**/*.tsx'], - rules: { - // Prevent file.name being used where FileId is expected - 'no-restricted-syntax': [ - 'error', - { - selector: 'MemberExpression[object.name="file"][property.name="name"]', - message: 'Avoid using file.name directly. Use FileWithId.fileId or safeGetFileId() instead to prevent ID collisions.' - }, - { - selector: 'CallExpression[callee.name="createOperation"] > ArrayExpression > CallExpression[callee.property.name="map"] > ArrowFunctionExpression > MemberExpression[object.name="f"][property.name="name"]', - message: 'Dangerous pattern: Using file.name as ID in createOperation. Use FileWithId.fileId instead.' - }, - { - selector: 'ArrayExpression[elements.length>0] CallExpression[callee.property.name="map"] > ArrowFunctionExpression > MemberExpression[property.name="name"]', - message: 'Potential file.name as ID usage detected. Ensure proper FileId usage instead of file.name.' - } - ] - } - } - ], - settings: { - // Custom settings for our file ID validation - 'file-id-validation': { - // Functions that should only accept FileId, not strings - 'file-id-only-functions': [ - 'recordOperation', - 'markOperationApplied', - 'markOperationFailed', - 'removeFiles', - 'updateFileRecord', - 'pinFile', - 'unpinFile' - ], - // Functions that should accept FileWithId instead of File - 'file-with-id-functions': [ - 'createOperation', - 'executeOperation', - 'isFilePinned' - ] - } - } -}; \ No newline at end of file diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index 4294180f3..dc0001b4e 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -24,7 +24,7 @@ const FileManager: React.FC = ({ selectedTool }) => { const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager(); // Wrapper for storeFile that generates UUID - const storeFileWithId = useCallback(async (file: File) => { + const storeStirlingFile = useCallback(async (file: File) => { const fileId = createFileId(); // Generate UUID for storage return await storeFile(file, fileId); }, [storeFile]); diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 7acfdead8..383010257 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -16,12 +16,12 @@ import styles from './FileEditor.module.css'; import FileEditorThumbnail from './FileEditorThumbnail'; import FilePickerModal from '../shared/FilePickerModal'; import SkeletonLoader from '../shared/SkeletonLoader'; -import { FileId, FileWithId } from '../../types/fileContext'; +import { FileId, StirlingFile } from '../../types/fileContext'; interface FileEditorProps { - onOpenPageEditor?: (file: FileWithId) => void; - onMergeFiles?: (files: FileWithId[]) => void; + onOpenPageEditor?: (file: StirlingFile) => void; + onMergeFiles?: (files: StirlingFile[]) => void; toolMode?: boolean; showUpload?: boolean; showBulkActions?: boolean; @@ -50,7 +50,7 @@ const FileEditor = ({ // Extract needed values from state (memoized to prevent infinite loops) const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]); - const activeFileRecords = useMemo(() => selectors.getFileRecords(), [selectors.getFilesSignature()]); + const activeWorkbenchFiles = useMemo(() => selectors.getWorkbenchFiles(), [selectors.getFilesSignature()]); const selectedFileIds = state.ui.selectedFileIds; const isProcessing = state.ui.isProcessing; @@ -92,10 +92,10 @@ const FileEditor = ({ const contextSelectedIdsRef = useRef([]); contextSelectedIdsRef.current = contextSelectedIds; - // Use activeFileRecords directly - no conversion needed + // Use activeWorkbenchFiles directly - no conversion needed const localSelectedIds = contextSelectedIds; - // Helper to convert FileRecord to FileThumbnail format + // Helper to convert WorkbenchFile to FileThumbnail format const recordToFileItem = useCallback((record: any) => { const file = selectors.getFile(record.id); if (!file) return null; @@ -253,26 +253,26 @@ const FileEditor = ({ }, [addFiles]); const selectAll = useCallback(() => { - setSelectedFiles(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly - }, [activeFileRecords, setSelectedFiles]); + setSelectedFiles(activeWorkbenchFiles.map(r => r.id)); // Use WorkbenchFile IDs directly + }, [activeWorkbenchFiles, setSelectedFiles]); const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]); const closeAllFiles = useCallback(() => { - if (activeFileRecords.length === 0) return; + if (activeWorkbenchFiles.length === 0) return; // Remove all files from context but keep in storage - const allFileIds = activeFileRecords.map(record => record.id); + const allFileIds = activeWorkbenchFiles.map(record => record.id); removeFiles(allFileIds, false); // false = keep in storage // Clear selections setSelectedFiles([]); - }, [activeFileRecords, removeFiles, setSelectedFiles]); + }, [activeWorkbenchFiles, removeFiles, setSelectedFiles]); const toggleFile = useCallback((fileId: FileId) => { const currentSelectedIds = contextSelectedIdsRef.current; - const targetRecord = activeFileRecords.find(r => r.id === fileId); + const targetRecord = activeWorkbenchFiles.find(r => r.id === fileId); if (!targetRecord) return; const contextFileId = fileId; // No need to create a new ID @@ -302,7 +302,7 @@ const FileEditor = ({ // Update context (this automatically updates tool selection since they use the same action) setSelectedFiles(newSelection); - }, [setSelectedFiles, toolMode, setStatus, activeFileRecords]); + }, [setSelectedFiles, toolMode, setStatus, activeWorkbenchFiles]); const toggleSelectionMode = useCallback(() => { setSelectionMode(prev => { @@ -316,7 +316,7 @@ const FileEditor = ({ // File reordering handler for drag and drop const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { - const currentIds = activeFileRecords.map(r => r.id); + const currentIds = activeWorkbenchFiles.map(r => r.id); // Find indices const sourceIndex = currentIds.findIndex(id => id === sourceFileId); @@ -368,13 +368,13 @@ const FileEditor = ({ // Update status const moveCount = filesToMove.length; setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); - }, [activeFileRecords, reorderFiles, setStatus]); + }, [activeWorkbenchFiles, reorderFiles, setStatus]); // File operations using context const handleDeleteFile = useCallback((fileId: FileId) => { - const record = activeFileRecords.find(r => r.id === fileId); + const record = activeWorkbenchFiles.find(r => r.id === fileId); const file = record ? selectors.getFile(record.id) : null; if (record && file) { @@ -405,27 +405,27 @@ const FileEditor = ({ const currentSelected = selectedFileIds.filter(id => id !== contextFileId); setSelectedFiles(currentSelected); } - }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]); + }, [activeWorkbenchFiles, selectors, removeFiles, setSelectedFiles, selectedFileIds]); const handleViewFile = useCallback((fileId: FileId) => { - const record = activeFileRecords.find(r => r.id === fileId); + const record = activeWorkbenchFiles.find(r => r.id === fileId); if (record) { // Set the file as selected in context and switch to viewer for preview setSelectedFiles([fileId]); navActions.setWorkbench('viewer'); } - }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]); + }, [activeWorkbenchFiles, setSelectedFiles, navActions.setWorkbench]); const handleMergeFromHere = useCallback((fileId: FileId) => { - const startIndex = activeFileRecords.findIndex(r => r.id === fileId); + const startIndex = activeWorkbenchFiles.findIndex(r => r.id === fileId); if (startIndex === -1) return; - const recordsToMerge = activeFileRecords.slice(startIndex); - const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as FileWithId[]; + const recordsToMerge = activeWorkbenchFiles.slice(startIndex); + const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[]; if (onMergeFiles) { onMergeFiles(filesToMerge); } - }, [activeFileRecords, selectors, onMergeFiles]); + }, [activeWorkbenchFiles, selectors, onMergeFiles]); const handleSplitFile = useCallback((fileId: FileId) => { const file = selectors.getFile(fileId); @@ -467,7 +467,7 @@ const FileEditor = ({ - {activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? ( + {activeWorkbenchFiles.length === 0 && !zipExtractionProgress.isExtracting ? (
📁 @@ -475,7 +475,7 @@ const FileEditor = ({ Upload PDF files, ZIP archives, or load from storage to get started
- ) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? ( + ) : activeWorkbenchFiles.length === 0 && zipExtractionProgress.isExtracting ? ( @@ -522,7 +522,7 @@ const FileEditor = ({ pointerEvents: 'auto' }} > - {activeFileRecords.map((record, index) => { + {activeWorkbenchFiles.map((record, index) => { const fileItem = recordToFileItem(record); if (!fileItem) return null; @@ -531,7 +531,7 @@ const FileEditor = ({ key={record.id} file={fileItem} index={index} - totalFiles={activeFileRecords.length} + totalFiles={activeWorkbenchFiles.length} selectedFiles={localSelectedIds} selectionMode={selectionMode} onToggleFile={toggleFile} diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts index b620c87b8..2a17f56e8 100644 --- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts @@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook { const globalProcessing = state.ui.isProcessing; // Get primary file record outside useMemo to track processedFile changes - const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null; - const processedFilePages = primaryFileRecord?.processedFile?.pages; - const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages; + const primaryWorkbenchFile = primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : null; + const processedFilePages = primaryWorkbenchFile?.processedFile?.pages; + const processedFileTotalPages = primaryWorkbenchFile?.processedFile?.totalPages; // Compute merged document with stable signature (prevents infinite loops) const mergedPdfDocument = useMemo((): PDFDocument | null => { @@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook { const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; // If we have file IDs but no file record, something is wrong - return null to show loading - if (!primaryFileRecord) { + if (!primaryWorkbenchFile) { console.log('🎬 PageEditor: No primary file record found, showing loading'); return null; } const name = activeFileIds.length === 1 - ? (primaryFileRecord.name ?? 'document.pdf') + ? (primaryWorkbenchFile.name ?? 'document.pdf') : activeFileIds - .map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, '')) + .map(id => (selectors.getWorkbenchFile(id)?.name ?? 'file').replace(/\.pdf$/i, '')) .join(' + '); // Build page insertion map from files with insertion positions @@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook { const originalFileIds: FileId[] = []; activeFileIds.forEach(fileId => { - const record = selectors.getFileRecord(fileId); + const record = selectors.getWorkbenchFile(fileId); if (record?.insertAfterPageId !== undefined) { if (!insertionMap.has(record.insertAfterPageId)) { insertionMap.set(record.insertAfterPageId, []); @@ -72,12 +72,12 @@ export function usePageDocument(): PageDocumentHook { // Helper function to create pages from a file const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => { - const fileRecord = selectors.getFileRecord(fileId); - if (!fileRecord) { + const workbenchFiles = selectors.getWorkbenchFile(fileId); + if (!workbenchFiles) { return []; } - const processedFile = fileRecord.processedFile; + const processedFile = workbenchFiles.processedFile; let filePages: PDFPage[] = []; if (processedFile?.pages && processedFile.pages.length > 0) { @@ -159,7 +159,7 @@ export function usePageDocument(): PageDocumentHook { }; return mergedDoc; - }, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]); + }, [activeFileIds, primaryFileId, primaryWorkbenchFile, processedFilePages, processedFileTotalPages, selectors, filesSignature]); // Large document detection for smart loading const isVeryLargeDocument = useMemo(() => { diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx index b433be2ca..d24395125 100644 --- a/frontend/src/components/shared/FileCard.tsx +++ b/frontend/src/components/shared/FileCard.tsx @@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditIcon from "@mui/icons-material/Edit"; -import { FileRecord } from "../../types/fileContext"; +import { WorkbenchFile } from "../../types/fileContext"; import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; interface FileCardProps { file: File; - record?: FileRecord; + record?: WorkbenchFile; onRemove: () => void; onDoubleClick?: () => void; onView?: () => void; diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index 2da1e2b09..f26ea904c 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import SortIcon from "@mui/icons-material/Sort"; import FileCard from "./FileCard"; -import { FileRecord } from "../../types/fileContext"; +import { WorkbenchFile } from "../../types/fileContext"; import { FileId } from "../../types/file"; interface FileGridProps { - files: Array<{ file: File; record?: FileRecord }>; + files: Array<{ file: File; record?: WorkbenchFile }>; onRemove?: (index: number) => void; - onDoubleClick?: (item: { file: File; record?: FileRecord }) => void; - onView?: (item: { file: File; record?: FileRecord }) => void; - onEdit?: (item: { file: File; record?: FileRecord }) => void; + onDoubleClick?: (item: { file: File; record?: WorkbenchFile }) => void; + onView?: (item: { file: File; record?: WorkbenchFile }) => void; + onEdit?: (item: { file: File; record?: WorkbenchFile }) => void; onSelect?: (fileId: FileId) => void; selectedFiles?: FileId[]; showSearch?: boolean; @@ -125,7 +125,7 @@ const FileGrid = ({ > {displayFiles.map((item, idx) => { if (!item.record?.id) { - console.error('FileGrid: File missing FileRecord with proper ID:', item.file.name); + console.error('FileGrid: File missing WorkbenchFile with proper ID:', item.file.name); return null; } const fileId = item.record.id; diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index b141ec493..24f927443 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -34,7 +34,6 @@ export default function RightRail() { const activeFiles = selectors.getFiles(); const filesSignature = selectors.getFilesSignature(); - const fileRecords = selectors.getFileRecords(); // Compute selection state and total items const getSelectionState = useCallback(() => { @@ -85,7 +84,7 @@ export default function RightRail() { if (currentView === 'fileEditor' || currentView === 'viewer') { // Download selected files (or all if none selected) const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles; - + filesToDownload.forEach(file => { const link = document.createElement('a'); link.href = URL.createObjectURL(file); @@ -206,8 +205,8 @@ export default function RightRail() { )} {/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */} -
@@ -358,14 +357,14 @@ export default function RightRail() { 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All')) } position="left" offset={12} arrow>
- void; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; - selectedFiles: FileWithId[]; + selectedFiles: StirlingFile[]; disabled?: boolean; } @@ -129,7 +129,7 @@ const ConvertSettings = ({ }; const filterFilesByExtension = (extension: string) => { - const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as FileWithId[]; + const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as StirlingFile[]; return files.filter(file => { const fileExtension = detectFileExtension(file.name); @@ -143,7 +143,7 @@ const ConvertSettings = ({ }); }; - const updateFileSelection = (files: FileWithId[]) => { + const updateFileSelection = (files: StirlingFile[]) => { const fileIds = files.map(file => file.fileId); setSelectedFiles(fileIds); }; diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx index ccad7ce67..49e057a1c 100644 --- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx @@ -3,12 +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'; +import { StirlingFile } from '../../../types/fileContext'; interface ConvertToPdfaSettingsProps { parameters: ConvertParameters; onParameterChange: (key: keyof ConvertParameters, value: any) => void; - selectedFiles: FileWithId[]; + selectedFiles: StirlingFile[]; disabled?: boolean; } diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 214159b05..261c9eae8 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -6,10 +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"; +import { StirlingFile } from "../../../types/fileContext"; export interface FileStatusIndicatorProps { - selectedFiles?: FileWithId[]; + selectedFiles?: StirlingFile[]; placeholder?: string; } diff --git a/frontend/src/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx index 11f7d4e02..8c188d4a9 100644 --- a/frontend/src/components/tools/shared/FilesToolStep.tsx +++ b/frontend/src/components/tools/shared/FilesToolStep.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import FileStatusIndicator from './FileStatusIndicator'; -import { FileWithId } from '../../../types/fileContext'; +import { StirlingFile } from '../../../types/fileContext'; export interface FilesToolStepProps { - selectedFiles: FileWithId[]; + selectedFiles: StirlingFile[]; isCollapsed?: boolean; onCollapsedClick?: () => void; placeholder?: string; diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 1bee8b9fc..83057c13a 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -4,10 +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'; +import { StirlingFile } from '../../../types/fileContext'; export interface FilesStepConfig { - selectedFiles: FileWithId[]; + selectedFiles: StirlingFile[]; isCollapsed?: boolean; placeholder?: string; onCollapsedClick?: () => void; diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 97ac3c645..3efaa01a4 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -20,9 +20,9 @@ import { FileContextActionsValue, FileContextActions, FileId, - FileRecord, - FileWithId, - createFileWithId + WorkbenchFile, + StirlingFile, + createStirlingFile } from '../types/fileContext'; // Import modular components @@ -81,7 +81,7 @@ function FileContextInner({ } // File operations using unified addFiles helper with persistence - const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { + const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager); // Auto-select the newly added files if requested @@ -100,15 +100,15 @@ function FileContextInner({ })); } - return addedFilesWithIds.map(({ file, id }) => createFileWithId(file, id)); + return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id)); }, [indexedDB, enablePersistence]); - const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => { + const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => { const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager); - return result.map(({ file, id }) => createFileWithId(file, id)); + return result.map(({ file, id }) => createStirlingFile(file, id)); }, []); - const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => { + const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => { const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager); // Auto-select the newly added files if requested @@ -116,7 +116,7 @@ function FileContextInner({ selectFiles(result); } - return result.map(({ file, id }) => createFileWithId(file, id)); + return result.map(({ file, id }) => createStirlingFile(file, id)); }, []); // Action creators @@ -127,8 +127,8 @@ function FileContextInner({ return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB); }, [indexedDB]); - const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise => { - return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB); + const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]): Promise => { + return undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds, stateRef, filesRef, dispatch, indexedDB); }, [indexedDB]); // Helper to find FileId from File object @@ -142,12 +142,12 @@ function FileContextInner({ }); }, []); - // File pinning functions - use FileWithId directly - const pinFileWrapper = useCallback((file: FileWithId) => { + // File pinning functions - use StirlingFile directly + const pinFileWrapper = useCallback((file: StirlingFile) => { baseActions.pinFile(file.fileId); }, [baseActions]); - const unpinFileWrapper = useCallback((file: FileWithId) => { + const unpinFileWrapper = useCallback((file: StirlingFile) => { baseActions.unpinFile(file.fileId); }, [baseActions]); @@ -170,8 +170,8 @@ function FileContextInner({ } } }, - updateFileRecord: (fileId: FileId, updates: Partial) => - lifecycleManager.updateFileRecord(fileId, updates, stateRef), + updateWorkbenchFile: (fileId: FileId, updates: Partial) => + lifecycleManager.updateWorkbenchFile(fileId, updates, stateRef), reorderFiles: (orderedFileIds: FileId[]) => { dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } }); }, @@ -295,7 +295,7 @@ export { useFileSelection, useFileManagement, useFileUI, - useFileRecord, + useWorkbenchFile, useAllFiles, useSelectedFiles, // Primary API hooks for tools diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts index 93c06c4d2..c3dfa82e6 100644 --- a/frontend/src/contexts/file/FileReducer.ts +++ b/frontend/src/contexts/file/FileReducer.ts @@ -6,7 +6,7 @@ import { FileId } from '../../types/file'; import { FileContextState, FileContextAction, - FileRecord + WorkbenchFile } from '../../types/fileContext'; // Initial state @@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = { function processFileSwap( state: FileContextState, filesToRemove: FileId[], - filesToAdd: FileRecord[] + filesToAdd: WorkbenchFile[] ): FileContextState { // Only remove unpinned files const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id)); @@ -70,11 +70,11 @@ function processFileSwap( export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState { switch (action.type) { case 'ADD_FILES': { - const { fileRecords } = action.payload; + const { workbenchFiles } = action.payload; const newIds: FileId[] = []; - const newById: Record = { ...state.files.byId }; + const newById: Record = { ...state.files.byId }; - fileRecords.forEach(record => { + workbenchFiles.forEach(record => { // Only add if not already present (dedupe by stable ID) if (!newById[record.id]) { newIds.push(record.id); @@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA } case 'CONSUME_FILES': { - const { inputFileIds, outputFileRecords } = action.payload; - return processFileSwap(state, inputFileIds, outputFileRecords); + const { inputFileIds, outputWorkbenchFiles } = action.payload; + return processFileSwap(state, inputFileIds, outputWorkbenchFiles); } case 'UNDO_CONSUME_FILES': { - const { inputFileRecords, outputFileIds } = action.payload; - return processFileSwap(state, outputFileIds, inputFileRecords); + const { inputWorkbenchFiles, outputFileIds } = action.payload; + return processFileSwap(state, outputFileIds, inputWorkbenchFiles); } case 'RESET_CONTEXT': { diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index e55108553..db33e6885 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -3,10 +3,10 @@ */ import { - FileRecord, + WorkbenchFile, FileContextAction, FileContextState, - toFileRecord, + toWorkbenchFile, createFileId, createQuickKey } from '../../types/fileContext'; @@ -109,8 +109,8 @@ export async function addFiles( await addFilesMutex.lock(); try { - const fileRecords: FileRecord[] = []; - const addedFiles: AddedFile[] = []; + const workbenchFiles: WorkbenchFile[] = []; + const addedFiles: AddedFile[] = []; // Build quickKey lookup from existing files for deduplication const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId); @@ -163,7 +163,7 @@ export async function addFiles( } // Create record with immediate thumbnail and page metadata - const record = toFileRecord(file, fileId); + const record = toWorkbenchFile(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; // Track blob URLs for cleanup (images return blob URLs that need revocation) @@ -184,7 +184,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + workbenchFiles.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -205,7 +205,7 @@ export async function addFiles( const fileId = createFileId(); filesRef.current.set(fileId, file); - const record = toFileRecord(file, fileId); + const record = toWorkbenchFile(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; // Track blob URLs for cleanup (images return blob URLs that need revocation) @@ -226,7 +226,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + workbenchFiles.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -254,7 +254,7 @@ export async function addFiles( filesRef.current.set(fileId, file); - const record = toFileRecord(file, fileId); + const record = toWorkbenchFile(file, fileId); // Generate processedFile metadata for stored files let pageCount: number = 1; @@ -301,7 +301,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + workbenchFiles.push(record); addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail }); } @@ -310,9 +310,9 @@ export async function addFiles( } // Dispatch ADD_FILES action if we have new files - if (fileRecords.length > 0) { - dispatch({ type: 'ADD_FILES', payload: { fileRecords } }); - if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`); + if (workbenchFiles.length > 0) { + dispatch({ type: 'ADD_FILES', payload: { workbenchFiles } }); + if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${workbenchFiles.length} files`); } return addedFiles; @@ -328,7 +328,7 @@ export async function addFiles( async function processFilesIntoRecords( files: File[], filesRef: React.MutableRefObject> -): Promise> { +): Promise> { return Promise.all( files.map(async (file) => { const fileId = createFileId(); @@ -347,7 +347,7 @@ async function processFilesIntoRecords( if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error); } - const record = toFileRecord(file, fileId); + const record = toWorkbenchFile(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; } @@ -365,10 +365,10 @@ async function processFilesIntoRecords( * Helper function to persist files to IndexedDB */ async function persistFilesToIndexedDB( - fileRecords: Array<{ file: File; fileId: FileId; thumbnail?: string }>, + workbenchFiles: Array<{ file: File; fileId: FileId; thumbnail?: string }>, indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise } ): Promise { - await Promise.all(fileRecords.map(async ({ file, fileId, thumbnail }) => { + await Promise.all(workbenchFiles.map(async ({ file, fileId, thumbnail }) => { try { await indexedDB.saveFile(file, fileId, thumbnail); } catch (error) { @@ -383,7 +383,6 @@ async function persistFilesToIndexedDB( export async function consumeFiles( inputFileIds: FileId[], outputFiles: File[], - stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise } | null @@ -391,11 +390,11 @@ export async function consumeFiles( if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`); // Process output files with thumbnails and metadata - const outputFileRecords = await processFilesIntoRecords(outputFiles, filesRef); + const outputWorkbenchFiles = await processFilesIntoRecords(outputFiles, filesRef); // Persist output files to IndexedDB if available if (indexedDB) { - await persistFilesToIndexedDB(outputFileRecords, indexedDB); + await persistFilesToIndexedDB(outputWorkbenchFiles, indexedDB); } // Dispatch the consume action @@ -403,21 +402,21 @@ export async function consumeFiles( type: 'CONSUME_FILES', payload: { inputFileIds, - outputFileRecords: outputFileRecords.map(({ record }) => record) + outputWorkbenchFiles: outputWorkbenchFiles.map(({ record }) => record) } }); - 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 ${outputWorkbenchFiles.length} outputs`); + // Return the output file IDs for undo tracking - return outputFileRecords.map(({ fileId }) => fileId); + return outputWorkbenchFiles.map(({ fileId }) => fileId); } /** * Helper function to restore files to filesRef and manage IndexedDB cleanup */ async function restoreFilesAndCleanup( - filesToRestore: Array<{ file: File; record: FileRecord }>, + filesToRestore: Array<{ file: File; record: WorkbenchFile }>, fileIdsToRemove: FileId[], filesRef: React.MutableRefObject>, indexedDB?: { deleteFile: (fileId: FileId) => Promise } | null @@ -440,7 +439,7 @@ async function restoreFilesAndCleanup( if (DEBUG) console.warn(`📄 Skipping empty file ${file.name}`); return; } - + // Restore the file to filesRef if (DEBUG) console.log(`📄 Restoring file ${file.name} with id ${record.id} to filesRef`); filesRef.current.set(record.id, file); @@ -455,7 +454,7 @@ async function restoreFilesAndCleanup( throw error; // Re-throw to trigger rollback }) ); - + // Execute all IndexedDB operations await Promise.all(indexedDBPromises); } @@ -466,28 +465,28 @@ async function restoreFilesAndCleanup( */ export async function undoConsumeFiles( inputFiles: File[], - inputFileRecords: FileRecord[], + inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[], stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { - if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputFileRecords.length} input files, removing ${outputFileIds.length} output files`); + if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputWorkbenchFiles.length} input files, removing ${outputFileIds.length} output files`); // Validate inputs - if (inputFiles.length !== inputFileRecords.length) { - throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputFileRecords.length})`); + if (inputFiles.length !== inputWorkbenchFiles.length) { + throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputWorkbenchFiles.length})`); } // Create a backup of current filesRef state for rollback const backupFilesRef = new Map(filesRef.current); - + try { // Prepare files to restore const filesToRestore = inputFiles.map((file, index) => ({ file, - record: inputFileRecords[index] + record: inputWorkbenchFiles[index] })); // Restore input files and clean up output files @@ -502,13 +501,13 @@ export async function undoConsumeFiles( dispatch({ type: 'UNDO_CONSUME_FILES', payload: { - inputFileRecords, + inputWorkbenchFiles, outputFileIds } }); - if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputFileRecords.length} inputs, removed ${outputFileIds.length} outputs`); - + if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputWorkbenchFiles.length} inputs, removed ${outputFileIds.length} outputs`); + } catch (error) { // Rollback filesRef to previous state if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error); diff --git a/frontend/src/contexts/file/fileHooks.ts b/frontend/src/contexts/file/fileHooks.ts index 609d38ab5..c78c10681 100644 --- a/frontend/src/contexts/file/fileHooks.ts +++ b/frontend/src/contexts/file/fileHooks.ts @@ -9,7 +9,7 @@ import { FileContextStateValue, FileContextActionsValue } from './contexts'; -import { FileRecord, FileWithId } from '../../types/fileContext'; +import { WorkbenchFile, StirlingFile } from '../../types/fileContext'; import { FileId } from '../../types/file'; /** @@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue { /** * Hook for current/primary file (first in list) */ -export function useCurrentFile(): { file?: File; record?: FileRecord } { +export function useCurrentFile(): { file?: File; record?: WorkbenchFile } { const { state, selectors } = useFileState(); const primaryFileId = state.files.ids[0]; return useMemo(() => ({ file: primaryFileId ? selectors.getFile(primaryFileId) : undefined, - record: primaryFileId ? selectors.getFileRecord(primaryFileId) : undefined + record: primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : undefined }), [primaryFileId, selectors]); } @@ -87,7 +87,7 @@ export function useFileManagement() { addFiles: actions.addFiles, removeFiles: actions.removeFiles, clearAllFiles: actions.clearAllFiles, - updateFileRecord: actions.updateFileRecord, + updateWorkbenchFile: actions.updateWorkbenchFile, reorderFiles: actions.reorderFiles }), [actions]); } @@ -111,24 +111,24 @@ export function useFileUI() { /** * Hook for specific file by ID (optimized for individual file access) */ -export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } { +export function useWorkbenchFile(fileId: FileId): { file?: File; record?: WorkbenchFile } { const { selectors } = useFileState(); return useMemo(() => ({ file: selectors.getFile(fileId), - record: selectors.getFileRecord(fileId) + record: selectors.getWorkbenchFile(fileId) }), [fileId, selectors]); } /** * Hook for all files (use sparingly - causes re-renders on file list changes) */ -export function useAllFiles(): { files: FileWithId[]; records: FileRecord[]; fileIds: FileId[] } { +export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getFiles(), - records: selectors.getFileRecords(), + records: selectors.getWorkbenchFiles(), fileIds: state.files.ids }), [state.files.ids, selectors]); } @@ -136,12 +136,12 @@ export function useAllFiles(): { files: FileWithId[]; records: FileRecord[]; fil /** * Hook for selected files (optimized for selection-based UI) */ -export function useSelectedFiles(): { files: FileWithId[]; records: FileRecord[]; fileIds: FileId[] } { +export function useSelectedFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getSelectedFiles(), - records: selectors.getSelectedFileRecords(), + records: selectors.getSelectedWorkbenchFiles(), fileIds: state.ui.selectedFileIds }), [state.ui.selectedFileIds, selectors]); } diff --git a/frontend/src/contexts/file/fileSelectors.ts b/frontend/src/contexts/file/fileSelectors.ts index e5876813b..6842a157f 100644 --- a/frontend/src/contexts/file/fileSelectors.ts +++ b/frontend/src/contexts/file/fileSelectors.ts @@ -4,11 +4,11 @@ import { FileId } from '../../types/file'; import { - FileRecord, + WorkbenchFile, FileContextState, FileContextSelectors, - FileWithId, - createFileWithId + StirlingFile, + createStirlingFile } from '../../types/fileContext'; /** @@ -21,7 +21,7 @@ export function createFileSelectors( return { getFile: (id: FileId) => { const file = filesRef.current.get(id); - return file ? createFileWithId(file, id) : undefined; + return file ? createStirlingFile(file, id) : undefined; }, getFiles: (ids?: FileId[]) => { @@ -29,14 +29,14 @@ export function createFileSelectors( return currentIds .map(id => { const file = filesRef.current.get(id); - return file ? createFileWithId(file, id) : undefined; + return file ? createStirlingFile(file, id) : undefined; }) - .filter(Boolean) as FileWithId[]; + .filter(Boolean) as StirlingFile[]; }, - getFileRecord: (id: FileId) => stateRef.current.files.byId[id], + getWorkbenchFile: (id: FileId) => stateRef.current.files.byId[id], - getFileRecords: (ids?: FileId[]) => { + getWorkbenchFiles: (ids?: FileId[]) => { const currentIds = ids || stateRef.current.files.ids; return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean); }, @@ -47,12 +47,12 @@ export function createFileSelectors( return stateRef.current.ui.selectedFileIds .map(id => { const file = filesRef.current.get(id); - return file ? createFileWithId(file, id) : undefined; + return file ? createStirlingFile(file, id) : undefined; }) - .filter(Boolean) as FileWithId[]; + .filter(Boolean) as StirlingFile[]; }, - getSelectedFileRecords: () => { + getSelectedWorkbenchFiles: () => { return stateRef.current.ui.selectedFileIds .map(id => stateRef.current.files.byId[id]) .filter(Boolean); @@ -67,18 +67,18 @@ export function createFileSelectors( return Array.from(stateRef.current.pinnedFiles) .map(id => { const file = filesRef.current.get(id); - return file ? createFileWithId(file, id) : undefined; + return file ? createStirlingFile(file, id) : undefined; }) - .filter(Boolean) as FileWithId[]; + .filter(Boolean) as StirlingFile[]; }, - getPinnedFileRecords: () => { + getPinnedWorkbenchFiles: () => { return Array.from(stateRef.current.pinnedFiles) .map(id => stateRef.current.files.byId[id]) .filter(Boolean); }, - isFilePinned: (file: FileWithId) => { + isFilePinned: (file: StirlingFile) => { return stateRef.current.pinnedFiles.has(file.fileId); }, @@ -98,9 +98,9 @@ export function createFileSelectors( /** * Helper for building quickKey sets for deduplication */ -export function buildQuickKeySet(fileRecords: Record): Set { +export function buildQuickKeySet(workbenchFiles: Record): Set { const quickKeys = new Set(); - Object.values(fileRecords).forEach(record => { + Object.values(workbenchFiles).forEach(record => { if (record.quickKey) { quickKeys.add(record.quickKey); } @@ -127,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz export function getPrimaryFile( stateRef: React.MutableRefObject, filesRef: React.MutableRefObject> -): { file?: File; record?: FileRecord } { +): { file?: File; record?: WorkbenchFile } { const primaryFileId = stateRef.current.files.ids[0]; if (!primaryFileId) return {}; diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 9986fe585..01b02b8c3 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -3,7 +3,7 @@ */ import { FileId } from '../../types/file'; -import { FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext'; +import { FileContextAction, WorkbenchFile, ProcessedFilePage } from '../../types/fileContext'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -166,7 +166,7 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateFileRecord = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { + updateWorkbenchFile = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { // Guard against updating removed files (race condition protection) if (!this.filesRef.current.has(fileId)) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); diff --git a/frontend/src/hooks/tools/shared/useBaseTool.ts b/frontend/src/hooks/tools/shared/useBaseTool.ts index 4995803e5..643e9bca5 100644 --- a/frontend/src/hooks/tools/shared/useBaseTool.ts +++ b/frontend/src/hooks/tools/shared/useBaseTool.ts @@ -4,11 +4,11 @@ import { useEndpointEnabled } from '../../useEndpointConfig'; import { BaseToolProps } from '../../../types/tool'; import { ToolOperationHook } from './useToolOperation'; import { BaseParametersHook } from './useBaseParameters'; -import { FileWithId } from '../../../types/fileContext'; +import { StirlingFile } from '../../../types/fileContext'; interface BaseToolReturn { // File management - selectedFiles: FileWithId[]; + selectedFiles: StirlingFile[]; // Tool-specific hooks params: BaseParametersHook; diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index 03275b0e7..05d7929f7 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -6,7 +6,7 @@ import { useToolState, type ProcessingProgress } from './useToolState'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolResources } from './useToolResources'; import { extractErrorMessage } from '../../../utils/toolErrorHandler'; -import { FileWithId, extractFiles, FileId, FileRecord } from '../../../types/fileContext'; +import { StirlingFile, extractFiles, FileId, WorkbenchFile } from '../../../types/fileContext'; import { ResponseHandler } from '../../../utils/toolResponseProcessor'; // Re-export for backwards compatibility @@ -102,7 +102,7 @@ export interface ToolOperationHook { progress: ProcessingProgress | null; // Actions - executeOperation: (params: TParams, selectedFiles: FileWithId[]) => Promise; + executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise; resetResults: () => void; clearError: () => void; cancelOperation: () => void; @@ -138,13 +138,13 @@ export const useToolOperation = ( // Track last operation for undo functionality const lastOperationRef = useRef<{ inputFiles: File[]; - inputFileRecords: FileRecord[]; + inputWorkbenchFiles: WorkbenchFile[]; outputFileIds: FileId[]; } | null>(null); const executeOperation = useCallback(async ( params: TParams, - selectedFiles: FileWithId[] + selectedFiles: StirlingFile[] ): Promise => { // Validation if (selectedFiles.length === 0) { @@ -168,7 +168,7 @@ export const useToolOperation = ( try { let processedFiles: File[]; - // Convert FileWithId to regular File objects for API processing + // Convert StirlingFile to regular File objects for API processing const validRegularFiles = extractFiles(validFiles); switch (config.toolType) { @@ -240,15 +240,15 @@ export const useToolOperation = ( // Replace input files with processed files (consumeFiles handles pinning) const inputFileIds: FileId[] = []; - const inputFileRecords: FileRecord[] = []; + const inputWorkbenchFiles: WorkbenchFile[] = []; // Build parallel arrays of IDs and records for undo tracking for (const file of validFiles) { const fileId = file.fileId; - const record = selectors.getFileRecord(fileId); + const record = selectors.getWorkbenchFile(fileId); if (record) { inputFileIds.push(fileId); - inputFileRecords.push(record); + inputWorkbenchFiles.push(record); } else { console.warn(`No file record found for file: ${file.name}`); } @@ -259,7 +259,7 @@ export const useToolOperation = ( // Store operation data for undo (only store what we need to avoid memory bloat) lastOperationRef.current = { inputFiles: extractFiles(validFiles), // Convert to File objects for undo - inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues + inputWorkbenchFiles: inputWorkbenchFiles.map(record => ({ ...record })), // Deep copy to avoid reference issues outputFileIds }; @@ -302,10 +302,10 @@ export const useToolOperation = ( return; } - const { inputFiles, inputFileRecords, outputFileIds } = lastOperationRef.current; + const { inputFiles, inputWorkbenchFiles, outputFileIds } = lastOperationRef.current; // Validate that we have data to undo - if (inputFiles.length === 0 || inputFileRecords.length === 0) { + if (inputFiles.length === 0 || inputWorkbenchFiles.length === 0) { actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data')); return; } @@ -317,7 +317,7 @@ export const useToolOperation = ( try { // Undo the consume operation - await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds); + await undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds); // Clear results and operation tracking resetResults(); diff --git a/frontend/src/hooks/useFileWithUrl.ts b/frontend/src/hooks/useFileWithUrl.ts index 84c06baa2..5176c1225 100644 --- a/frontend/src/hooks/useFileWithUrl.ts +++ b/frontend/src/hooks/useFileWithUrl.ts @@ -9,7 +9,7 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u return useMemo(() => { if (!file) return null; - // Validate that file is a proper File, FileWithId, or Blob object + // Validate that file is a proper File, StirlingFile, or Blob object if (!isFileObject(file) && !(file instanceof Blob)) { console.warn('useFileWithUrl: Expected File or Blob, got:', file); return null; diff --git a/frontend/src/hooks/usePdfSignatureDetection.ts b/frontend/src/hooks/usePdfSignatureDetection.ts index 84075f972..b14c1a637 100644 --- a/frontend/src/hooks/usePdfSignatureDetection.ts +++ b/frontend/src/hooks/usePdfSignatureDetection.ts @@ -1,14 +1,14 @@ import { useState, useEffect } from 'react'; import * as pdfjsLib from 'pdfjs-dist'; import { pdfWorkerManager } from '../services/pdfWorkerManager'; -import { FileWithId } from '../types/fileContext'; +import { StirlingFile } from '../types/fileContext'; export interface PdfSignatureDetectionResult { hasDigitalSignatures: boolean; isChecking: boolean; } -export const usePdfSignatureDetection = (files: FileWithId[]): PdfSignatureDetectionResult => { +export const usePdfSignatureDetection = (files: StirlingFile[]): PdfSignatureDetectionResult => { const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false); const [isChecking, setIsChecking] = useState(false); diff --git a/frontend/src/tests/convert/ConvertIntegration.test.tsx b/frontend/src/tests/convert/ConvertIntegration.test.tsx index 37a25c2a5..4efb41d7e 100644 --- a/frontend/src/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertIntegration.test.tsx @@ -18,8 +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'; +import { createTestStirlingFile } from '../utils/testFileHelpers'; +import { StirlingFile } from '../../types/fileContext'; // Mock axios vi.mock('axios'); @@ -57,9 +57,9 @@ const createTestFile = (name: string, content: string, type: string): File => { return new File([content], name, { type }); }; -const createPDFFile = (): FileWithId => { +const createPDFFile = (): StirlingFile => { 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 createTestFileWithId('test.pdf', pdfContent, 'application/pdf'); + return createTestStirlingFile('test.pdf', pdfContent, 'application/pdf'); }; // Test wrapper component @@ -164,7 +164,7 @@ describe('Convert Tool Integration Tests', () => { wrapper: TestWrapper }); - const testFile = createTestFileWithId('invalid.txt', 'not a pdf', 'text/plain'); + const testFile = createTestStirlingFile('invalid.txt', 'not a pdf', 'text/plain'); const parameters: ConvertParameters = { fromExtension: 'pdf', toExtension: 'png', @@ -428,7 +428,7 @@ describe('Convert Tool Integration Tests', () => { }); const files = [ createPDFFile(), - createTestFileWithId('test2.pdf', '%PDF-1.4...', 'application/pdf') + createTestStirlingFile('test2.pdf', '%PDF-1.4...', 'application/pdf') ] const parameters: ConvertParameters = { fromExtension: 'pdf', @@ -529,7 +529,7 @@ describe('Convert Tool Integration Tests', () => { wrapper: TestWrapper }); - const corruptedFile = createTestFileWithId('corrupted.pdf', 'not-a-pdf', 'application/pdf'); + const corruptedFile = createTestStirlingFile('corrupted.pdf', 'not-a-pdf', 'application/pdf'); const parameters: ConvertParameters = { fromExtension: 'pdf', toExtension: 'png', diff --git a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx index 3ba6cc589..2904135e0 100644 --- a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx @@ -14,8 +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'; +import { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers'; +import { StirlingFile } from '../../types/fileContext'; // Mock axios vi.mock('axios'); @@ -83,7 +83,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock DOCX file - const docxFile = createTestFileWithId('document.docx', 'docx content', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + const docxFile = createTestStirlingFile('document.docx', 'docx content', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); // Test auto-detection act(() => { @@ -119,7 +119,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock unknown file - const unknownFile = createTestFileWithId('document.xyz', 'unknown content', 'application/octet-stream'); + const unknownFile = createTestStirlingFile('document.xyz', 'unknown content', 'application/octet-stream'); // Test auto-detection act(() => { @@ -290,7 +290,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const htmlFile = createTestFileWithId('page.html', 'content', 'text/html'); + const htmlFile = createTestStirlingFile('page.html', 'content', 'text/html'); // Set up HTML conversion parameters act(() => { @@ -320,7 +320,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const emlFile = createTestFileWithId('email.eml', 'email content', 'message/rfc822'); + const emlFile = createTestStirlingFile('email.eml', 'email content', 'message/rfc822'); // Set up email conversion parameters act(() => { @@ -357,7 +357,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const pdfFile = createTestFileWithId('document.pdf', 'pdf content', 'application/pdf'); + const pdfFile = createTestStirlingFile('document.pdf', 'pdf content', 'application/pdf'); // Set up PDF/A conversion parameters act(() => { diff --git a/frontend/src/tests/utils/testFileHelpers.ts b/frontend/src/tests/utils/testFileHelpers.ts index be8cd9774..80b3c74cf 100644 --- a/frontend/src/tests/utils/testFileHelpers.ts +++ b/frontend/src/tests/utils/testFileHelpers.ts @@ -1,28 +1,28 @@ /** - * Test utilities for creating FileWithId objects in tests + * Test utilities for creating StirlingFile objects in tests */ -import { FileWithId, createFileWithId } from '../../types/fileContext'; +import { StirlingFile, createStirlingFile } from '../../types/fileContext'; /** - * Create a FileWithId object for testing purposes + * Create a StirlingFile object for testing purposes */ -export function createTestFileWithId( +export function createTestStirlingFile( name: string, content: string = 'test content', type: string = 'application/pdf' -): FileWithId { +): StirlingFile { const file = new File([content], name, { type }); - return createFileWithId(file); + return createStirlingFile(file); } /** - * Create multiple FileWithId objects for testing + * Create multiple StirlingFile objects for testing */ export function createTestFilesWithId( files: Array<{ name: string; content?: string; type?: string }> -): FileWithId[] { +): StirlingFile[] { return files.map(({ name, content = 'test content', type = 'application/pdf' }) => - createTestFileWithId(name, content, type) + createTestStirlingFile(name, content, type) ); } \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 5520e2cde..2d5c9e034 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -44,7 +44,7 @@ export interface ProcessedFileMetadata { [key: string]: any; } -export interface FileRecord { +export interface WorkbenchFile { id: FileId; name: string; size: number; @@ -62,7 +62,7 @@ export interface FileRecord { export interface FileContextNormalizedFiles { ids: FileId[]; - byId: Record; + byId: Record; } // Helper functions - UUID-based primary keys (zero collisions, synchronous) @@ -85,60 +85,60 @@ export function createQuickKey(file: File): string { return `${file.name}|${file.size}|${file.lastModified}`; } -// File with embedded UUID - replaces loose File + FileId parameter passing -export interface FileWithId extends File { +// Stirling PDF file with embedded UUID - replaces loose File + FileId parameter passing +export interface StirlingFile extends File { readonly fileId: FileId; readonly quickKey: string; // Fast deduplication key: name|size|lastModified } // Type guard to check if a File object has an embedded fileId -export function isFileWithId(file: File): file is FileWithId { +export function isStirlingFile(file: File): file is StirlingFile { return 'fileId' in file && typeof (file as any).fileId === 'string' && 'quickKey' in file && typeof (file as any).quickKey === 'string'; } -// Create a FileWithId from a regular File object -export function createFileWithId(file: File, id?: FileId): FileWithId { +// Create a StirlingFile from a regular File object +export function createStirlingFile(file: File, id?: FileId): StirlingFile { const fileId = id || createFileId(); const quickKey = createQuickKey(file); - + // File properties are not enumerable, so we need to copy them explicitly // This avoids prototype chain issues while preserving all File functionality - const fileWithId = { + const stirlingFile = { // Explicitly copy File properties (they're not enumerable) name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, webkitRelativePath: file.webkitRelativePath, - + // Add our custom properties fileId: fileId, quickKey: quickKey, - + // Preserve File prototype methods by binding them to the original file arrayBuffer: file.arrayBuffer.bind(file), slice: file.slice.bind(file), stream: file.stream.bind(file), text: file.text.bind(file) - } as FileWithId; - - return fileWithId; + } as StirlingFile; + + return stirlingFile; } -// Extract FileIds from FileWithId array -export function extractFileIds(files: FileWithId[]): FileId[] { +// Extract FileIds from StirlingFile array +export function extractFileIds(files: StirlingFile[]): FileId[] { return files.map(file => file.fileId); } -// Extract regular File objects from FileWithId array -export function extractFiles(files: FileWithId[]): File[] { +// Extract regular File objects from StirlingFile array +export function extractFiles(files: StirlingFile[]): File[] { return files as File[]; } -// Check if an object is a File or FileWithId (replaces instanceof File checks) -export function isFileObject(obj: any): obj is File | FileWithId { - return obj && +// Check if an object is a File or StirlingFile (replaces instanceof File checks) +export function isFileObject(obj: any): obj is File | StirlingFile { + return obj && typeof obj.name === 'string' && typeof obj.size === 'number' && typeof obj.type === 'string' && @@ -148,7 +148,7 @@ export function isFileObject(obj: any): obj is File | FileWithId { -export function toFileRecord(file: File, id?: FileId): FileRecord { +export function toWorkbenchFile(file: File, id?: FileId): WorkbenchFile { const fileId = id || createFileId(); return { id: fileId, @@ -161,7 +161,7 @@ export function toFileRecord(file: File, id?: FileId): FileRecord { }; } -export function revokeFileResources(record: FileRecord): void { +export function revokeFileResources(record: WorkbenchFile): void { // Only revoke blob: URLs to prevent errors on other schemes if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) { try { @@ -235,7 +235,7 @@ export interface FileContextState { // Core file management - lightweight file IDs only files: { ids: FileId[]; - byId: Record; + byId: Record; }; // Pinned files - files that won't be consumed by tools @@ -254,16 +254,16 @@ export interface FileContextState { // Action types for reducer pattern export type FileContextAction = // File management actions - | { type: 'ADD_FILES'; payload: { fileRecords: FileRecord[] } } + | { type: 'ADD_FILES'; payload: { workbenchFiles: WorkbenchFile[] } } | { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } } - | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } + | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } | { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } } // Pinned files actions | { type: 'PIN_FILE'; payload: { fileId: FileId } } | { type: 'UNPIN_FILE'; payload: { fileId: FileId } } - | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputFileRecords: FileRecord[] } } - | { type: 'UNDO_CONSUME_FILES'; payload: { inputFileRecords: FileRecord[]; outputFileIds: FileId[] } } + | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputWorkbenchFiles: WorkbenchFile[] } } + | { type: 'UNDO_CONSUME_FILES'; payload: { inputWorkbenchFiles: WorkbenchFile[]; outputFileIds: FileId[] } } // UI actions | { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } } @@ -279,22 +279,22 @@ export type FileContextAction = export interface FileContextActions { // File management - lightweight actions only - addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise; - addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise; - addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise; + addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise; + addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise; + addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise; removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise; - updateFileRecord: (id: FileId, updates: Partial) => void; + updateWorkbenchFile: (id: FileId, updates: Partial) => void; reorderFiles: (orderedFileIds: FileId[]) => void; clearAllFiles: () => Promise; clearAllData: () => Promise; - // File pinning - accepts FileWithId for safer type checking - pinFile: (file: FileWithId) => void; - unpinFile: (file: FileWithId) => void; + // File pinning - accepts StirlingFile for safer type checking + pinFile: (file: StirlingFile) => void; + unpinFile: (file: StirlingFile) => void; // File consumption (replace unpinned files with outputs) consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; - undoConsumeFiles: (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]) => Promise; + undoConsumeFiles: (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]) => Promise; // Selection management setSelectedFiles: (fileIds: FileId[]) => void; setSelectedPages: (pageNumbers: number[]) => void; @@ -317,17 +317,17 @@ export interface FileContextActions { // File selectors (separate from actions to avoid re-renders) export interface FileContextSelectors { - getFile: (id: FileId) => FileWithId | undefined; - getFiles: (ids?: FileId[]) => FileWithId[]; - getFileRecord: (id: FileId) => FileRecord | undefined; - getFileRecords: (ids?: FileId[]) => FileRecord[]; + getFile: (id: FileId) => StirlingFile | undefined; + getFiles: (ids?: FileId[]) => StirlingFile[]; + getWorkbenchFile: (id: FileId) => WorkbenchFile | undefined; + getWorkbenchFiles: (ids?: FileId[]) => WorkbenchFile[]; getAllFileIds: () => FileId[]; - getSelectedFiles: () => FileWithId[]; - getSelectedFileRecords: () => FileRecord[]; + getSelectedFiles: () => StirlingFile[]; + getSelectedWorkbenchFiles: () => WorkbenchFile[]; getPinnedFileIds: () => FileId[]; - getPinnedFiles: () => FileWithId[]; - getPinnedFileRecords: () => FileRecord[]; - isFilePinned: (file: FileWithId) => boolean; + getPinnedFiles: () => StirlingFile[]; + getPinnedWorkbenchFiles: () => WorkbenchFile[]; + isFilePinned: (file: StirlingFile) => boolean; getFilesSignature: () => string; } diff --git a/frontend/src/types/fileIdSafety.d.ts b/frontend/src/types/fileIdSafety.d.ts index 3235d3b3a..9316af45a 100644 --- a/frontend/src/types/fileIdSafety.d.ts +++ b/frontend/src/types/fileIdSafety.d.ts @@ -2,7 +2,7 @@ * Type safety declarations to prevent file.name/UUID confusion */ -import { FileId, FileWithId, OperationType, FileOperation } from './fileContext'; +import { FileId, StirlingFile, OperationType, FileOperation } from './fileContext'; declare global { namespace FileIdSafety { @@ -13,15 +13,15 @@ declare global { : T : T; - // Mark functions that should only accept FileWithId, not regular File - type FileWithIdOnlyFunction any> = T extends (...args: infer P) => infer R + // Mark functions that should only accept StirlingFile, not regular File + type StirlingFileOnlyFunction any> = T extends (...args: infer P) => infer R ? P extends readonly [File, ...any[]] - ? never // Reject File parameters in first position for FileWithId functions + ? never // Reject File parameters in first position for StirlingFile functions : T : T; - // Utility type to enforce FileWithId usage - type RequireFileWithId = T extends File ? FileWithId : T; + // Utility type to enforce StirlingFile usage + type RequireStirlingFile = T extends File ? StirlingFile : T; } // Extend Window interface to add runtime validation helpers @@ -31,19 +31,19 @@ declare global { } } -// Augment FileContext types to prevent bypassing FileWithId +// Augment FileContext types to prevent bypassing StirlingFile declare module '../contexts/FileContext' { export interface StrictFileContextActions { - pinFile: (file: FileWithId) => void; // Must be FileWithId - unpinFile: (file: FileWithId) => void; // Must be FileWithId - addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise; // Returns FileWithId - consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; // Returns FileWithId + pinFile: (file: StirlingFile) => void; // Must be StirlingFile + unpinFile: (file: StirlingFile) => void; // Must be StirlingFile + addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise; // Returns StirlingFile + consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; // Returns StirlingFile } export interface StrictFileContextSelectors { - getFile: (id: FileId) => FileWithId | undefined; // Returns FileWithId - getFiles: (ids?: FileId[]) => FileWithId[]; // Returns FileWithId[] - isFilePinned: (file: FileWithId) => boolean; // Must be FileWithId + getFile: (id: FileId) => StirlingFile | undefined; // Returns StirlingFile + getFiles: (ids?: FileId[]) => StirlingFile[]; // Returns StirlingFile[] + isFilePinned: (file: StirlingFile) => boolean; // Must be StirlingFile } }