mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Update naming of file types
This commit is contained in:
parent
4886ef7ddb
commit
a571288f8f
@ -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'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -24,7 +24,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
|
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
|
||||||
|
|
||||||
// Wrapper for storeFile that generates UUID
|
// 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
|
const fileId = createFileId(); // Generate UUID for storage
|
||||||
return await storeFile(file, fileId);
|
return await storeFile(file, fileId);
|
||||||
}, [storeFile]);
|
}, [storeFile]);
|
||||||
|
@ -16,12 +16,12 @@ import styles from './FileEditor.module.css';
|
|||||||
import FileEditorThumbnail from './FileEditorThumbnail';
|
import FileEditorThumbnail from './FileEditorThumbnail';
|
||||||
import FilePickerModal from '../shared/FilePickerModal';
|
import FilePickerModal from '../shared/FilePickerModal';
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||||
import { FileId, FileWithId } from '../../types/fileContext';
|
import { FileId, StirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
|
|
||||||
interface FileEditorProps {
|
interface FileEditorProps {
|
||||||
onOpenPageEditor?: (file: FileWithId) => void;
|
onOpenPageEditor?: (file: StirlingFile) => void;
|
||||||
onMergeFiles?: (files: FileWithId[]) => void;
|
onMergeFiles?: (files: StirlingFile[]) => void;
|
||||||
toolMode?: boolean;
|
toolMode?: boolean;
|
||||||
showUpload?: boolean;
|
showUpload?: boolean;
|
||||||
showBulkActions?: boolean;
|
showBulkActions?: boolean;
|
||||||
@ -50,7 +50,7 @@ const FileEditor = ({
|
|||||||
|
|
||||||
// Extract needed values from state (memoized to prevent infinite loops)
|
// Extract needed values from state (memoized to prevent infinite loops)
|
||||||
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
|
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 selectedFileIds = state.ui.selectedFileIds;
|
||||||
const isProcessing = state.ui.isProcessing;
|
const isProcessing = state.ui.isProcessing;
|
||||||
|
|
||||||
@ -92,10 +92,10 @@ const FileEditor = ({
|
|||||||
const contextSelectedIdsRef = useRef<FileId[]>([]);
|
const contextSelectedIdsRef = useRef<FileId[]>([]);
|
||||||
contextSelectedIdsRef.current = contextSelectedIds;
|
contextSelectedIdsRef.current = contextSelectedIds;
|
||||||
|
|
||||||
// Use activeFileRecords directly - no conversion needed
|
// Use activeWorkbenchFiles directly - no conversion needed
|
||||||
const localSelectedIds = contextSelectedIds;
|
const localSelectedIds = contextSelectedIds;
|
||||||
|
|
||||||
// Helper to convert FileRecord to FileThumbnail format
|
// Helper to convert WorkbenchFile to FileThumbnail format
|
||||||
const recordToFileItem = useCallback((record: any) => {
|
const recordToFileItem = useCallback((record: any) => {
|
||||||
const file = selectors.getFile(record.id);
|
const file = selectors.getFile(record.id);
|
||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
@ -253,26 +253,26 @@ const FileEditor = ({
|
|||||||
}, [addFiles]);
|
}, [addFiles]);
|
||||||
|
|
||||||
const selectAll = useCallback(() => {
|
const selectAll = useCallback(() => {
|
||||||
setSelectedFiles(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly
|
setSelectedFiles(activeWorkbenchFiles.map(r => r.id)); // Use WorkbenchFile IDs directly
|
||||||
}, [activeFileRecords, setSelectedFiles]);
|
}, [activeWorkbenchFiles, setSelectedFiles]);
|
||||||
|
|
||||||
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
|
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
|
||||||
|
|
||||||
const closeAllFiles = useCallback(() => {
|
const closeAllFiles = useCallback(() => {
|
||||||
if (activeFileRecords.length === 0) return;
|
if (activeWorkbenchFiles.length === 0) return;
|
||||||
|
|
||||||
// Remove all files from context but keep in storage
|
// 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
|
removeFiles(allFileIds, false); // false = keep in storage
|
||||||
|
|
||||||
// Clear selections
|
// Clear selections
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
}, [activeFileRecords, removeFiles, setSelectedFiles]);
|
}, [activeWorkbenchFiles, removeFiles, setSelectedFiles]);
|
||||||
|
|
||||||
const toggleFile = useCallback((fileId: FileId) => {
|
const toggleFile = useCallback((fileId: FileId) => {
|
||||||
const currentSelectedIds = contextSelectedIdsRef.current;
|
const currentSelectedIds = contextSelectedIdsRef.current;
|
||||||
|
|
||||||
const targetRecord = activeFileRecords.find(r => r.id === fileId);
|
const targetRecord = activeWorkbenchFiles.find(r => r.id === fileId);
|
||||||
if (!targetRecord) return;
|
if (!targetRecord) return;
|
||||||
|
|
||||||
const contextFileId = fileId; // No need to create a new ID
|
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)
|
// Update context (this automatically updates tool selection since they use the same action)
|
||||||
setSelectedFiles(newSelection);
|
setSelectedFiles(newSelection);
|
||||||
}, [setSelectedFiles, toolMode, setStatus, activeFileRecords]);
|
}, [setSelectedFiles, toolMode, setStatus, activeWorkbenchFiles]);
|
||||||
|
|
||||||
const toggleSelectionMode = useCallback(() => {
|
const toggleSelectionMode = useCallback(() => {
|
||||||
setSelectionMode(prev => {
|
setSelectionMode(prev => {
|
||||||
@ -316,7 +316,7 @@ const FileEditor = ({
|
|||||||
|
|
||||||
// File reordering handler for drag and drop
|
// File reordering handler for drag and drop
|
||||||
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
|
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
|
// Find indices
|
||||||
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
|
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
|
||||||
@ -368,13 +368,13 @@ const FileEditor = ({
|
|||||||
// Update status
|
// Update status
|
||||||
const moveCount = filesToMove.length;
|
const moveCount = filesToMove.length;
|
||||||
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
||||||
}, [activeFileRecords, reorderFiles, setStatus]);
|
}, [activeWorkbenchFiles, reorderFiles, setStatus]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// File operations using context
|
// File operations using context
|
||||||
const handleDeleteFile = useCallback((fileId: FileId) => {
|
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;
|
const file = record ? selectors.getFile(record.id) : null;
|
||||||
|
|
||||||
if (record && file) {
|
if (record && file) {
|
||||||
@ -405,27 +405,27 @@ const FileEditor = ({
|
|||||||
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
|
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
|
||||||
setSelectedFiles(currentSelected);
|
setSelectedFiles(currentSelected);
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
|
}, [activeWorkbenchFiles, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
|
||||||
|
|
||||||
const handleViewFile = useCallback((fileId: FileId) => {
|
const handleViewFile = useCallback((fileId: FileId) => {
|
||||||
const record = activeFileRecords.find(r => r.id === fileId);
|
const record = activeWorkbenchFiles.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]);
|
||||||
navActions.setWorkbench('viewer');
|
navActions.setWorkbench('viewer');
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
|
}, [activeWorkbenchFiles, setSelectedFiles, navActions.setWorkbench]);
|
||||||
|
|
||||||
const handleMergeFromHere = useCallback((fileId: FileId) => {
|
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;
|
if (startIndex === -1) return;
|
||||||
|
|
||||||
const recordsToMerge = activeFileRecords.slice(startIndex);
|
const recordsToMerge = activeWorkbenchFiles.slice(startIndex);
|
||||||
const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as FileWithId[];
|
const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[];
|
||||||
if (onMergeFiles) {
|
if (onMergeFiles) {
|
||||||
onMergeFiles(filesToMerge);
|
onMergeFiles(filesToMerge);
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, selectors, onMergeFiles]);
|
}, [activeWorkbenchFiles, selectors, onMergeFiles]);
|
||||||
|
|
||||||
const handleSplitFile = useCallback((fileId: FileId) => {
|
const handleSplitFile = useCallback((fileId: FileId) => {
|
||||||
const file = selectors.getFile(fileId);
|
const file = selectors.getFile(fileId);
|
||||||
@ -467,7 +467,7 @@ const FileEditor = ({
|
|||||||
<Box p="md" pt="xl">
|
<Box p="md" pt="xl">
|
||||||
|
|
||||||
|
|
||||||
{activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? (
|
{activeWorkbenchFiles.length === 0 && !zipExtractionProgress.isExtracting ? (
|
||||||
<Center h="60vh">
|
<Center h="60vh">
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Text size="lg" c="dimmed">📁</Text>
|
<Text size="lg" c="dimmed">📁</Text>
|
||||||
@ -475,7 +475,7 @@ const FileEditor = ({
|
|||||||
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
|
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? (
|
) : activeWorkbenchFiles.length === 0 && zipExtractionProgress.isExtracting ? (
|
||||||
<Box>
|
<Box>
|
||||||
<SkeletonLoader type="controls" />
|
<SkeletonLoader type="controls" />
|
||||||
|
|
||||||
@ -522,7 +522,7 @@ const FileEditor = ({
|
|||||||
pointerEvents: 'auto'
|
pointerEvents: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeFileRecords.map((record, index) => {
|
{activeWorkbenchFiles.map((record, index) => {
|
||||||
const fileItem = recordToFileItem(record);
|
const fileItem = recordToFileItem(record);
|
||||||
if (!fileItem) return null;
|
if (!fileItem) return null;
|
||||||
|
|
||||||
@ -531,7 +531,7 @@ const FileEditor = ({
|
|||||||
key={record.id}
|
key={record.id}
|
||||||
file={fileItem}
|
file={fileItem}
|
||||||
index={index}
|
index={index}
|
||||||
totalFiles={activeFileRecords.length}
|
totalFiles={activeWorkbenchFiles.length}
|
||||||
selectedFiles={localSelectedIds}
|
selectedFiles={localSelectedIds}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
onToggleFile={toggleFile}
|
onToggleFile={toggleFile}
|
||||||
|
@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const globalProcessing = state.ui.isProcessing;
|
const globalProcessing = state.ui.isProcessing;
|
||||||
|
|
||||||
// Get primary file record outside useMemo to track processedFile changes
|
// Get primary file record outside useMemo to track processedFile changes
|
||||||
const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
|
const primaryWorkbenchFile = primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : null;
|
||||||
const processedFilePages = primaryFileRecord?.processedFile?.pages;
|
const processedFilePages = primaryWorkbenchFile?.processedFile?.pages;
|
||||||
const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages;
|
const processedFileTotalPages = primaryWorkbenchFile?.processedFile?.totalPages;
|
||||||
|
|
||||||
// Compute merged document with stable signature (prevents infinite loops)
|
// Compute merged document with stable signature (prevents infinite loops)
|
||||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||||
@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
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 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');
|
console.log('🎬 PageEditor: No primary file record found, showing loading');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name =
|
const name =
|
||||||
activeFileIds.length === 1
|
activeFileIds.length === 1
|
||||||
? (primaryFileRecord.name ?? 'document.pdf')
|
? (primaryWorkbenchFile.name ?? 'document.pdf')
|
||||||
: activeFileIds
|
: activeFileIds
|
||||||
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
.map(id => (selectors.getWorkbenchFile(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
||||||
.join(' + ');
|
.join(' + ');
|
||||||
|
|
||||||
// Build page insertion map from files with insertion positions
|
// Build page insertion map from files with insertion positions
|
||||||
@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const originalFileIds: FileId[] = [];
|
const originalFileIds: FileId[] = [];
|
||||||
|
|
||||||
activeFileIds.forEach(fileId => {
|
activeFileIds.forEach(fileId => {
|
||||||
const record = selectors.getFileRecord(fileId);
|
const record = selectors.getWorkbenchFile(fileId);
|
||||||
if (record?.insertAfterPageId !== undefined) {
|
if (record?.insertAfterPageId !== undefined) {
|
||||||
if (!insertionMap.has(record.insertAfterPageId)) {
|
if (!insertionMap.has(record.insertAfterPageId)) {
|
||||||
insertionMap.set(record.insertAfterPageId, []);
|
insertionMap.set(record.insertAfterPageId, []);
|
||||||
@ -72,12 +72,12 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
|
|
||||||
// Helper function to create pages from a file
|
// Helper function to create pages from a file
|
||||||
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
|
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
|
||||||
const fileRecord = selectors.getFileRecord(fileId);
|
const workbenchFiles = selectors.getWorkbenchFile(fileId);
|
||||||
if (!fileRecord) {
|
if (!workbenchFiles) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedFile = fileRecord.processedFile;
|
const processedFile = workbenchFiles.processedFile;
|
||||||
let filePages: PDFPage[] = [];
|
let filePages: PDFPage[] = [];
|
||||||
|
|
||||||
if (processedFile?.pages && processedFile.pages.length > 0) {
|
if (processedFile?.pages && processedFile.pages.length > 0) {
|
||||||
@ -159,7 +159,7 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return mergedDoc;
|
return mergedDoc;
|
||||||
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
}, [activeFileIds, primaryFileId, primaryWorkbenchFile, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
||||||
|
|
||||||
// Large document detection for smart loading
|
// Large document detection for smart loading
|
||||||
const isVeryLargeDocument = useMemo(() => {
|
const isVeryLargeDocument = useMemo(() => {
|
||||||
|
@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
|
|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
|
||||||
import { FileRecord } from "../../types/fileContext";
|
import { WorkbenchFile } from "../../types/fileContext";
|
||||||
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
||||||
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: File;
|
file: File;
|
||||||
record?: FileRecord;
|
record?: WorkbenchFile;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
|
@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import SortIcon from "@mui/icons-material/Sort";
|
import SortIcon from "@mui/icons-material/Sort";
|
||||||
import FileCard from "./FileCard";
|
import FileCard from "./FileCard";
|
||||||
import { FileRecord } from "../../types/fileContext";
|
import { WorkbenchFile } from "../../types/fileContext";
|
||||||
import { FileId } from "../../types/file";
|
import { FileId } from "../../types/file";
|
||||||
|
|
||||||
interface FileGridProps {
|
interface FileGridProps {
|
||||||
files: Array<{ file: File; record?: FileRecord }>;
|
files: Array<{ file: File; record?: WorkbenchFile }>;
|
||||||
onRemove?: (index: number) => void;
|
onRemove?: (index: number) => void;
|
||||||
onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
|
onDoubleClick?: (item: { file: File; record?: WorkbenchFile }) => void;
|
||||||
onView?: (item: { file: File; record?: FileRecord }) => void;
|
onView?: (item: { file: File; record?: WorkbenchFile }) => void;
|
||||||
onEdit?: (item: { file: File; record?: FileRecord }) => void;
|
onEdit?: (item: { file: File; record?: WorkbenchFile }) => void;
|
||||||
onSelect?: (fileId: FileId) => void;
|
onSelect?: (fileId: FileId) => void;
|
||||||
selectedFiles?: FileId[];
|
selectedFiles?: FileId[];
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
@ -125,7 +125,7 @@ const FileGrid = ({
|
|||||||
>
|
>
|
||||||
{displayFiles.map((item, idx) => {
|
{displayFiles.map((item, idx) => {
|
||||||
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 WorkbenchFile with proper ID:', item.file.name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const fileId = item.record.id;
|
const fileId = item.record.id;
|
||||||
|
@ -34,7 +34,6 @@ export default function RightRail() {
|
|||||||
|
|
||||||
const activeFiles = selectors.getFiles();
|
const activeFiles = selectors.getFiles();
|
||||||
const filesSignature = selectors.getFilesSignature();
|
const filesSignature = selectors.getFilesSignature();
|
||||||
const fileRecords = selectors.getFileRecords();
|
|
||||||
|
|
||||||
// Compute selection state and total items
|
// Compute selection state and total items
|
||||||
const getSelectionState = useCallback(() => {
|
const getSelectionState = useCallback(() => {
|
||||||
@ -85,7 +84,7 @@ export default function RightRail() {
|
|||||||
if (currentView === 'fileEditor' || currentView === 'viewer') {
|
if (currentView === 'fileEditor' || currentView === 'viewer') {
|
||||||
// Download selected files (or all if none selected)
|
// Download selected files (or all if none selected)
|
||||||
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||||
|
|
||||||
filesToDownload.forEach(file => {
|
filesToDownload.forEach(file => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(file);
|
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 */}
|
{/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */}
|
||||||
<div
|
<div
|
||||||
className={`right-rail-slot ${currentView !== 'viewer' ? 'visible right-rail-enter' : 'right-rail-exit'}`}
|
className={`right-rail-slot ${currentView !== 'viewer' ? 'visible right-rail-enter' : 'right-rail-exit'}`}
|
||||||
aria-hidden={currentView === 'viewer'}
|
aria-hidden={currentView === 'viewer'}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
@ -358,14 +357,14 @@ export default function RightRail() {
|
|||||||
<LanguageSelector position="left-start" offset={6} compact />
|
<LanguageSelector position="left-start" offset={6} compact />
|
||||||
|
|
||||||
<Tooltip content={
|
<Tooltip content={
|
||||||
currentView === 'pageEditor'
|
currentView === 'pageEditor'
|
||||||
? t('rightRail.exportAll', 'Export PDF')
|
? t('rightRail.exportAll', 'Export PDF')
|
||||||
: (selectedCount > 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
|
: (selectedCount > 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
|
||||||
} position="left" offset={12} arrow>
|
} position="left" offset={12} arrow>
|
||||||
<div>
|
<div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={handleExportAll}
|
onClick={handleExportAll}
|
||||||
disabled={currentView === 'viewer' || totalItems === 0}
|
disabled={currentView === 'viewer' || totalItems === 0}
|
||||||
|
@ -22,13 +22,13 @@ import {
|
|||||||
OUTPUT_OPTIONS,
|
OUTPUT_OPTIONS,
|
||||||
FIT_OPTIONS
|
FIT_OPTIONS
|
||||||
} from "../../../constants/convertConstants";
|
} from "../../../constants/convertConstants";
|
||||||
import { FileWithId } from "../../../types/fileContext";
|
import { StirlingFile } 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: FileWithId[];
|
selectedFiles: StirlingFile[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ const ConvertSettings = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterFilesByExtension = (extension: string) => {
|
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 => {
|
return files.filter(file => {
|
||||||
const fileExtension = detectFileExtension(file.name);
|
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);
|
const fileIds = files.map(file => file.fileId);
|
||||||
setSelectedFiles(fileIds);
|
setSelectedFiles(fileIds);
|
||||||
};
|
};
|
||||||
|
@ -3,12 +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';
|
import { StirlingFile } 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: FileWithId[];
|
selectedFiles: StirlingFile[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,10 +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";
|
import { StirlingFile } from "../../../types/fileContext";
|
||||||
|
|
||||||
export interface FileStatusIndicatorProps {
|
export interface FileStatusIndicatorProps {
|
||||||
selectedFiles?: FileWithId[];
|
selectedFiles?: StirlingFile[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +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';
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
export interface FilesToolStepProps {
|
export interface FilesToolStepProps {
|
||||||
selectedFiles: FileWithId[];
|
selectedFiles: StirlingFile[];
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
onCollapsedClick?: () => void;
|
onCollapsedClick?: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -4,10 +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';
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
export interface FilesStepConfig {
|
export interface FilesStepConfig {
|
||||||
selectedFiles: FileWithId[];
|
selectedFiles: StirlingFile[];
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onCollapsedClick?: () => void;
|
onCollapsedClick?: () => void;
|
||||||
|
@ -20,9 +20,9 @@ import {
|
|||||||
FileContextActionsValue,
|
FileContextActionsValue,
|
||||||
FileContextActions,
|
FileContextActions,
|
||||||
FileId,
|
FileId,
|
||||||
FileRecord,
|
WorkbenchFile,
|
||||||
FileWithId,
|
StirlingFile,
|
||||||
createFileWithId
|
createStirlingFile
|
||||||
} from '../types/fileContext';
|
} from '../types/fileContext';
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
@ -81,7 +81,7 @@ function FileContextInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File operations using unified addFiles helper with persistence
|
// File operations using unified addFiles helper with persistence
|
||||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<FileWithId[]> => {
|
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||||
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// 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]);
|
}, [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<StirlingFile[]> => {
|
||||||
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
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<FileWithId[]> => {
|
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -116,7 +116,7 @@ function FileContextInner({
|
|||||||
selectFiles(result);
|
selectFiles(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map(({ file, id }) => createFileWithId(file, id));
|
return result.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Action creators
|
// Action creators
|
||||||
@ -127,8 +127,8 @@ function FileContextInner({
|
|||||||
return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
|
return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise<void> => {
|
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]): Promise<void> => {
|
||||||
return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
|
return undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
// Helper to find FileId from File object
|
// Helper to find FileId from File object
|
||||||
@ -142,12 +142,12 @@ function FileContextInner({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// File pinning functions - use FileWithId directly
|
// File pinning functions - use StirlingFile directly
|
||||||
const pinFileWrapper = useCallback((file: FileWithId) => {
|
const pinFileWrapper = useCallback((file: StirlingFile) => {
|
||||||
baseActions.pinFile(file.fileId);
|
baseActions.pinFile(file.fileId);
|
||||||
}, [baseActions]);
|
}, [baseActions]);
|
||||||
|
|
||||||
const unpinFileWrapper = useCallback((file: FileWithId) => {
|
const unpinFileWrapper = useCallback((file: StirlingFile) => {
|
||||||
baseActions.unpinFile(file.fileId);
|
baseActions.unpinFile(file.fileId);
|
||||||
}, [baseActions]);
|
}, [baseActions]);
|
||||||
|
|
||||||
@ -170,8 +170,8 @@ function FileContextInner({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateFileRecord: (fileId: FileId, updates: Partial<FileRecord>) =>
|
updateWorkbenchFile: (fileId: FileId, updates: Partial<WorkbenchFile>) =>
|
||||||
lifecycleManager.updateFileRecord(fileId, updates, stateRef),
|
lifecycleManager.updateWorkbenchFile(fileId, updates, stateRef),
|
||||||
reorderFiles: (orderedFileIds: FileId[]) => {
|
reorderFiles: (orderedFileIds: FileId[]) => {
|
||||||
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
|
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
|
||||||
},
|
},
|
||||||
@ -295,7 +295,7 @@ export {
|
|||||||
useFileSelection,
|
useFileSelection,
|
||||||
useFileManagement,
|
useFileManagement,
|
||||||
useFileUI,
|
useFileUI,
|
||||||
useFileRecord,
|
useWorkbenchFile,
|
||||||
useAllFiles,
|
useAllFiles,
|
||||||
useSelectedFiles,
|
useSelectedFiles,
|
||||||
// Primary API hooks for tools
|
// Primary API hooks for tools
|
||||||
|
@ -6,7 +6,7 @@ import { FileId } from '../../types/file';
|
|||||||
import {
|
import {
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextAction,
|
FileContextAction,
|
||||||
FileRecord
|
WorkbenchFile
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = {
|
|||||||
function processFileSwap(
|
function processFileSwap(
|
||||||
state: FileContextState,
|
state: FileContextState,
|
||||||
filesToRemove: FileId[],
|
filesToRemove: FileId[],
|
||||||
filesToAdd: FileRecord[]
|
filesToAdd: WorkbenchFile[]
|
||||||
): FileContextState {
|
): FileContextState {
|
||||||
// Only remove unpinned files
|
// Only remove unpinned files
|
||||||
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
|
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
|
||||||
@ -70,11 +70,11 @@ function processFileSwap(
|
|||||||
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'ADD_FILES': {
|
case 'ADD_FILES': {
|
||||||
const { fileRecords } = action.payload;
|
const { workbenchFiles } = action.payload;
|
||||||
const newIds: FileId[] = [];
|
const newIds: FileId[] = [];
|
||||||
const newById: Record<FileId, FileRecord> = { ...state.files.byId };
|
const newById: Record<FileId, WorkbenchFile> = { ...state.files.byId };
|
||||||
|
|
||||||
fileRecords.forEach(record => {
|
workbenchFiles.forEach(record => {
|
||||||
// Only add if not already present (dedupe by stable ID)
|
// Only add if not already present (dedupe by stable ID)
|
||||||
if (!newById[record.id]) {
|
if (!newById[record.id]) {
|
||||||
newIds.push(record.id);
|
newIds.push(record.id);
|
||||||
@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'CONSUME_FILES': {
|
case 'CONSUME_FILES': {
|
||||||
const { inputFileIds, outputFileRecords } = action.payload;
|
const { inputFileIds, outputWorkbenchFiles } = action.payload;
|
||||||
return processFileSwap(state, inputFileIds, outputFileRecords);
|
return processFileSwap(state, inputFileIds, outputWorkbenchFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UNDO_CONSUME_FILES': {
|
case 'UNDO_CONSUME_FILES': {
|
||||||
const { inputFileRecords, outputFileIds } = action.payload;
|
const { inputWorkbenchFiles, outputFileIds } = action.payload;
|
||||||
return processFileSwap(state, outputFileIds, inputFileRecords);
|
return processFileSwap(state, outputFileIds, inputWorkbenchFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RESET_CONTEXT': {
|
case 'RESET_CONTEXT': {
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FileRecord,
|
WorkbenchFile,
|
||||||
FileContextAction,
|
FileContextAction,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
toFileRecord,
|
toWorkbenchFile,
|
||||||
createFileId,
|
createFileId,
|
||||||
createQuickKey
|
createQuickKey
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
@ -109,8 +109,8 @@ export async function addFiles(
|
|||||||
await addFilesMutex.lock();
|
await addFilesMutex.lock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileRecords: FileRecord[] = [];
|
const workbenchFiles: WorkbenchFile[] = [];
|
||||||
const addedFiles: AddedFile[] = [];
|
const addedFiles: AddedFile[] = [];
|
||||||
|
|
||||||
// Build quickKey lookup from existing files for deduplication
|
// Build quickKey lookup from existing files for deduplication
|
||||||
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
||||||
@ -163,7 +163,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create record with immediate thumbnail and page metadata
|
// Create record with immediate thumbnail and page metadata
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toWorkbenchFile(file, fileId);
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||||
@ -184,7 +184,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
workbenchFiles.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail });
|
addedFiles.push({ file, id: fileId, thumbnail });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -205,7 +205,7 @@ export async function addFiles(
|
|||||||
const fileId = createFileId();
|
const fileId = createFileId();
|
||||||
filesRef.current.set(fileId, file);
|
filesRef.current.set(fileId, file);
|
||||||
|
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toWorkbenchFile(file, fileId);
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||||
@ -226,7 +226,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
workbenchFiles.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail });
|
addedFiles.push({ file, id: fileId, thumbnail });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -254,7 +254,7 @@ export async function addFiles(
|
|||||||
|
|
||||||
filesRef.current.set(fileId, file);
|
filesRef.current.set(fileId, file);
|
||||||
|
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toWorkbenchFile(file, fileId);
|
||||||
|
|
||||||
// Generate processedFile metadata for stored files
|
// Generate processedFile metadata for stored files
|
||||||
let pageCount: number = 1;
|
let pageCount: number = 1;
|
||||||
@ -301,7 +301,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
workbenchFiles.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail });
|
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
|
// Dispatch ADD_FILES action if we have new files
|
||||||
if (fileRecords.length > 0) {
|
if (workbenchFiles.length > 0) {
|
||||||
dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
|
dispatch({ type: 'ADD_FILES', payload: { workbenchFiles } });
|
||||||
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`);
|
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${workbenchFiles.length} files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addedFiles;
|
return addedFiles;
|
||||||
@ -328,7 +328,7 @@ export async function addFiles(
|
|||||||
async function processFilesIntoRecords(
|
async function processFilesIntoRecords(
|
||||||
files: File[],
|
files: File[],
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): Promise<Array<{ record: FileRecord; file: File; fileId: FileId; thumbnail?: string }>> {
|
): Promise<Array<{ record: WorkbenchFile; file: File; fileId: FileId; thumbnail?: string }>> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
const fileId = createFileId();
|
const fileId = createFileId();
|
||||||
@ -347,7 +347,7 @@ async function processFilesIntoRecords(
|
|||||||
if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error);
|
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) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
}
|
}
|
||||||
@ -365,10 +365,10 @@ async function processFilesIntoRecords(
|
|||||||
* Helper function to persist files to IndexedDB
|
* Helper function to persist files to IndexedDB
|
||||||
*/
|
*/
|
||||||
async function persistFilesToIndexedDB(
|
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<any> }
|
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await Promise.all(fileRecords.map(async ({ file, fileId, thumbnail }) => {
|
await Promise.all(workbenchFiles.map(async ({ file, fileId, thumbnail }) => {
|
||||||
try {
|
try {
|
||||||
await indexedDB.saveFile(file, fileId, thumbnail);
|
await indexedDB.saveFile(file, fileId, thumbnail);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -383,7 +383,6 @@ async function persistFilesToIndexedDB(
|
|||||||
export async function consumeFiles(
|
export async function consumeFiles(
|
||||||
inputFileIds: FileId[],
|
inputFileIds: FileId[],
|
||||||
outputFiles: File[],
|
outputFiles: File[],
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } | null
|
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } | null
|
||||||
@ -391,11 +390,11 @@ export async function consumeFiles(
|
|||||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
||||||
|
|
||||||
// Process output files with thumbnails and metadata
|
// 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
|
// Persist output files to IndexedDB if available
|
||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
await persistFilesToIndexedDB(outputFileRecords, indexedDB);
|
await persistFilesToIndexedDB(outputWorkbenchFiles, indexedDB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch the consume action
|
// Dispatch the consume action
|
||||||
@ -403,21 +402,21 @@ export async function consumeFiles(
|
|||||||
type: 'CONSUME_FILES',
|
type: 'CONSUME_FILES',
|
||||||
payload: {
|
payload: {
|
||||||
inputFileIds,
|
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 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
|
* Helper function to restore files to filesRef and manage IndexedDB cleanup
|
||||||
*/
|
*/
|
||||||
async function restoreFilesAndCleanup(
|
async function restoreFilesAndCleanup(
|
||||||
filesToRestore: Array<{ file: File; record: FileRecord }>,
|
filesToRestore: Array<{ file: File; record: WorkbenchFile }>,
|
||||||
fileIdsToRemove: FileId[],
|
fileIdsToRemove: FileId[],
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
@ -440,7 +439,7 @@ async function restoreFilesAndCleanup(
|
|||||||
if (DEBUG) console.warn(`📄 Skipping empty file ${file.name}`);
|
if (DEBUG) console.warn(`📄 Skipping empty file ${file.name}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the file to filesRef
|
// Restore the file to filesRef
|
||||||
if (DEBUG) console.log(`📄 Restoring file ${file.name} with id ${record.id} to filesRef`);
|
if (DEBUG) console.log(`📄 Restoring file ${file.name} with id ${record.id} to filesRef`);
|
||||||
filesRef.current.set(record.id, file);
|
filesRef.current.set(record.id, file);
|
||||||
@ -455,7 +454,7 @@ async function restoreFilesAndCleanup(
|
|||||||
throw error; // Re-throw to trigger rollback
|
throw error; // Re-throw to trigger rollback
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute all IndexedDB operations
|
// Execute all IndexedDB operations
|
||||||
await Promise.all(indexedDBPromises);
|
await Promise.all(indexedDBPromises);
|
||||||
}
|
}
|
||||||
@ -466,28 +465,28 @@ async function restoreFilesAndCleanup(
|
|||||||
*/
|
*/
|
||||||
export async function undoConsumeFiles(
|
export async function undoConsumeFiles(
|
||||||
inputFiles: File[],
|
inputFiles: File[],
|
||||||
inputFileRecords: FileRecord[],
|
inputWorkbenchFiles: WorkbenchFile[],
|
||||||
outputFileIds: FileId[],
|
outputFileIds: FileId[],
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
stateRef: React.MutableRefObject<FileContextState>,
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
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
|
// Validate inputs
|
||||||
if (inputFiles.length !== inputFileRecords.length) {
|
if (inputFiles.length !== inputWorkbenchFiles.length) {
|
||||||
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputFileRecords.length})`);
|
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputWorkbenchFiles.length})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a backup of current filesRef state for rollback
|
// Create a backup of current filesRef state for rollback
|
||||||
const backupFilesRef = new Map(filesRef.current);
|
const backupFilesRef = new Map(filesRef.current);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare files to restore
|
// Prepare files to restore
|
||||||
const filesToRestore = inputFiles.map((file, index) => ({
|
const filesToRestore = inputFiles.map((file, index) => ({
|
||||||
file,
|
file,
|
||||||
record: inputFileRecords[index]
|
record: inputWorkbenchFiles[index]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Restore input files and clean up output files
|
// Restore input files and clean up output files
|
||||||
@ -502,13 +501,13 @@ export async function undoConsumeFiles(
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: 'UNDO_CONSUME_FILES',
|
type: 'UNDO_CONSUME_FILES',
|
||||||
payload: {
|
payload: {
|
||||||
inputFileRecords,
|
inputWorkbenchFiles,
|
||||||
outputFileIds
|
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) {
|
} catch (error) {
|
||||||
// Rollback filesRef to previous state
|
// Rollback filesRef to previous state
|
||||||
if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error);
|
if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error);
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue
|
FileContextActionsValue
|
||||||
} from './contexts';
|
} from './contexts';
|
||||||
import { FileRecord, FileWithId } from '../../types/fileContext';
|
import { WorkbenchFile, StirlingFile } from '../../types/fileContext';
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue {
|
|||||||
/**
|
/**
|
||||||
* Hook for current/primary file (first in list)
|
* 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 { state, selectors } = useFileState();
|
||||||
|
|
||||||
const primaryFileId = state.files.ids[0];
|
const primaryFileId = state.files.ids[0];
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
||||||
record: primaryFileId ? selectors.getFileRecord(primaryFileId) : undefined
|
record: primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : undefined
|
||||||
}), [primaryFileId, selectors]);
|
}), [primaryFileId, selectors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export function useFileManagement() {
|
|||||||
addFiles: actions.addFiles,
|
addFiles: actions.addFiles,
|
||||||
removeFiles: actions.removeFiles,
|
removeFiles: actions.removeFiles,
|
||||||
clearAllFiles: actions.clearAllFiles,
|
clearAllFiles: actions.clearAllFiles,
|
||||||
updateFileRecord: actions.updateFileRecord,
|
updateWorkbenchFile: actions.updateWorkbenchFile,
|
||||||
reorderFiles: actions.reorderFiles
|
reorderFiles: actions.reorderFiles
|
||||||
}), [actions]);
|
}), [actions]);
|
||||||
}
|
}
|
||||||
@ -111,24 +111,24 @@ export function useFileUI() {
|
|||||||
/**
|
/**
|
||||||
* Hook for specific file by ID (optimized for individual file access)
|
* 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();
|
const { selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: selectors.getFile(fileId),
|
file: selectors.getFile(fileId),
|
||||||
record: selectors.getFileRecord(fileId)
|
record: selectors.getWorkbenchFile(fileId)
|
||||||
}), [fileId, selectors]);
|
}), [fileId, selectors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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: FileWithId[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getFiles(),
|
files: selectors.getFiles(),
|
||||||
records: selectors.getFileRecords(),
|
records: selectors.getWorkbenchFiles(),
|
||||||
fileIds: state.files.ids
|
fileIds: state.files.ids
|
||||||
}), [state.files.ids, selectors]);
|
}), [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)
|
* 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();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getSelectedFiles(),
|
files: selectors.getSelectedFiles(),
|
||||||
records: selectors.getSelectedFileRecords(),
|
records: selectors.getSelectedWorkbenchFiles(),
|
||||||
fileIds: state.ui.selectedFileIds
|
fileIds: state.ui.selectedFileIds
|
||||||
}), [state.ui.selectedFileIds, selectors]);
|
}), [state.ui.selectedFileIds, selectors]);
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
import {
|
import {
|
||||||
FileRecord,
|
WorkbenchFile,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextSelectors,
|
FileContextSelectors,
|
||||||
FileWithId,
|
StirlingFile,
|
||||||
createFileWithId
|
createStirlingFile
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,7 +21,7 @@ export function createFileSelectors(
|
|||||||
return {
|
return {
|
||||||
getFile: (id: FileId) => {
|
getFile: (id: FileId) => {
|
||||||
const file = filesRef.current.get(id);
|
const file = filesRef.current.get(id);
|
||||||
return file ? createFileWithId(file, id) : undefined;
|
return file ? createStirlingFile(file, id) : undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
getFiles: (ids?: FileId[]) => {
|
getFiles: (ids?: FileId[]) => {
|
||||||
@ -29,14 +29,14 @@ export function createFileSelectors(
|
|||||||
return currentIds
|
return currentIds
|
||||||
.map(id => {
|
.map(id => {
|
||||||
const file = filesRef.current.get(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;
|
const currentIds = ids || stateRef.current.files.ids;
|
||||||
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
|
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
|
||||||
},
|
},
|
||||||
@ -47,12 +47,12 @@ export function createFileSelectors(
|
|||||||
return stateRef.current.ui.selectedFileIds
|
return stateRef.current.ui.selectedFileIds
|
||||||
.map(id => {
|
.map(id => {
|
||||||
const file = filesRef.current.get(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
|
return stateRef.current.ui.selectedFileIds
|
||||||
.map(id => stateRef.current.files.byId[id])
|
.map(id => stateRef.current.files.byId[id])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@ -67,18 +67,18 @@ export function createFileSelectors(
|
|||||||
return Array.from(stateRef.current.pinnedFiles)
|
return Array.from(stateRef.current.pinnedFiles)
|
||||||
.map(id => {
|
.map(id => {
|
||||||
const file = filesRef.current.get(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)
|
return Array.from(stateRef.current.pinnedFiles)
|
||||||
.map(id => stateRef.current.files.byId[id])
|
.map(id => stateRef.current.files.byId[id])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
isFilePinned: (file: FileWithId) => {
|
isFilePinned: (file: StirlingFile) => {
|
||||||
return stateRef.current.pinnedFiles.has(file.fileId);
|
return stateRef.current.pinnedFiles.has(file.fileId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -98,9 +98,9 @@ export function createFileSelectors(
|
|||||||
/**
|
/**
|
||||||
* Helper for building quickKey sets for deduplication
|
* Helper for building quickKey sets for deduplication
|
||||||
*/
|
*/
|
||||||
export function buildQuickKeySet(fileRecords: Record<FileId, FileRecord>): Set<string> {
|
export function buildQuickKeySet(workbenchFiles: Record<FileId, WorkbenchFile>): Set<string> {
|
||||||
const quickKeys = new Set<string>();
|
const quickKeys = new Set<string>();
|
||||||
Object.values(fileRecords).forEach(record => {
|
Object.values(workbenchFiles).forEach(record => {
|
||||||
if (record.quickKey) {
|
if (record.quickKey) {
|
||||||
quickKeys.add(record.quickKey);
|
quickKeys.add(record.quickKey);
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz
|
|||||||
export function getPrimaryFile(
|
export function getPrimaryFile(
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
stateRef: React.MutableRefObject<FileContextState>,
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): { file?: File; record?: FileRecord } {
|
): { file?: File; record?: WorkbenchFile } {
|
||||||
const primaryFileId = stateRef.current.files.ids[0];
|
const primaryFileId = stateRef.current.files.ids[0];
|
||||||
if (!primaryFileId) return {};
|
if (!primaryFileId) return {};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FileId } from '../../types/file';
|
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';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ export class FileLifecycleManager {
|
|||||||
/**
|
/**
|
||||||
* Update file record with race condition guards
|
* Update file record with race condition guards
|
||||||
*/
|
*/
|
||||||
updateFileRecord = (fileId: FileId, updates: Partial<FileRecord>, stateRef?: React.MutableRefObject<any>): void => {
|
updateWorkbenchFile = (fileId: FileId, updates: Partial<WorkbenchFile>, stateRef?: React.MutableRefObject<any>): void => {
|
||||||
// Guard against updating removed files (race condition protection)
|
// Guard against updating removed files (race condition protection)
|
||||||
if (!this.filesRef.current.has(fileId)) {
|
if (!this.filesRef.current.has(fileId)) {
|
||||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||||
|
@ -4,11 +4,11 @@ import { useEndpointEnabled } from '../../useEndpointConfig';
|
|||||||
import { BaseToolProps } from '../../../types/tool';
|
import { BaseToolProps } from '../../../types/tool';
|
||||||
import { ToolOperationHook } from './useToolOperation';
|
import { ToolOperationHook } from './useToolOperation';
|
||||||
import { BaseParametersHook } from './useBaseParameters';
|
import { BaseParametersHook } from './useBaseParameters';
|
||||||
import { FileWithId } from '../../../types/fileContext';
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
interface BaseToolReturn<TParams> {
|
interface BaseToolReturn<TParams> {
|
||||||
// File management
|
// File management
|
||||||
selectedFiles: FileWithId[];
|
selectedFiles: StirlingFile[];
|
||||||
|
|
||||||
// Tool-specific hooks
|
// Tool-specific hooks
|
||||||
params: BaseParametersHook<TParams>;
|
params: BaseParametersHook<TParams>;
|
||||||
|
@ -6,7 +6,7 @@ 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 { FileWithId, extractFiles, FileId, FileRecord } from '../../../types/fileContext';
|
import { StirlingFile, extractFiles, FileId, WorkbenchFile } from '../../../types/fileContext';
|
||||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
@ -102,7 +102,7 @@ export interface ToolOperationHook<TParams = void> {
|
|||||||
progress: ProcessingProgress | null;
|
progress: ProcessingProgress | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
executeOperation: (params: TParams, selectedFiles: FileWithId[]) => Promise<void>;
|
executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise<void>;
|
||||||
resetResults: () => void;
|
resetResults: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
cancelOperation: () => void;
|
cancelOperation: () => void;
|
||||||
@ -138,13 +138,13 @@ export const useToolOperation = <TParams>(
|
|||||||
// Track last operation for undo functionality
|
// Track last operation for undo functionality
|
||||||
const lastOperationRef = useRef<{
|
const lastOperationRef = useRef<{
|
||||||
inputFiles: File[];
|
inputFiles: File[];
|
||||||
inputFileRecords: FileRecord[];
|
inputWorkbenchFiles: WorkbenchFile[];
|
||||||
outputFileIds: FileId[];
|
outputFileIds: FileId[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const executeOperation = useCallback(async (
|
const executeOperation = useCallback(async (
|
||||||
params: TParams,
|
params: TParams,
|
||||||
selectedFiles: FileWithId[]
|
selectedFiles: StirlingFile[]
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Validation
|
// Validation
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
@ -168,7 +168,7 @@ export const useToolOperation = <TParams>(
|
|||||||
try {
|
try {
|
||||||
let processedFiles: File[];
|
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);
|
const validRegularFiles = extractFiles(validFiles);
|
||||||
|
|
||||||
switch (config.toolType) {
|
switch (config.toolType) {
|
||||||
@ -240,15 +240,15 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
// Replace input files with processed files (consumeFiles handles pinning)
|
// Replace input files with processed files (consumeFiles handles pinning)
|
||||||
const inputFileIds: FileId[] = [];
|
const inputFileIds: FileId[] = [];
|
||||||
const inputFileRecords: FileRecord[] = [];
|
const inputWorkbenchFiles: WorkbenchFile[] = [];
|
||||||
|
|
||||||
// Build parallel arrays of IDs and records for undo tracking
|
// Build parallel arrays of IDs and records for undo tracking
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
const fileId = file.fileId;
|
const fileId = file.fileId;
|
||||||
const record = selectors.getFileRecord(fileId);
|
const record = selectors.getWorkbenchFile(fileId);
|
||||||
if (record) {
|
if (record) {
|
||||||
inputFileIds.push(fileId);
|
inputFileIds.push(fileId);
|
||||||
inputFileRecords.push(record);
|
inputWorkbenchFiles.push(record);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`No file record found for file: ${file.name}`);
|
console.warn(`No file record found for file: ${file.name}`);
|
||||||
}
|
}
|
||||||
@ -259,7 +259,7 @@ export const useToolOperation = <TParams>(
|
|||||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||||
lastOperationRef.current = {
|
lastOperationRef.current = {
|
||||||
inputFiles: extractFiles(validFiles), // Convert to File objects for undo
|
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
|
outputFileIds
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -302,10 +302,10 @@ export const useToolOperation = <TParams>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { inputFiles, inputFileRecords, outputFileIds } = lastOperationRef.current;
|
const { inputFiles, inputWorkbenchFiles, outputFileIds } = lastOperationRef.current;
|
||||||
|
|
||||||
// Validate that we have data to undo
|
// 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'));
|
actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -317,7 +317,7 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Undo the consume operation
|
// Undo the consume operation
|
||||||
await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds);
|
await undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds);
|
||||||
|
|
||||||
// Clear results and operation tracking
|
// Clear results and operation tracking
|
||||||
resetResults();
|
resetResults();
|
||||||
|
@ -9,7 +9,7 @@ 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, FileWithId, or Blob object
|
// Validate that file is a proper File, StirlingFile, or Blob object
|
||||||
if (!isFileObject(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;
|
||||||
|
@ -1,14 +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';
|
import { StirlingFile } from '../types/fileContext';
|
||||||
|
|
||||||
export interface PdfSignatureDetectionResult {
|
export interface PdfSignatureDetectionResult {
|
||||||
hasDigitalSignatures: boolean;
|
hasDigitalSignatures: boolean;
|
||||||
isChecking: boolean;
|
isChecking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePdfSignatureDetection = (files: FileWithId[]): PdfSignatureDetectionResult => {
|
export const usePdfSignatureDetection = (files: StirlingFile[]): PdfSignatureDetectionResult => {
|
||||||
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
|
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
|
|
||||||
|
@ -18,8 +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 { createTestStirlingFile } from '../utils/testFileHelpers';
|
||||||
import { FileWithId } from '../../types/fileContext';
|
import { StirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
@ -57,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 = (): 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';
|
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
|
// Test wrapper component
|
||||||
@ -164,7 +164,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
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 = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
toExtension: 'png',
|
||||||
@ -428,7 +428,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
const files = [
|
const files = [
|
||||||
createPDFFile(),
|
createPDFFile(),
|
||||||
createTestFileWithId('test2.pdf', '%PDF-1.4...', 'application/pdf')
|
createTestStirlingFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
|
||||||
]
|
]
|
||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
@ -529,7 +529,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
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 = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
toExtension: 'png',
|
||||||
|
@ -14,8 +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 { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers';
|
||||||
import { FileWithId } from '../../types/fileContext';
|
import { StirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
@ -83,7 +83,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create mock DOCX file
|
// 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
|
// Test auto-detection
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -119,7 +119,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create mock unknown file
|
// 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
|
// Test auto-detection
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -290,7 +290,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
});
|
});
|
||||||
|
|
||||||
const htmlFile = createTestFileWithId('page.html', '<html>content</html>', 'text/html');
|
const htmlFile = createTestStirlingFile('page.html', '<html>content</html>', 'text/html');
|
||||||
|
|
||||||
// Set up HTML conversion parameters
|
// Set up HTML conversion parameters
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -320,7 +320,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
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
|
// Set up email conversion parameters
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -357,7 +357,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
wrapper: TestWrapper
|
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
|
// Set up PDF/A conversion parameters
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -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,
|
name: string,
|
||||||
content: string = 'test content',
|
content: string = 'test content',
|
||||||
type: string = 'application/pdf'
|
type: string = 'application/pdf'
|
||||||
): FileWithId {
|
): StirlingFile {
|
||||||
const file = new File([content], name, { type });
|
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(
|
export function createTestFilesWithId(
|
||||||
files: Array<{ name: string; content?: string; type?: string }>
|
files: Array<{ name: string; content?: string; type?: string }>
|
||||||
): FileWithId[] {
|
): StirlingFile[] {
|
||||||
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
|
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
|
||||||
createTestFileWithId(name, content, type)
|
createTestStirlingFile(name, content, type)
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -44,7 +44,7 @@ export interface ProcessedFileMetadata {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRecord {
|
export interface WorkbenchFile {
|
||||||
id: FileId;
|
id: FileId;
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
@ -62,7 +62,7 @@ export interface FileRecord {
|
|||||||
|
|
||||||
export interface FileContextNormalizedFiles {
|
export interface FileContextNormalizedFiles {
|
||||||
ids: FileId[];
|
ids: FileId[];
|
||||||
byId: Record<FileId, FileRecord>;
|
byId: Record<FileId, WorkbenchFile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions - UUID-based primary keys (zero collisions, synchronous)
|
// 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}`;
|
return `${file.name}|${file.size}|${file.lastModified}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File with embedded UUID - replaces loose File + FileId parameter passing
|
// Stirling PDF file with embedded UUID - replaces loose File + FileId parameter passing
|
||||||
export interface FileWithId extends File {
|
export interface StirlingFile extends File {
|
||||||
readonly fileId: FileId;
|
readonly fileId: FileId;
|
||||||
readonly quickKey: string; // Fast deduplication key: name|size|lastModified
|
readonly quickKey: string; // Fast deduplication key: name|size|lastModified
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type guard to check if a File object has an embedded fileId
|
// 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' &&
|
return 'fileId' in file && typeof (file as any).fileId === 'string' &&
|
||||||
'quickKey' in file && typeof (file as any).quickKey === 'string';
|
'quickKey' in file && typeof (file as any).quickKey === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a FileWithId from a regular File object
|
// Create a StirlingFile from a regular File object
|
||||||
export function createFileWithId(file: File, id?: FileId): FileWithId {
|
export function createStirlingFile(file: File, id?: FileId): StirlingFile {
|
||||||
const fileId = id || createFileId();
|
const fileId = id || createFileId();
|
||||||
const quickKey = createQuickKey(file);
|
const quickKey = createQuickKey(file);
|
||||||
|
|
||||||
// File properties are not enumerable, so we need to copy them explicitly
|
// File properties are not enumerable, so we need to copy them explicitly
|
||||||
// This avoids prototype chain issues while preserving all File functionality
|
// This avoids prototype chain issues while preserving all File functionality
|
||||||
const fileWithId = {
|
const stirlingFile = {
|
||||||
// Explicitly copy File properties (they're not enumerable)
|
// Explicitly copy File properties (they're not enumerable)
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
lastModified: file.lastModified,
|
lastModified: file.lastModified,
|
||||||
webkitRelativePath: file.webkitRelativePath,
|
webkitRelativePath: file.webkitRelativePath,
|
||||||
|
|
||||||
// Add our custom properties
|
// Add our custom properties
|
||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
quickKey: quickKey,
|
quickKey: quickKey,
|
||||||
|
|
||||||
// Preserve File prototype methods by binding them to the original file
|
// Preserve File prototype methods by binding them to the original file
|
||||||
arrayBuffer: file.arrayBuffer.bind(file),
|
arrayBuffer: file.arrayBuffer.bind(file),
|
||||||
slice: file.slice.bind(file),
|
slice: file.slice.bind(file),
|
||||||
stream: file.stream.bind(file),
|
stream: file.stream.bind(file),
|
||||||
text: file.text.bind(file)
|
text: file.text.bind(file)
|
||||||
} as FileWithId;
|
} as StirlingFile;
|
||||||
|
|
||||||
return fileWithId;
|
return stirlingFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract FileIds from FileWithId array
|
// Extract FileIds from StirlingFile array
|
||||||
export function extractFileIds(files: FileWithId[]): FileId[] {
|
export function extractFileIds(files: StirlingFile[]): FileId[] {
|
||||||
return files.map(file => file.fileId);
|
return files.map(file => file.fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract regular File objects from FileWithId array
|
// Extract regular File objects from StirlingFile array
|
||||||
export function extractFiles(files: FileWithId[]): File[] {
|
export function extractFiles(files: StirlingFile[]): File[] {
|
||||||
return files as File[];
|
return files as File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if an object is a File or FileWithId (replaces instanceof File checks)
|
// Check if an object is a File or StirlingFile (replaces instanceof File checks)
|
||||||
export function isFileObject(obj: any): obj is File | FileWithId {
|
export function isFileObject(obj: any): obj is File | StirlingFile {
|
||||||
return obj &&
|
return obj &&
|
||||||
typeof obj.name === 'string' &&
|
typeof obj.name === 'string' &&
|
||||||
typeof obj.size === 'number' &&
|
typeof obj.size === 'number' &&
|
||||||
typeof obj.type === 'string' &&
|
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();
|
const fileId = id || createFileId();
|
||||||
return {
|
return {
|
||||||
id: fileId,
|
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
|
// Only revoke blob: URLs to prevent errors on other schemes
|
||||||
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
||||||
try {
|
try {
|
||||||
@ -235,7 +235,7 @@ export interface FileContextState {
|
|||||||
// Core file management - lightweight file IDs only
|
// Core file management - lightweight file IDs only
|
||||||
files: {
|
files: {
|
||||||
ids: FileId[];
|
ids: FileId[];
|
||||||
byId: Record<FileId, FileRecord>;
|
byId: Record<FileId, WorkbenchFile>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pinned files - files that won't be consumed by tools
|
// Pinned files - files that won't be consumed by tools
|
||||||
@ -254,16 +254,16 @@ export interface FileContextState {
|
|||||||
// Action types for reducer pattern
|
// Action types for reducer pattern
|
||||||
export type FileContextAction =
|
export type FileContextAction =
|
||||||
// File management actions
|
// File management actions
|
||||||
| { type: 'ADD_FILES'; payload: { fileRecords: FileRecord[] } }
|
| { type: 'ADD_FILES'; payload: { workbenchFiles: WorkbenchFile[] } }
|
||||||
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
|
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
|
||||||
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<FileRecord> } }
|
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<WorkbenchFile> } }
|
||||||
| { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } }
|
| { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } }
|
||||||
|
|
||||||
// Pinned files actions
|
// Pinned files actions
|
||||||
| { type: 'PIN_FILE'; payload: { fileId: FileId } }
|
| { type: 'PIN_FILE'; payload: { fileId: FileId } }
|
||||||
| { type: 'UNPIN_FILE'; payload: { fileId: FileId } }
|
| { type: 'UNPIN_FILE'; payload: { fileId: FileId } }
|
||||||
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputFileRecords: FileRecord[] } }
|
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputWorkbenchFiles: WorkbenchFile[] } }
|
||||||
| { type: 'UNDO_CONSUME_FILES'; payload: { inputFileRecords: FileRecord[]; outputFileIds: FileId[] } }
|
| { type: 'UNDO_CONSUME_FILES'; payload: { inputWorkbenchFiles: WorkbenchFile[]; outputFileIds: FileId[] } }
|
||||||
|
|
||||||
// UI actions
|
// UI actions
|
||||||
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
||||||
@ -279,22 +279,22 @@ export type FileContextAction =
|
|||||||
|
|
||||||
export interface FileContextActions {
|
export interface FileContextActions {
|
||||||
// File management - lightweight actions only
|
// File management - lightweight actions only
|
||||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<FileWithId[]>;
|
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<FileWithId[]>;
|
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
|
||||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<FileWithId[]>;
|
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
updateWorkbenchFile: (id: FileId, updates: Partial<WorkbenchFile>) => void;
|
||||||
reorderFiles: (orderedFileIds: FileId[]) => void;
|
reorderFiles: (orderedFileIds: FileId[]) => void;
|
||||||
clearAllFiles: () => Promise<void>;
|
clearAllFiles: () => Promise<void>;
|
||||||
clearAllData: () => Promise<void>;
|
clearAllData: () => Promise<void>;
|
||||||
|
|
||||||
// File pinning - accepts FileWithId for safer type checking
|
// File pinning - accepts StirlingFile for safer type checking
|
||||||
pinFile: (file: FileWithId) => void;
|
pinFile: (file: StirlingFile) => void;
|
||||||
unpinFile: (file: FileWithId) => void;
|
unpinFile: (file: StirlingFile) => void;
|
||||||
|
|
||||||
// File consumption (replace unpinned files with outputs)
|
// File consumption (replace unpinned files with outputs)
|
||||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
|
||||||
undoConsumeFiles: (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]) => Promise<void>;
|
undoConsumeFiles: (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]) => Promise<void>;
|
||||||
// Selection management
|
// Selection management
|
||||||
setSelectedFiles: (fileIds: FileId[]) => void;
|
setSelectedFiles: (fileIds: FileId[]) => void;
|
||||||
setSelectedPages: (pageNumbers: number[]) => void;
|
setSelectedPages: (pageNumbers: number[]) => void;
|
||||||
@ -317,17 +317,17 @@ export interface FileContextActions {
|
|||||||
|
|
||||||
// File selectors (separate from actions to avoid re-renders)
|
// File selectors (separate from actions to avoid re-renders)
|
||||||
export interface FileContextSelectors {
|
export interface FileContextSelectors {
|
||||||
getFile: (id: FileId) => FileWithId | undefined;
|
getFile: (id: FileId) => StirlingFile | undefined;
|
||||||
getFiles: (ids?: FileId[]) => FileWithId[];
|
getFiles: (ids?: FileId[]) => StirlingFile[];
|
||||||
getFileRecord: (id: FileId) => FileRecord | undefined;
|
getWorkbenchFile: (id: FileId) => WorkbenchFile | undefined;
|
||||||
getFileRecords: (ids?: FileId[]) => FileRecord[];
|
getWorkbenchFiles: (ids?: FileId[]) => WorkbenchFile[];
|
||||||
getAllFileIds: () => FileId[];
|
getAllFileIds: () => FileId[];
|
||||||
getSelectedFiles: () => FileWithId[];
|
getSelectedFiles: () => StirlingFile[];
|
||||||
getSelectedFileRecords: () => FileRecord[];
|
getSelectedWorkbenchFiles: () => WorkbenchFile[];
|
||||||
getPinnedFileIds: () => FileId[];
|
getPinnedFileIds: () => FileId[];
|
||||||
getPinnedFiles: () => FileWithId[];
|
getPinnedFiles: () => StirlingFile[];
|
||||||
getPinnedFileRecords: () => FileRecord[];
|
getPinnedWorkbenchFiles: () => WorkbenchFile[];
|
||||||
isFilePinned: (file: FileWithId) => boolean;
|
isFilePinned: (file: StirlingFile) => boolean;
|
||||||
getFilesSignature: () => string;
|
getFilesSignature: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
frontend/src/types/fileIdSafety.d.ts
vendored
28
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, OperationType, FileOperation } from './fileContext';
|
import { FileId, StirlingFile, OperationType, FileOperation } from './fileContext';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace FileIdSafety {
|
namespace FileIdSafety {
|
||||||
@ -13,15 +13,15 @@ declare global {
|
|||||||
: T
|
: T
|
||||||
: T;
|
: T;
|
||||||
|
|
||||||
// Mark functions that should only accept FileWithId, not regular File
|
// Mark functions that should only accept StirlingFile, not regular File
|
||||||
type FileWithIdOnlyFunction<T extends (...args: any[]) => any> = T extends (...args: infer P) => infer R
|
type StirlingFileOnlyFunction<T extends (...args: any[]) => any> = T extends (...args: infer P) => infer R
|
||||||
? P extends readonly [File, ...any[]]
|
? 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
|
||||||
: T;
|
: T;
|
||||||
|
|
||||||
// Utility type to enforce FileWithId usage
|
// Utility type to enforce StirlingFile usage
|
||||||
type RequireFileWithId<T> = T extends File ? FileWithId : T;
|
type RequireStirlingFile<T> = T extends File ? StirlingFile : T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend Window interface to add runtime validation helpers
|
// 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' {
|
declare module '../contexts/FileContext' {
|
||||||
export interface StrictFileContextActions {
|
export interface StrictFileContextActions {
|
||||||
pinFile: (file: FileWithId) => void; // Must be FileWithId
|
pinFile: (file: StirlingFile) => void; // Must be StirlingFile
|
||||||
unpinFile: (file: FileWithId) => void; // Must be FileWithId
|
unpinFile: (file: StirlingFile) => void; // Must be StirlingFile
|
||||||
addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise<FileWithId[]>; // Returns FileWithId
|
addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise<StirlingFile[]>; // Returns StirlingFile
|
||||||
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileWithId[]>; // Returns FileWithId
|
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<StirlingFile[]>; // Returns StirlingFile
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StrictFileContextSelectors {
|
export interface StrictFileContextSelectors {
|
||||||
getFile: (id: FileId) => FileWithId | undefined; // Returns FileWithId
|
getFile: (id: FileId) => StirlingFile | undefined; // Returns StirlingFile
|
||||||
getFiles: (ids?: FileId[]) => FileWithId[]; // Returns FileWithId[]
|
getFiles: (ids?: FileId[]) => StirlingFile[]; // Returns StirlingFile[]
|
||||||
isFilePinned: (file: FileWithId) => boolean; // Must be FileWithId
|
isFilePinned: (file: StirlingFile) => boolean; // Must be StirlingFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user