Files always get added to recent

This commit is contained in:
Connor Yoh 2025-08-06 17:29:37 +01:00
parent 45c775a8e0
commit e378e27c60
7 changed files with 78 additions and 128 deletions

View File

@ -1,7 +1,6 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { Modal } from '@mantine/core'; import { Modal } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { useTranslation } from 'react-i18next';
import { FileWithUrl } from '../types/file'; import { FileWithUrl } from '../types/file';
import { useFileManager } from '../hooks/useFileManager'; import { useFileManager } from '../hooks/useFileManager';
import { useFilesModalContext } from '../contexts/FilesModalContext'; import { useFilesModalContext } from '../contexts/FilesModalContext';
@ -16,13 +15,12 @@ interface FileManagerProps {
} }
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => { const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const { t } = useTranslation(); const { isFilesModalOpen, closeFilesModal, onFilesSelect } = useFilesModalContext();
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]); const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, touchFile } = useFileManager(); const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
// File management handlers // File management handlers
const isFileSupported = useCallback((fileName: string) => { const isFileSupported = useCallback((fileName: string) => {
@ -40,9 +38,6 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
try { try {
const fileObjects = await Promise.all( const fileObjects = await Promise.all(
files.map(async (fileWithUrl) => { files.map(async (fileWithUrl) => {
if (fileWithUrl.file) {
return fileWithUrl.file;
}
return await convertToFile(fileWithUrl); return await convertToFile(fileWithUrl);
}) })
); );
@ -55,15 +50,14 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const handleNewFileUpload = useCallback(async (files: File[]) => { const handleNewFileUpload = useCallback(async (files: File[]) => {
if (files.length > 0) { if (files.length > 0) {
try { try {
// Store files and refresh recent files // Files will get IDs assigned through onFilesSelect -> FileContext addFiles
await Promise.all(files.map(file => storeFile(file)));
onFilesSelect(files); onFilesSelect(files);
await refreshRecentFiles(); await refreshRecentFiles();
} catch (error) { } catch (error) {
console.error('Failed to process dropped files:', error); console.error('Failed to process dropped files:', error);
} }
} }
}, [storeFile, onFilesSelect, refreshRecentFiles]); }, [onFilesSelect, refreshRecentFiles]);
const handleRemoveFileByIndex = useCallback(async (index: number) => { const handleRemoveFileByIndex = useCallback(async (index: number) => {
await handleRemoveFile(index, recentFiles, setRecentFiles); await handleRemoveFile(index, recentFiles, setRecentFiles);

View File

@ -1,36 +0,0 @@
import React from 'react';
import { Modal } from '@mantine/core';
import FileUploadSelector from './FileUploadSelector';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { Tool } from '../../types/tool';
interface FileUploadModalProps {
selectedTool?: Tool | null;
}
const FileUploadModal: React.FC<FileUploadModalProps> = ({ selectedTool }) => {
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
return (
<Modal
opened={isFilesModalOpen}
onClose={closeFilesModal}
title="Upload Files"
size="xl"
centered
>
<FileUploadSelector
title="Upload Files"
subtitle="Choose files from storage or upload new files"
onFileSelect={onFileSelect}
onFilesSelect={onFilesSelect}
accept={["*/*"]}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
data-testid="file-upload-modal"
/>
</Modal>
);
};
export default FileUploadModal;

View File

@ -100,7 +100,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'REMOVE_FILES': case 'REMOVE_FILES':
const remainingFiles = state.activeFiles.filter(file => { const remainingFiles = state.activeFiles.filter(file => {
const fileId = getFileId(file); const fileId = getFileId(file);
return !action.payload.includes(fileId); return !fileId || !action.payload.includes(fileId);
}); });
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : []; const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
return { return {
@ -491,26 +491,38 @@ export function FileContextProvider({
}, [cleanupFile]); }, [cleanupFile]);
// Action implementations // Action implementations
const addFiles = useCallback(async (files: File[]) => { const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
dispatch({ type: 'ADD_FILES', payload: files }); dispatch({ type: 'ADD_FILES', payload: files });
// Auto-save to IndexedDB if persistence enabled // Auto-save to IndexedDB if persistence enabled
if (enablePersistence) { if (enablePersistence) {
for (const file of files) { for (const file of files) {
try { try {
// Check if file already has an ID (already in IndexedDB) // Check if file already has an explicit ID property (already in IndexedDB)
const fileId = getFileId(file); const fileId = getFileId(file);
if (!fileId) { if (!fileId) {
// File doesn't have ID, store it and get the ID // File doesn't have explicit ID, store it with thumbnail
const storedFile = await fileStorage.storeFile(file); try {
// Generate thumbnail for better recent files experience
const thumbnail = await thumbnailGenerationService.generateThumbnail(file);
const storedFile = await fileStorage.storeFile(file, thumbnail);
// Add the ID to the file object // Add the ID to the file object
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false }); Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
} catch (thumbnailError) {
// If thumbnail generation fails, store without thumbnail
console.warn('Failed to generate thumbnail, storing without:', thumbnailError);
const storedFile = await fileStorage.storeFile(file);
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
}
} }
} catch (error) { } catch (error) {
console.error('Failed to store file:', error); console.error('Failed to store file:', error);
} }
} }
} }
// Return files with their IDs assigned
return files;
}, [enablePersistence]); }, [enablePersistence]);
const removeFiles = useCallback((fileIds: string[], deleteFromStorage: boolean = true) => { const removeFiles = useCallback((fileIds: string[], deleteFromStorage: boolean = true) => {
@ -682,7 +694,7 @@ export function FileContextProvider({
const getFileById = useCallback((fileId: string): File | undefined => { const getFileById = useCallback((fileId: string): File | undefined => {
return state.activeFiles.find(file => { return state.activeFiles.find(file => {
const actualFileId = getFileId(file); const actualFileId = getFileId(file);
return actualFileId === fileId; return actualFileId && actualFileId === fileId;
}); });
}, [state.activeFiles]); }, [state.activeFiles]);

View File

@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react'; import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
import { FileWithUrl } from '../types/file'; import { FileWithUrl } from '../types/file';
import { StoredFile } from '../services/fileStorage';
// Type for the context value - now contains everything directly // Type for the context value - now contains everything directly
interface FileManagerContextValue { interface FileManagerContextValue {
@ -40,7 +41,7 @@ interface FileManagerProviderProps {
isOpen: boolean; isOpen: boolean;
onFileRemove: (index: number) => void; onFileRemove: (index: number) => void;
modalHeight: string; modalHeight: string;
storeFile: (file: File) => Promise<void>; storeFile: (file: File) => Promise<StoredFile>;
refreshRecentFiles: () => Promise<void>; refreshRecentFiles: () => Promise<void>;
} }
@ -65,7 +66,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const createdBlobUrls = useRef<Set<string>>(new Set()); const createdBlobUrls = useRef<Set<string>>(new Set());
// Computed values (with null safety) // Computed values (with null safety)
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id)); const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name));
const filteredFiles = (recentFiles || []).filter(file => const filteredFiles = (recentFiles || []).filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase()) file.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@ -122,14 +123,13 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const files = Array.from(event.target.files || []); const files = Array.from(event.target.files || []);
if (files.length > 0) { if (files.length > 0) {
try { try {
// Store files and refresh recent files (same as drag-and-drop) // Create FileWithUrl objects - FileContext will handle storage and ID assignment
await Promise.all(files.map(file => storeFile(file)));
const fileWithUrls = files.map(file => { const fileWithUrls = files.map(file => {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
createdBlobUrls.current.add(url); createdBlobUrls.current.add(url);
return { return {
id: `local-${Date.now()}-${Math.random()}`, // No ID assigned here - FileContext will handle storage and ID assignment
name: file.name, name: file.name,
file, file,
url, url,

View File

@ -1,21 +1,58 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useState, useCallback } from 'react';
import { useFilesModal, UseFilesModalReturn } from '../hooks/useFilesModal';
import { useFileHandler } from '../hooks/useFileHandler'; import { useFileHandler } from '../hooks/useFileHandler';
interface FilesModalContextType extends UseFilesModalReturn {} interface FilesModalContextType {
isFilesModalOpen: boolean;
openFilesModal: () => void;
closeFilesModal: () => void;
onFileSelect: (file: File) => void;
onFilesSelect: (files: File[]) => void;
onModalClose: () => void;
setOnModalClose: (callback: () => void) => void;
}
const FilesModalContext = createContext<FilesModalContextType | null>(null); const FilesModalContext = createContext<FilesModalContextType | null>(null);
export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { addToActiveFiles, addMultipleFiles } = useFileHandler(); const { addToActiveFiles, addMultipleFiles } = useFileHandler();
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
const filesModal = useFilesModal({ const openFilesModal = useCallback(() => {
onFileSelect: addToActiveFiles, setIsFilesModalOpen(true);
onFilesSelect: addMultipleFiles, }, []);
});
const closeFilesModal = useCallback(() => {
setIsFilesModalOpen(false);
onModalClose?.();
}, [onModalClose]);
const handleFileSelect = useCallback((file: File) => {
addToActiveFiles(file);
closeFilesModal();
}, [addToActiveFiles, closeFilesModal]);
const handleFilesSelect = useCallback((files: File[]) => {
addMultipleFiles(files);
closeFilesModal();
}, [addMultipleFiles, closeFilesModal]);
const setModalCloseCallback = useCallback((callback: () => void) => {
setOnModalClose(() => callback);
}, []);
const contextValue: FilesModalContextType = {
isFilesModalOpen,
openFilesModal,
closeFilesModal,
onFileSelect: handleFileSelect,
onFilesSelect: handleFilesSelect,
onModalClose,
setOnModalClose: setModalCloseCallback,
};
return ( return (
<FilesModalContext.Provider value={filesModal}> <FilesModalContext.Provider value={contextValue}>
{children} {children}
</FilesModalContext.Provider> </FilesModalContext.Provider>
); );

View File

@ -1,57 +0,0 @@
import { useState, useCallback } from 'react';
export interface UseFilesModalReturn {
isFilesModalOpen: boolean;
openFilesModal: () => void;
closeFilesModal: () => void;
onFileSelect?: (file: File) => void;
onFilesSelect?: (files: File[]) => void;
onModalClose?: () => void;
setOnModalClose: (callback: () => void) => void;
}
interface UseFilesModalProps {
onFileSelect?: (file: File) => void;
onFilesSelect?: (files: File[]) => void;
}
export const useFilesModal = ({
onFileSelect,
onFilesSelect
}: UseFilesModalProps = {}): UseFilesModalReturn => {
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
const openFilesModal = useCallback(() => {
setIsFilesModalOpen(true);
}, []);
const closeFilesModal = useCallback(() => {
setIsFilesModalOpen(false);
onModalClose?.();
}, [onModalClose]);
const handleFileSelect = useCallback((file: File) => {
onFileSelect?.(file);
closeFilesModal();
}, [onFileSelect, closeFilesModal]);
const handleFilesSelect = useCallback((files: File[]) => {
onFilesSelect?.(files);
closeFilesModal();
}, [onFilesSelect, closeFilesModal]);
const setModalCloseCallback = useCallback((callback: () => void) => {
setOnModalClose(() => callback);
}, []);
return {
isFilesModalOpen,
openFilesModal,
closeFilesModal,
onFileSelect: handleFileSelect,
onFilesSelect: handleFilesSelect,
onModalClose,
setOnModalClose: setModalCloseCallback,
};
};

View File

@ -1,8 +1,8 @@
import { FileWithUrl } from "../types/file"; import { FileWithUrl } from "../types/file";
import { StoredFile, fileStorage } from "../services/fileStorage"; import { StoredFile, fileStorage } from "../services/fileStorage";
export function getFileId(file: File): string { export function getFileId(file: File): string | null {
return (file as File & { id?: string }).id || file.name; return (file as File & { id?: string }).id || null;
} }
/** /**