Enforce type checking in CI (#4126)

# 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>
This commit is contained in:
James Brunton
2025-08-11 09:16:16 +01:00
committed by GitHub
parent 507ad1dc61
commit af5a9d1ae1
52 changed files with 1141 additions and 919 deletions

View File

@@ -3,9 +3,9 @@
*/
import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef } from 'react';
import {
FileContextValue,
FileContextState,
import {
FileContextValue,
FileContextState,
FileContextProviderProps,
ModeType,
ViewType,
@@ -53,7 +53,7 @@ const initialState: FileContextState = {
};
// Action types
type FileContextAction =
type FileContextAction =
| { type: 'SET_ACTIVE_FILES'; payload: File[] }
| { type: 'ADD_FILES'; payload: File[] }
| { type: 'REMOVE_FILES'; payload: string[] }
@@ -126,7 +126,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'SET_CURRENT_MODE':
const coreViews = ['viewer', 'pageEditor', 'fileEditor'];
const isToolMode = !coreViews.includes(action.payload);
return {
...state,
currentMode: action.payload,
@@ -193,8 +193,8 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
const existing = newHistory.get(action.payload.fileId);
newHistory.set(action.payload.fileId, {
fileId: action.payload.fileId,
pageOperations: existing ?
[...existing.pageOperations, ...action.payload.operations] :
pageOperations: existing ?
[...existing.pageOperations, ...action.payload.operations] :
action.payload.operations,
lastModified: Date.now()
});
@@ -213,7 +213,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
const { fileId, operation } = action.payload;
const newOperationHistory = new Map(state.fileOperationHistory);
const existingHistory = newOperationHistory.get(fileId);
if (existingHistory) {
// Add operation to existing history
newOperationHistory.set(fileId, {
@@ -231,7 +231,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
lastModified: Date.now()
});
}
return {
...state,
fileOperationHistory: newOperationHistory
@@ -240,10 +240,10 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'MARK_OPERATION_APPLIED':
const appliedHistory = new Map(state.fileOperationHistory);
const appliedFileHistory = appliedHistory.get(action.payload.fileId);
if (appliedFileHistory) {
const updatedOperations = appliedFileHistory.operations.map(op =>
op.id === action.payload.operationId
const updatedOperations = appliedFileHistory.operations.map(op =>
op.id === action.payload.operationId
? { ...op, status: 'applied' as const }
: op
);
@@ -253,7 +253,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
lastModified: Date.now()
});
}
return {
...state,
fileOperationHistory: appliedHistory
@@ -262,12 +262,12 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'MARK_OPERATION_FAILED':
const failedHistory = new Map(state.fileOperationHistory);
const failedFileHistory = failedHistory.get(action.payload.fileId);
if (failedFileHistory) {
const updatedOperations = failedFileHistory.operations.map(op =>
op.id === action.payload.operationId
? {
...op,
const updatedOperations = failedFileHistory.operations.map(op =>
op.id === action.payload.operationId
? {
...op,
status: 'failed' as const,
metadata: { ...op.metadata, error: action.payload.error }
}
@@ -279,7 +279,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
lastModified: Date.now()
});
}
return {
...state,
fileOperationHistory: failedHistory
@@ -337,19 +337,19 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
const FileContext = createContext<FileContextValue | undefined>(undefined);
// Provider component
export function FileContextProvider({
children,
export function FileContextProvider({
children,
enableUrlSync = true,
enablePersistence = true,
maxCacheSize = 1024 * 1024 * 1024 // 1GB
}: FileContextProviderProps) {
const [state, dispatch] = useReducer(fileContextReducer, initialState);
// Cleanup timers and refs
const cleanupTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
const blobUrls = useRef<Set<string>>(new Set());
const pdfDocuments = useRef<Map<string, PDFDocument>>(new Map());
// Enhanced file processing hook
const {
processedFiles,
@@ -367,11 +367,11 @@ export function FileContextProvider({
// Update processed files when they change
useEffect(() => {
dispatch({ type: 'SET_PROCESSED_FILES', payload: processedFiles });
dispatch({
type: 'SET_PROCESSING',
payload: {
isProcessing: globalProcessing,
progress: processingProgress.overall
dispatch({
type: 'SET_PROCESSING',
payload: {
isProcessing: globalProcessing,
progress: processingProgress.overall
}
});
}, [processedFiles, globalProcessing, processingProgress.overall]);
@@ -397,7 +397,7 @@ export function FileContextProvider({
const cleanupFile = useCallback(async (fileId: string) => {
console.log('Cleaning up file:', fileId);
try {
// Cancel any pending cleanup timer
const timer = cleanupTimers.current.get(fileId);
@@ -425,7 +425,7 @@ export function FileContextProvider({
const cleanupAllFiles = useCallback(() => {
console.log('Cleaning up all files');
try {
// Clear all timers
cleanupTimers.current.forEach(timer => clearTimeout(timer));
@@ -461,7 +461,8 @@ export function FileContextProvider({
// Force garbage collection hint
if (typeof window !== 'undefined' && window.gc) {
setTimeout(() => window.gc(), 100);
let gc = window.gc
setTimeout(() => gc(), 100);
}
} catch (error) {
@@ -486,14 +487,14 @@ export function FileContextProvider({
const timer = setTimeout(() => {
cleanupFile(fileId);
}, delay);
cleanupTimers.current.set(fileId, timer);
}, [cleanupFile]);
// Action implementations
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
dispatch({ type: 'ADD_FILES', payload: files });
// Auto-save to IndexedDB if persistence enabled
if (enablePersistence) {
for (const file of files) {
@@ -504,7 +505,7 @@ export function FileContextProvider({
// File doesn't have explicit ID, store it with thumbnail
try {
// Generate thumbnail for better recent files experience
const thumbnail = await thumbnailGenerationService.generateThumbnail(file);
const thumbnail = await (thumbnailGenerationService as any /* FIX ME */).generateThumbnail(file);
const storedFile = await fileStorage.storeFile(file, thumbnail);
// Add the ID to the file object
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
@@ -520,7 +521,7 @@ export function FileContextProvider({
}
}
}
// Return files with their IDs assigned
return files;
}, [enablePersistence]);
@@ -532,9 +533,9 @@ export function FileContextProvider({
enhancedPDFProcessingService.cancelProcessing(fileId);
cleanupFile(fileId);
});
dispatch({ type: 'REMOVE_FILES', payload: fileIds });
// Remove from IndexedDB only if requested
if (enablePersistence && deleteFromStorage) {
fileIds.forEach(async (fileId) => {
@@ -557,7 +558,7 @@ export function FileContextProvider({
const clearAllFiles = useCallback(() => {
// Cleanup all memory before clearing files
cleanupAllFiles();
dispatch({ type: 'SET_ACTIVE_FILES', payload: [] });
dispatch({ type: 'CLEAR_SELECTIONS' });
}, [cleanupAllFiles]);
@@ -594,11 +595,12 @@ export function FileContextProvider({
const setCurrentMode = useCallback((mode: ModeType) => {
requestNavigation(() => {
dispatch({ type: 'SET_CURRENT_MODE', payload: mode });
if (state.currentMode !== mode && state.activeFiles.length > 0) {
if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) {
let gc = window.gc;
window.requestIdleCallback(() => {
window.gc();
gc();
}, { timeout: 5000 });
}
}
@@ -608,11 +610,12 @@ export function FileContextProvider({
const setCurrentView = useCallback((view: ViewType) => {
requestNavigation(() => {
dispatch({ type: 'SET_CURRENT_VIEW', payload: view });
if (state.currentView !== view && state.activeFiles.length > 0) {
if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) {
let gc = window.gc;
window.requestIdleCallback(() => {
window.gc();
gc();
}, { timeout: 5000 });
}
}
@@ -642,8 +645,8 @@ export function FileContextProvider({
}, []);
const applyPageOperations = useCallback((fileId: string, operations: PageOperation[]) => {
dispatch({
type: 'ADD_PAGE_OPERATIONS',
dispatch({
type: 'ADD_PAGE_OPERATIONS',
payload: { fileId, operations }
});
}, []);
@@ -718,18 +721,18 @@ export function FileContextProvider({
// Context persistence
const saveContext = useCallback(async () => {
if (!enablePersistence) return;
try {
const contextData = {
currentView: state.currentView,
currentTool: state.currentTool,
selectedFileIds: state.selectedFileIds,
selectedPageIds: state.selectedPageIds,
selectedPageNumbers: state.selectedPageNumbers,
viewerConfig: state.viewerConfig,
lastExportConfig: state.lastExportConfig,
timestamp: Date.now()
};
localStorage.setItem('fileContext', JSON.stringify(contextData));
} catch (error) {
console.error('Failed to save context:', error);
@@ -738,7 +741,7 @@ export function FileContextProvider({
const loadContext = useCallback(async () => {
if (!enablePersistence) return;
try {
const saved = localStorage.getItem('fileContext');
if (saved) {
@@ -779,7 +782,7 @@ export function FileContextProvider({
const contextValue: FileContextValue = {
// State
...state,
// Actions
addFiles,
removeFiles,
@@ -804,7 +807,7 @@ export function FileContextProvider({
saveContext,
loadContext,
resetContext,
// Operation history management
recordOperation,
markOperationApplied,
@@ -812,13 +815,13 @@ export function FileContextProvider({
getFileHistory,
getAppliedOperations,
clearFileHistory,
// Navigation guard system
setHasUnsavedChanges,
requestNavigation,
confirmNavigation,
cancelNavigation,
// Memory management
trackBlobUrl,
trackPdfDocument,
@@ -852,17 +855,17 @@ export function useCurrentFile() {
}
export function useFileSelection() {
const {
selectedFileIds,
selectedPageIds,
setSelectedFiles,
setSelectedPages,
clearSelections
const {
selectedFileIds,
selectedPageNumbers,
setSelectedFiles,
setSelectedPages,
clearSelections
} = useFileContext();
return {
selectedFileIds,
selectedPageIds,
selectedPageNumbers,
setSelectedFiles,
setSelectedPages,
clearSelections
@@ -875,4 +878,4 @@ export function useViewerState() {
config: viewerConfig,
updateConfig: updateViewerConfig
};
}
}

View File

@@ -10,8 +10,8 @@ interface FileManagerContextValue {
searchTerm: string;
selectedFiles: FileWithUrl[];
filteredFiles: FileWithUrl[];
fileInputRef: React.RefObject<HTMLInputElement>;
fileInputRef: React.RefObject<HTMLInputElement | null>;
// Handlers
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
onLocalFileClick: () => void;
@@ -21,7 +21,7 @@ interface FileManagerContextValue {
onOpenFiles: () => void;
onSearchChange: (value: string) => void;
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
// External props
recentFiles: FileWithUrl[];
isFileSupported: (fileName: string) => boolean;
@@ -61,7 +61,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
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());
@@ -85,10 +85,14 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const handleFileSelect = useCallback((file: FileWithUrl) => {
setSelectedFileIds(prev => {
if (prev.includes(file.id)) {
return prev.filter(id => id !== file.id);
if (file.id) {
if (prev.includes(file.id)) {
return prev.filter(id => id !== file.id);
} else {
return [...prev, file.id];
}
} else {
return [...prev, file.id];
return prev;
}
});
}, []);
@@ -127,7 +131,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
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,
@@ -137,8 +141,8 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
lastModified: file.lastModified,
};
});
onFilesSelected(fileWithUrls);
onFilesSelected(fileWithUrls as any /* FIX ME */);
await refreshRecentFiles();
onClose();
} catch (error) {
@@ -176,7 +180,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
selectedFiles,
filteredFiles,
fileInputRef,
// Handlers
onSourceChange: handleSourceChange,
onLocalFileClick: handleLocalFileClick,
@@ -186,7 +190,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
onOpenFiles: handleOpenFiles,
onSearchChange: handleSearchChange,
onFileInputChange: handleFileInputChange,
// External props
recentFiles,
isFileSupported,
@@ -203,16 +207,16 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
// 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 };
export { FileManagerContext };

View File

@@ -7,7 +7,7 @@ interface FilesModalContextType {
closeFilesModal: () => void;
onFileSelect: (file: File) => void;
onFilesSelect: (files: File[]) => void;
onModalClose: () => void;
onModalClose?: () => void;
setOnModalClose: (callback: () => void) => void;
}
@@ -64,4 +64,4 @@ export const useFilesModalContext = () => {
throw new Error('useFilesModalContext must be used within FilesModalProvider');
}
return context;
};
};