# Description of Changes Currently, the `tsconfig.json` file enforces strict type checking, but nothing in CI checks that the code is actually correctly typed. [Vite only transpiles TypeScript code](https://vite.dev/guide/features.html#transpile-only) so doesn't ensure that the TS code we're running is correct. This PR adds running of the type checker to CI and fixes the type errors that have already crept into the codebase. Note that many of the changes I've made to 'fix the types' are just using `any` to disable the type checker because the code is under too much churn to fix anything properly at the moment. I still think enabling the type checker now is the best course of action though because otherwise we'll never be able to fix all of them, and it should at least help us not break things when adding new code. Co-authored-by: James <james@crosscourtanalytics.com>
223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
|
import { FileWithUrl } from '../types/file';
|
|
import { StoredFile } from '../services/fileStorage';
|
|
|
|
// Type for the context value - now contains everything directly
|
|
interface FileManagerContextValue {
|
|
// State
|
|
activeSource: 'recent' | 'local' | 'drive';
|
|
selectedFileIds: string[];
|
|
searchTerm: string;
|
|
selectedFiles: FileWithUrl[];
|
|
filteredFiles: FileWithUrl[];
|
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
|
|
// Handlers
|
|
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
|
onLocalFileClick: () => void;
|
|
onFileSelect: (file: FileWithUrl) => void;
|
|
onFileRemove: (index: number) => void;
|
|
onFileDoubleClick: (file: FileWithUrl) => void;
|
|
onOpenFiles: () => void;
|
|
onSearchChange: (value: string) => void;
|
|
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
|
|
// External props
|
|
recentFiles: FileWithUrl[];
|
|
isFileSupported: (fileName: string) => boolean;
|
|
modalHeight: string;
|
|
}
|
|
|
|
// Create the context
|
|
const FileManagerContext = createContext<FileManagerContextValue | null>(null);
|
|
|
|
// Provider component props
|
|
interface FileManagerProviderProps {
|
|
children: React.ReactNode;
|
|
recentFiles: FileWithUrl[];
|
|
onFilesSelected: (files: FileWithUrl[]) => void;
|
|
onClose: () => void;
|
|
isFileSupported: (fileName: string) => boolean;
|
|
isOpen: boolean;
|
|
onFileRemove: (index: number) => void;
|
|
modalHeight: string;
|
|
storeFile: (file: File) => Promise<StoredFile>;
|
|
refreshRecentFiles: () => Promise<void>;
|
|
}
|
|
|
|
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|
children,
|
|
recentFiles,
|
|
onFilesSelected,
|
|
onClose,
|
|
isFileSupported,
|
|
isOpen,
|
|
onFileRemove,
|
|
modalHeight,
|
|
storeFile,
|
|
refreshRecentFiles,
|
|
}) => {
|
|
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
|
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Track blob URLs for cleanup
|
|
const createdBlobUrls = useRef<Set<string>>(new Set());
|
|
|
|
// Computed values (with null safety)
|
|
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name));
|
|
const filteredFiles = (recentFiles || []).filter(file =>
|
|
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => {
|
|
setActiveSource(source);
|
|
if (source !== 'recent') {
|
|
setSelectedFileIds([]);
|
|
setSearchTerm('');
|
|
}
|
|
}, []);
|
|
|
|
const handleLocalFileClick = useCallback(() => {
|
|
fileInputRef.current?.click();
|
|
}, []);
|
|
|
|
const handleFileSelect = useCallback((file: FileWithUrl) => {
|
|
setSelectedFileIds(prev => {
|
|
if (file.id) {
|
|
if (prev.includes(file.id)) {
|
|
return prev.filter(id => id !== file.id);
|
|
} else {
|
|
return [...prev, file.id];
|
|
}
|
|
} else {
|
|
return prev;
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
const handleFileRemove = useCallback((index: number) => {
|
|
const fileToRemove = filteredFiles[index];
|
|
if (fileToRemove) {
|
|
setSelectedFileIds(prev => prev.filter(id => id !== fileToRemove.id));
|
|
}
|
|
onFileRemove(index);
|
|
}, [filteredFiles, onFileRemove]);
|
|
|
|
const handleFileDoubleClick = useCallback((file: FileWithUrl) => {
|
|
if (isFileSupported(file.name)) {
|
|
onFilesSelected([file]);
|
|
onClose();
|
|
}
|
|
}, [isFileSupported, onFilesSelected, onClose]);
|
|
|
|
const handleOpenFiles = useCallback(() => {
|
|
if (selectedFiles.length > 0) {
|
|
onFilesSelected(selectedFiles);
|
|
onClose();
|
|
}
|
|
}, [selectedFiles, onFilesSelected, onClose]);
|
|
|
|
const handleSearchChange = useCallback((value: string) => {
|
|
setSearchTerm(value);
|
|
}, []);
|
|
|
|
const handleFileInputChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(event.target.files || []);
|
|
if (files.length > 0) {
|
|
try {
|
|
// Create FileWithUrl objects - FileContext will handle storage and ID assignment
|
|
const fileWithUrls = files.map(file => {
|
|
const url = URL.createObjectURL(file);
|
|
createdBlobUrls.current.add(url);
|
|
|
|
return {
|
|
// No ID assigned here - FileContext will handle storage and ID assignment
|
|
name: file.name,
|
|
file,
|
|
url,
|
|
size: file.size,
|
|
lastModified: file.lastModified,
|
|
};
|
|
});
|
|
|
|
onFilesSelected(fileWithUrls as any /* FIX ME */);
|
|
await refreshRecentFiles();
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Failed to process selected files:', error);
|
|
}
|
|
}
|
|
event.target.value = '';
|
|
}, [storeFile, onFilesSelected, refreshRecentFiles, onClose]);
|
|
|
|
// Cleanup blob URLs when component unmounts
|
|
useEffect(() => {
|
|
return () => {
|
|
// Clean up all created blob URLs
|
|
createdBlobUrls.current.forEach(url => {
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
createdBlobUrls.current.clear();
|
|
};
|
|
}, []);
|
|
|
|
// Reset state when modal closes
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setActiveSource('recent');
|
|
setSelectedFileIds([]);
|
|
setSearchTerm('');
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const contextValue: FileManagerContextValue = {
|
|
// State
|
|
activeSource,
|
|
selectedFileIds,
|
|
searchTerm,
|
|
selectedFiles,
|
|
filteredFiles,
|
|
fileInputRef,
|
|
|
|
// Handlers
|
|
onSourceChange: handleSourceChange,
|
|
onLocalFileClick: handleLocalFileClick,
|
|
onFileSelect: handleFileSelect,
|
|
onFileRemove: handleFileRemove,
|
|
onFileDoubleClick: handleFileDoubleClick,
|
|
onOpenFiles: handleOpenFiles,
|
|
onSearchChange: handleSearchChange,
|
|
onFileInputChange: handleFileInputChange,
|
|
|
|
// External props
|
|
recentFiles,
|
|
isFileSupported,
|
|
modalHeight,
|
|
};
|
|
|
|
return (
|
|
<FileManagerContext.Provider value={contextValue}>
|
|
{children}
|
|
</FileManagerContext.Provider>
|
|
);
|
|
};
|
|
|
|
// Custom hook to use the context
|
|
export const useFileManagerContext = (): FileManagerContextValue => {
|
|
const context = useContext(FileManagerContext);
|
|
|
|
if (!context) {
|
|
throw new Error(
|
|
'useFileManagerContext must be used within a FileManagerProvider. ' +
|
|
'Make sure you wrap your component with <FileManagerProvider>.'
|
|
);
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
// Export the context for advanced use cases
|
|
export { FileManagerContext };
|