From af5a9d1ae112f5852833feaa0f60d201f552ed52 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 11 Aug 2025 09:16:16 +0100 Subject: [PATCH] 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 --- .claude/settings.local.json | 3 +- frontend/package-lock.json | 16 ++ frontend/package.json | 5 +- .../src/components/fileEditor/FileEditor.tsx | 144 +++++------ .../history/FileOperationHistory.tsx | 11 +- frontend/src/components/layout/Workbench.tsx | 18 +- .../components/pageEditor/DragDropGrid.tsx | 2 +- .../components/pageEditor/FileThumbnail.tsx | 2 +- .../src/components/pageEditor/PageEditor.tsx | 152 +++++------ .../pageEditor/PageEditorControls.tsx | 2 +- .../components/pageEditor/PageThumbnail.tsx | 16 +- frontend/src/components/shared/FileCard.tsx | 3 +- frontend/src/components/shared/FileGrid.tsx | 6 +- .../src/components/shared/TopControls.tsx | 6 +- frontend/src/components/tools/ToolPanel.tsx | 18 +- .../convert/ConvertFromImageSettings.tsx | 10 +- .../tools/convert/ConvertFromWebSettings.tsx | 1 - .../tools/convert/ConvertToImageSettings.tsx | 4 +- .../tools/convert/ConvertToPdfaSettings.tsx | 2 +- .../components/tools/split/SplitSettings.tsx | 4 +- frontend/src/components/viewer/Viewer.tsx | 45 ++-- frontend/src/constants/convertConstants.ts | 6 +- frontend/src/constants/splitConstants.ts | 12 +- frontend/src/contexts/FileContext.tsx | 125 +++++----- frontend/src/contexts/FileManagerContext.tsx | 34 +-- frontend/src/contexts/FilesModalContext.tsx | 4 +- .../tools/convert/useConvertOperation.ts | 28 +-- .../useConvertParametersAutoDetection.test.ts | 160 ++++++------ .../src/hooks/tools/ocr/useOCROperation.ts | 2 +- .../src/hooks/tools/shared/useToolApiCalls.ts | 4 +- .../hooks/tools/shared/useToolOperation.ts | 6 +- .../hooks/tools/shared/useToolResources.ts | 22 +- frontend/src/hooks/useFileManager.ts | 9 +- frontend/src/hooks/useFileWithUrl.ts | 18 +- frontend/src/hooks/useIndexedDBThumbnail.ts | 22 +- frontend/src/pages/HomePage.tsx | 10 +- .../services/enhancedPDFProcessingService.ts | 133 +++++----- frontend/src/services/fileAnalyzer.ts | 34 +-- frontend/src/services/fileStorage.ts | 92 +++---- frontend/src/services/pdfProcessingService.ts | 39 +-- frontend/src/services/zipFileService.ts | 51 ++-- frontend/src/setupTests.ts | 4 +- frontend/src/tests/convert/ConvertE2E.spec.ts | 208 ++++++++++------ .../tests/convert/ConvertIntegration.test.tsx | 235 ++++++++++++++---- .../ConvertSmartDetectionIntegration.test.tsx | 162 ++++++------ frontend/src/theme/mantineTheme.ts | 36 +-- frontend/src/types/fileContext.ts | 44 ++-- frontend/src/types/pageEditor.ts | 3 +- frontend/src/types/tool.ts | 7 +- frontend/src/utils/fileUtils.ts | 34 +-- frontend/src/utils/thumbnailUtils.ts | 42 ++-- frontend/src/utils/toolOperationTracker.ts | 4 +- 52 files changed, 1141 insertions(+), 919 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cc5ded896..8032f1d50 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(find:*)", "Bash(npm test)", "Bash(npm test:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(npx tsc:*)" ], "deny": [] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 060a51d64..b2141beb7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ }, "devDependencies": { "@playwright/test": "^1.40.0", + "@types/node": "^24.2.0", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", @@ -2384,6 +2385,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dev": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -7404,6 +7414,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4ff3484b3..b59be58e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,8 +34,8 @@ "web-vitals": "^2.1.4" }, "scripts": { - "dev": "vite", - "build": "vite build", + "dev": "npx tsc --noEmit && vite", + "build": "npx tsc --noEmit && vite build", "preview": "vite preview", "generate-licenses": "node scripts/generate-licenses.js", "test": "vitest", @@ -65,6 +65,7 @@ }, "devDependencies": { "@playwright/test": "^1.40.0", + "@types/node": "^24.2.0", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index ca5f594b8..1494dfa9a 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -70,11 +70,11 @@ const FileEditor = ({ } = fileContext; // Get file selection context - const { - selectedFiles: toolSelectedFiles, - setSelectedFiles: setToolSelectedFiles, - maxFiles, - isToolMode + const { + selectedFiles: toolSelectedFiles, + setSelectedFiles: setToolSelectedFiles, + maxFiles, + isToolMode } = useFileSelection(); const [files, setFiles] = useState([]); @@ -82,7 +82,7 @@ const FileEditor = ({ const [error, setError] = useState(null); const [localLoading, setLocalLoading] = useState(false); const [selectionMode, setSelectionMode] = useState(toolMode); - + // Enable selection mode automatically in tool mode React.useEffect(() => { if (toolMode) { @@ -115,7 +115,7 @@ const FileEditor = ({ // Get selected file IDs from context (defensive programming) const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : []; - + // Map context selections to local file IDs for UI display const localSelectedIds = files .filter(file => { @@ -144,33 +144,33 @@ const FileEditor = ({ // Check if the actual content has changed, not just references const currentActiveFileNames = activeFiles.map(f => f.name); const currentProcessedFilesSize = processedFiles.size; - + const activeFilesChanged = JSON.stringify(currentActiveFileNames) !== JSON.stringify(lastActiveFilesRef.current); const processedFilesChanged = currentProcessedFilesSize !== lastProcessedFilesRef.current; - + if (!activeFilesChanged && !processedFilesChanged) { return; } - + // Update refs lastActiveFilesRef.current = currentActiveFileNames; lastProcessedFilesRef.current = currentProcessedFilesSize; - + const convertActiveFiles = async () => { - + if (activeFiles.length > 0) { setLocalLoading(true); try { // Process files in chunks to avoid blocking UI const convertedFiles: FileItem[] = []; - + for (let i = 0; i < activeFiles.length; i++) { const file = activeFiles[i]; - + // Try to get thumbnail from processed file first const processedFile = processedFiles.get(file); let thumbnail = processedFile?.pages?.[0]?.thumbnail; - + // If no thumbnail from processed file, try to generate one if (!thumbnail) { try { @@ -180,28 +180,28 @@ const FileEditor = ({ thumbnail = undefined; // Use placeholder } } - + const convertedFile = { id: `file-${Date.now()}-${Math.random()}`, name: file.name, pageCount: processedFile?.totalPages || Math.floor(Math.random() * 20) + 1, - thumbnail, + thumbnail: thumbnail || '', size: file.size, file, }; - + convertedFiles.push(convertedFile); - + // Update progress setConversionProgress(((i + 1) / activeFiles.length) * 100); - + // Yield to main thread between files if (i < activeFiles.length - 1) { await new Promise(resolve => requestAnimationFrame(resolve)); } } - - + + setFiles(convertedFiles); } catch (err) { console.error('Error converting active files:', err); @@ -237,7 +237,7 @@ const FileEditor = ({ try { // Validate ZIP file first const validation = await zipFileService.validateZipFile(file); - + if (validation.isValid && validation.containsPDFs) { // ZIP contains PDFs - extract them setZipExtractionProgress({ @@ -269,7 +269,7 @@ const FileEditor = ({ if (extractionResult.success) { allExtractedFiles.push(...extractionResult.extractedFiles); - + // Record ZIP extraction operation const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const operation: FileOperation = { @@ -289,10 +289,10 @@ const FileEditor = ({ } } }; - + recordOperation(file.name, operation); markOperationApplied(file.name, operationId); - + if (extractionResult.errors.length > 0) { errors.push(...extractionResult.errors); } @@ -344,7 +344,7 @@ const FileEditor = ({ } } }; - + recordOperation(file.name, operation); markOperationApplied(file.name, operationId); } @@ -357,7 +357,7 @@ const FileEditor = ({ const errorMessage = err instanceof Error ? err.message : 'Failed to process files'; setError(errorMessage); console.error('File processing error:', err); - + // Reset extraction progress on error setZipExtractionProgress({ isExtracting: false, @@ -377,7 +377,7 @@ const FileEditor = ({ const closeAllFiles = useCallback(() => { if (activeFiles.length === 0) return; - + // Record close all operation for each file activeFiles.forEach(file => { const operationId = `close-all-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; @@ -396,14 +396,14 @@ const FileEditor = ({ } } }; - + recordOperation(file.name, operation); markOperationApplied(file.name, operationId); }); - + // Remove all files from context but keep in storage removeFiles(activeFiles.map(f => (f as any).id || f.name), false); - + // Clear selections setContextSelectedFiles([]); }, [activeFiles, removeFiles, setContextSelectedFiles, recordOperation, markOperationApplied]); @@ -411,12 +411,12 @@ const FileEditor = ({ const toggleFile = useCallback((fileId: string) => { const targetFile = files.find(f => f.id === fileId); if (!targetFile) return; - + const contextFileId = (targetFile.file as any).id || targetFile.name; const isSelected = contextSelectedIds.includes(contextFileId); - + let newSelection: string[]; - + if (isSelected) { // Remove file from selection newSelection = contextSelectedIds.filter(id => id !== contextFileId); @@ -433,10 +433,10 @@ const FileEditor = ({ newSelection = [...contextSelectedIds, contextFileId]; } } - + // Update context setContextSelectedFiles(newSelection); - + // Update tool selection context if in tool mode if (isToolMode || toolMode) { const selectedFiles = files @@ -572,12 +572,12 @@ const FileEditor = ({ console.log('handleDeleteFile called with fileId:', fileId); const file = files.find(f => f.id === fileId); console.log('Found file:', file); - + if (file) { console.log('Attempting to remove file:', file.name); console.log('Actual file object:', file.file); console.log('Actual file.file.name:', file.file.name); - + // Record close operation const fileName = file.file.name; const fileId = (file.file as any).id || fileName; @@ -597,19 +597,16 @@ const FileEditor = ({ } } }; - + recordOperation(fileName, operation); - + // Remove file from context but keep in storage (close, don't delete) console.log('Calling removeFiles with:', [fileId]); removeFiles([fileId], false); - + // Remove from context selections - setContextSelectedFiles(prev => { - const safePrev = Array.isArray(prev) ? prev : []; - return safePrev.filter(id => id !== fileId); - }); - + const newSelection = contextSelectedIds.filter(id => id !== fileId); + setContextSelectedFiles(newSelection); // Mark operation as applied markOperationApplied(fileName, operationId); } else { @@ -670,7 +667,7 @@ const FileEditor = ({ accept={["*/*"]} multiple={true} maxSize={2 * 1024 * 1024 * 1024} - style={{ + style={{ height: '100vh', border: 'none', borderRadius: 0, @@ -707,7 +704,7 @@ const FileEditor = ({ ) : files.length === 0 && (localLoading || zipExtractionProgress.isExtracting) ? ( - + {/* ZIP Extraction Progress */} {zipExtractionProgress.isExtracting && ( @@ -721,10 +718,10 @@ const FileEditor = ({ {zipExtractionProgress.extractedCount} of {zipExtractionProgress.totalFiles} files extracted -
@@ -737,7 +734,7 @@ const FileEditor = ({
)} - + {/* Processing indicator */} {localLoading && ( @@ -745,10 +742,10 @@ const FileEditor = ({ Loading files... {Math.round(conversionProgress)}% -
@@ -761,27 +758,27 @@ const FileEditor = ({
)} - +
) : ( ( + onDragStart={handleDragStart as any /* FIX ME */} + onDragEnd={handleDragEnd} + onDragOver={handleDragOver} + onDragEnter={handleDragEnter as any /* FIX ME */} + onDragLeave={handleDragLeave} + onDrop={handleDrop as any /* FIX ME */} + onEndZoneDragEnter={handleEndZoneDragEnter} + draggedItem={draggedFile as any /* FIX ME */} + dropTarget={dropTarget as any /* FIX ME */} + multiItemDrag={multiFileDrag as any /* FIX ME */} + dragPosition={dragPosition} + renderItem={(file, index, refs) => ( setShowFilePickerModal(false)} storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent onSelectFiles={handleLoadFromStorage} - allowMultiple={true} /> {status && ( diff --git a/frontend/src/components/history/FileOperationHistory.tsx b/frontend/src/components/history/FileOperationHistory.tsx index 365b5a8f8..93b9cf015 100644 --- a/frontend/src/components/history/FileOperationHistory.tsx +++ b/frontend/src/components/history/FileOperationHistory.tsx @@ -27,9 +27,10 @@ const FileOperationHistory: React.FC = ({ maxHeight = 400 }) => { const { getFileHistory, getAppliedOperations } = useFileContext(); - + const history = getFileHistory(fileId); - const operations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || []; + const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || []; + const operations = allOperations.filter(op => 'fileIds' in op) as FileOperation[]; const formatTimestamp = (timestamp: number) => { return new Date(timestamp).toLocaleString(); @@ -62,7 +63,7 @@ const FileOperationHistory: React.FC = ({ } }; - const renderOperationDetails = (operation: FileOperation | PageOperation) => { + const renderOperationDetails = (operation: FileOperation) => { if ('metadata' in operation && operation.metadata) { const { metadata } = operation; return ( @@ -142,7 +143,7 @@ const FileOperationHistory: React.FC = ({ - + = ({ ); }; -export default FileOperationHistory; \ No newline at end of file +export default FileOperationHistory; diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index b0c984ee8..e6f101803 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -18,18 +18,18 @@ import LandingPage from '../shared/LandingPage'; export default function Workbench() { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); - + // Use context-based hooks to eliminate all prop drilling const { activeFiles, currentView, setCurrentView } = useFileContext(); - const { - previewFile, - pageEditorFunctions, + const { + previewFile, + pageEditorFunctions, sidebarsVisible, - setPreviewFile, + setPreviewFile, setPageEditorFunctions, setSidebarsVisible } = useWorkbenchState(); - + const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection(); const { addToActiveFiles } = useFileHandler(); @@ -142,10 +142,10 @@ export default function Workbench() { {/* Top Controls */} - + {/* Main content area */} ); -} \ No newline at end of file +} diff --git a/frontend/src/components/pageEditor/DragDropGrid.tsx b/frontend/src/components/pageEditor/DragDropGrid.tsx index 39dbb396f..3639f74d9 100644 --- a/frontend/src/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/components/pageEditor/DragDropGrid.tsx @@ -22,7 +22,7 @@ interface DragDropGridProps { renderItem: (item: T, index: number, refs: React.MutableRefObject>) => React.ReactNode; renderSplitMarker?: (item: T, index: number) => React.ReactNode; draggedItem: number | null; - dropTarget: number | null; + dropTarget: number | 'end' | null; multiItemDrag: {pageNumbers: number[], count: number} | null; dragPosition: {x: number, y: number} | null; } diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index b129ce6d9..eba9a12c5 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -345,7 +345,7 @@ const FileThumbnail = ({ onClose={() => setShowHistory(false)} title={`Operation History - ${file.name}`} size="lg" - scrollAreaComponent="div" + scrollAreaComponent={'div' as any} > void; exportLoading: boolean; selectionMode: boolean; - selectedPages: string[]; + selectedPages: number[]; closePdf: () => void; }) => void; } @@ -56,7 +56,7 @@ const PageEditor = ({ // Get file context const fileContext = useFileContext(); const { file: currentFile, processedFile: currentProcessedFile } = useCurrentFile(); - + // Use file context state const { activeFiles, @@ -81,12 +81,12 @@ const PageEditor = ({ // Simple computed document from processed files (no caching needed) const mergedPdfDocument = useMemo(() => { if (activeFiles.length === 0) return null; - + if (activeFiles.length === 1) { // Single file const processedFile = processedFiles.get(activeFiles[0]); if (!processedFile) return null; - + return { id: processedFile.id, name: activeFiles[0].name, @@ -108,7 +108,7 @@ const PageEditor = ({ const processedFile = processedFiles.get(file); if (processedFile) { filenames.push(file.name.replace(/\.pdf$/i, '')); - + processedFile.pages.forEach((page, pageIndex) => { const newPage: PDFPage = { ...page, @@ -119,7 +119,7 @@ const PageEditor = ({ }; allPages.push(newPage); }); - + totalPages += processedFile.pages.length; } }); @@ -140,7 +140,7 @@ const PageEditor = ({ const displayDocument = editedDocument || mergedPdfDocument; const [filename, setFilename] = useState(""); - + // Page editor state (use context for selectedPages) const [status, setStatus] = useState(null); @@ -149,7 +149,7 @@ const PageEditor = ({ // Drag and drop state const [draggedPage, setDraggedPage] = useState(null); - const [dropTarget, setDropTarget] = useState(null); + const [dropTarget, setDropTarget] = useState(null); const [multiPageDrag, setMultiPageDrag] = useState<{pageNumbers: number[], count: number} | null>(null); const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null); @@ -200,54 +200,54 @@ const PageEditor = ({ const [thumbnailGenerationStarted, setThumbnailGenerationStarted] = useState(false); // Thumbnail generation (opt-in for visual tools) - const { + const { generateThumbnails, - addThumbnailToCache, - getThumbnailFromCache, + addThumbnailToCache, + getThumbnailFromCache, stopGeneration, - destroyThumbnails + destroyThumbnails } = useThumbnailGeneration(); // Start thumbnail generation process (separate from document loading) const startThumbnailGeneration = useCallback(() => { console.log('🎬 PageEditor: startThumbnailGeneration called'); console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'activeFiles:', activeFiles.length, 'started:', thumbnailGenerationStarted); - + if (!mergedPdfDocument || activeFiles.length !== 1 || thumbnailGenerationStarted) { console.log('🎬 PageEditor: Skipping thumbnail generation due to conditions'); return; } - + const file = activeFiles[0]; const totalPages = mergedPdfDocument.totalPages; - + console.log('🎬 PageEditor: Starting thumbnail generation for', totalPages, 'pages'); setThumbnailGenerationStarted(true); - + // Run everything asynchronously to avoid blocking the main thread setTimeout(async () => { try { // Load PDF array buffer for Web Workers const arrayBuffer = await file.arrayBuffer(); - + // Generate page numbers for pages that don't have thumbnails yet const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1) .filter(pageNum => { const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); return !page?.thumbnail; // Only generate for pages without thumbnails }); - + console.log(`🎬 PageEditor: Generating thumbnails for ${pageNumbers.length} pages (out of ${totalPages} total):`, pageNumbers.slice(0, 10), pageNumbers.length > 10 ? '...' : ''); - + // If no pages need thumbnails, we're done if (pageNumbers.length === 0) { console.log('🎬 PageEditor: All pages already have thumbnails, no generation needed'); return; } - + // Calculate quality scale based on file size const scale = activeFiles.length === 1 ? calculateScaleFromFileSize(activeFiles[0].size) : 0.2; - + // Start parallel thumbnail generation WITHOUT blocking the main thread const generationPromise = generateThumbnails( arrayBuffer, @@ -267,11 +267,11 @@ const PageEditor = ({ // Check cache first, then send thumbnail const pageId = `${file.name}-page-${pageNumber}`; const cached = getThumbnailFromCache(pageId); - + if (!cached) { // Cache and send to component addThumbnailToCache(pageId, thumbnail); - + window.dispatchEvent(new CustomEvent('thumbnailReady', { detail: { pageNumber, thumbnail, pageId } })); @@ -292,7 +292,7 @@ const PageEditor = ({ console.error('✗ PageEditor: Web Worker thumbnail generation failed:', error); setThumbnailGenerationStarted(false); }); - + } catch (error) { console.error('Failed to start Web Worker thumbnail generation:', error); setThumbnailGenerationStarted(false); @@ -304,25 +304,25 @@ const PageEditor = ({ useEffect(() => { console.log('🎬 PageEditor: Thumbnail generation effect triggered'); console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'started:', thumbnailGenerationStarted); - + if (mergedPdfDocument && !thumbnailGenerationStarted) { // Check if ALL pages already have thumbnails from processed files const totalPages = mergedPdfDocument.pages.length; const pagesWithThumbnails = mergedPdfDocument.pages.filter(page => page.thumbnail).length; const hasAllThumbnails = pagesWithThumbnails === totalPages; - + console.log('🎬 PageEditor: Thumbnail status:', { totalPages, pagesWithThumbnails, hasAllThumbnails, missingThumbnails: totalPages - pagesWithThumbnails }); - + if (hasAllThumbnails) { console.log('🎬 PageEditor: Skipping generation - all thumbnails already exist'); return; // Skip generation if ALL thumbnails already exist } - + console.log('🎬 PageEditor: Some thumbnails missing, proceeding with generation'); // Small delay to let document render, then start thumbnail generation console.log('🎬 PageEditor: Scheduling thumbnail generation in 500ms'); @@ -394,10 +394,10 @@ const PageEditor = ({ const togglePage = useCallback((pageNumber: number) => { console.log('🔄 Toggling page', pageNumber); - + // Check if currently selected and update accordingly const isCurrentlySelected = selectedPageNumbers.includes(pageNumber); - + if (isCurrentlySelected) { // Remove from selection console.log('🔄 Removing page', pageNumber); @@ -524,24 +524,24 @@ const PageEditor = ({ // Update PDF document state with edit tracking const setPdfDocument = useCallback((updatedDoc: PDFDocument) => { console.log('setPdfDocument called - setting edited state'); - + // Update local edit state for immediate visual feedback setEditedDocument(updatedDoc); setHasUnsavedChanges(true); // Use global state setHasUnsavedDraft(true); // Mark that we have unsaved draft changes - + // Auto-save to drafts (debounced) - only if we have new changes if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } - + autoSaveTimer.current = setTimeout(() => { if (hasUnsavedDraft) { saveDraftToIndexedDB(updatedDoc); setHasUnsavedDraft(false); // Mark draft as saved } }, 30000); // Auto-save after 30 seconds of inactivity - + return updatedDoc; }, [setHasUnsavedChanges, hasUnsavedDraft]); @@ -554,7 +554,7 @@ const PageEditor = ({ timestamp: Date.now(), originalFiles: activeFiles.map(f => f.name) }; - + // Save to 'pdf-drafts' store in IndexedDB const request = indexedDB.open('stirling-pdf-drafts', 1); request.onupgradeneeded = () => { @@ -563,7 +563,7 @@ const PageEditor = ({ db.createObjectStore('drafts'); } }; - + request.onsuccess = () => { const db = request.result; const transaction = db.transaction('drafts', 'readwrite'); @@ -581,7 +581,7 @@ const PageEditor = ({ try { const draftKey = `draft-${mergedPdfDocument?.id || 'merged'}`; const request = indexedDB.open('stirling-pdf-drafts', 1); - + request.onsuccess = () => { const db = request.result; const transaction = db.transaction('drafts', 'readwrite'); @@ -596,12 +596,12 @@ const PageEditor = ({ // Apply changes to create new processed file const applyChanges = useCallback(async () => { if (!editedDocument || !mergedPdfDocument) return; - + try { if (activeFiles.length === 1) { const file = activeFiles[0]; const currentProcessedFile = processedFiles.get(file); - + if (currentProcessedFile) { const updatedProcessedFile = { ...currentProcessedFile, @@ -614,14 +614,14 @@ const PageEditor = ({ totalPages: editedDocument.pages.length, lastModified: Date.now() }; - + updateProcessedFile(file, updatedProcessedFile); } } else if (activeFiles.length > 1) { setStatus('Apply changes for multiple files not yet supported'); return; } - + // Wait for the processed file update to complete before clearing edit state setTimeout(() => { setEditedDocument(null); @@ -630,7 +630,7 @@ const PageEditor = ({ cleanupDraft(); setStatus('Changes applied successfully'); }, 100); - + } catch (error) { console.error('Failed to apply changes:', error); setStatus('Failed to apply changes'); @@ -653,7 +653,7 @@ const PageEditor = ({ // Skip animation for large documents (500+ pages) to improve performance const isLargeDocument = displayDocument.pages.length > 500; - + if (isLargeDocument) { // For large documents, just execute the command without animation if (pagesToMove.length > 1) { @@ -678,7 +678,7 @@ const PageEditor = ({ // Only capture positions for potentially affected pages const currentPositions = new Map(); - + affectedPageIds.forEach(pageId => { const element = document.querySelector(`[data-page-number="${pageId}"]`); if (element) { @@ -728,14 +728,14 @@ const PageEditor = ({ if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { elementsToAnimate.push(element); - + // Apply initial transform element.style.transform = `translate(${deltaX}px, ${deltaY}px)`; element.style.transition = 'none'; - + // Force reflow element.offsetHeight; - + // Animate to final position element.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; element.style.transform = 'translate(0px, 0px)'; @@ -863,13 +863,13 @@ const PageEditor = ({ if (!mergedPdfDocument) return; // Convert page numbers to page IDs for export service - const exportPageIds = selectedOnly + const exportPageIds = selectedOnly ? selectedPageNumbers.map(pageNum => { const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); return page?.id || ''; }).filter(id => id) : []; - + const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); @@ -881,16 +881,16 @@ const PageEditor = ({ setExportLoading(true); try { // Convert page numbers to page IDs for export service - const exportPageIds = selectedOnly + const exportPageIds = selectedOnly ? selectedPageNumbers.map(pageNum => { const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); return page?.id || ''; }).filter(id => id) : []; - + const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { - setError(errors.join(', ')); + setStatus(errors.join(', ')); return; } @@ -921,7 +921,7 @@ const PageEditor = ({ } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Export failed'; - setError(errorMessage); + setStatus(errorMessage); } finally { setExportLoading(false); } @@ -1009,26 +1009,26 @@ const PageEditor = ({ // Check for existing drafts const checkForDrafts = useCallback(async () => { if (!mergedPdfDocument) return; - + try { const draftKey = `draft-${mergedPdfDocument.id || 'merged'}`; const request = indexedDB.open('stirling-pdf-drafts', 1); - + request.onsuccess = () => { const db = request.result; if (!db.objectStoreNames.contains('drafts')) return; - + const transaction = db.transaction('drafts', 'readonly'); const store = transaction.objectStore('drafts'); const getRequest = store.get(draftKey); - + getRequest.onsuccess = () => { const draft = getRequest.result; if (draft && draft.timestamp) { // Check if draft is recent (within last 24 hours) const draftAge = Date.now() - draft.timestamp; const twentyFourHours = 24 * 60 * 60 * 1000; - + if (draftAge < twentyFourHours) { setFoundDraft(draft); setShowResumeModal(true); @@ -1066,12 +1066,12 @@ const PageEditor = ({ useEffect(() => { return () => { console.log('PageEditor unmounting - cleaning up resources'); - + // Clear auto-save timer if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } - + // Clean up draft if component unmounts with unsaved changes if (hasUnsavedChanges) { cleanupDraft(); @@ -1125,7 +1125,7 @@ const PageEditor = ({ {showLoading && ( - + {/* Progress indicator */} @@ -1136,10 +1136,10 @@ const PageEditor = ({ {Math.round(processingProgress || 0)}% -
@@ -1151,7 +1151,7 @@ const PageEditor = ({ }} />
- +
)} @@ -1165,10 +1165,10 @@ const PageEditor = ({ Processing thumbnails... {Math.round(processingProgress || 0)}% -
@@ -1210,7 +1210,7 @@ const PageEditor = ({ )} - + {/* Apply Changes Button */} {hasUnsavedChanges && ( - +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx b/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx index 78d2e75a8..0681821fd 100644 --- a/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx @@ -30,12 +30,12 @@ const ConvertFromImageSettings = ({ })} data={[ { value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") }, - { value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") }, + { value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") }, { value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") }, ]} disabled={disabled} /> - +