From 13daa069ad7050f6dd756b63eb8abb9df8465bfa Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Thu, 2 Oct 2025 16:05:20 +0100 Subject: [PATCH] Zip in common area --- .../src/components/fileEditor/FileEditor.tsx | 20 ++--- frontend/src/contexts/FileManagerContext.tsx | 20 +++-- frontend/src/services/zipFileService.ts | 77 ++++++++++++++++++- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index f7a1aa9f7..7f0fb0cad 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack, Group } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext'; +import { useFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext'; import { useNavigationActions } from '../../contexts/NavigationContext'; import { zipFileService } from '../../services/zipFileService'; import { detectFileExtension } from '../../utils/fileUtils'; @@ -37,6 +37,7 @@ const FileEditor = ({ // Use optimized FileContext hooks const { state, selectors } = useFileState(); const { addFiles, removeFiles, reorderFiles } = useFileManagement(); + const { actions } = useFileActions(); // Extract needed values from state (memoized to prevent infinite loops) const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]); @@ -314,19 +315,19 @@ const FileEditor = ({ const file = record ? selectors.getFile(record.id) : null; if (record && file) { try { - // Extract files from the ZIP - const extractionResult = await zipFileService.extractPdfFiles(file); + // Extract and store files using shared service method + const result = await zipFileService.extractAndStoreFilesWithHistory(file, record); - if (extractionResult.success && extractionResult.extractedFiles.length > 0) { - // Add extracted files to FileContext - await addFiles(extractionResult.extractedFiles); + if (result.success && result.extractedStubs.length > 0) { + // Add extracted file stubs to FileContext + await actions.addStirlingFileStubs(result.extractedStubs); // Remove the original ZIP file removeFiles([fileId], false); alert({ alertType: 'success', - title: `Extracted ${extractionResult.extractedFiles.length} file(s) from ${file.name}`, + title: `Extracted ${result.extractedStubs.length} file(s) from ${file.name}`, expandable: false, durationMs: 3500 }); @@ -334,7 +335,8 @@ const FileEditor = ({ alert({ alertType: 'error', title: `Failed to extract files from ${file.name}`, - expandable: false, + body: result.errors.join('\n'), + expandable: true, durationMs: 3500 }); } @@ -348,7 +350,7 @@ const FileEditor = ({ }); } } - }, [activeStirlingFileStubs, selectors, addFiles, removeFiles]); + }, [activeStirlingFileStubs, selectors, actions, removeFiles]); const handleViewFile = useCallback((fileId: FileId) => { const record = activeStirlingFileStubs.find(r => r.id === fileId); diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 6eabb4144..28a30fc20 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -554,23 +554,21 @@ export const FileManagerProvider: React.FC = ({ return; } - // Extract files from the ZIP - const extractionResult = await zipFileService.extractPdfFiles(stirlingFile); + // Extract and store files using shared service method + const result = await zipFileService.extractAndStoreFilesWithHistory(stirlingFile, file); - if (extractionResult.success && extractionResult.extractedFiles.length > 0) { - // Add extracted files to the file manager - onNewFilesSelect(extractionResult.extractedFiles); + if (result.success) { + // Refresh file manager to show new files + await refreshRecentFiles(); + } - // Optionally remove the original ZIP file - const fileIndex = filteredFiles.findIndex(f => f.id === file.id); - if (fileIndex !== -1) { - await handleFileRemove(fileIndex); - } + if (result.errors.length > 0) { + console.error('Errors during unzip:', result.errors); } } catch (error) { console.error('Failed to unzip file:', error); } - }, [onNewFilesSelect, filteredFiles, handleFileRemove]); + }, [refreshRecentFiles]); // Cleanup blob URLs when component unmounts useEffect(() => { diff --git a/frontend/src/services/zipFileService.ts b/frontend/src/services/zipFileService.ts index 6ce56e729..9803c96ed 100644 --- a/frontend/src/services/zipFileService.ts +++ b/frontend/src/services/zipFileService.ts @@ -1,5 +1,7 @@ import JSZip, { JSZipObject } from 'jszip'; -import { StirlingFileStub } from '../types/fileContext'; +import { StirlingFileStub, createStirlingFile } from '../types/fileContext'; +import { generateThumbnailForFile } from '../utils/thumbnailUtils'; +import { fileStorage } from './fileStorage'; // Undocumented interface in JSZip for JSZipObject._data interface CompressedObject { @@ -440,6 +442,79 @@ export class ZipFileService { return mimeTypes[ext || ''] || 'application/octet-stream'; } + + /** + * Extract PDF files from ZIP and store them in IndexedDB with preserved history metadata + * Used by both FileManager and FileEditor to avoid code duplication + * + * @param zipFile - The ZIP file to extract from + * @param zipStub - The StirlingFileStub for the ZIP (contains metadata to preserve) + * @returns Object with success status, extracted stubs, and any errors + */ + async extractAndStoreFilesWithHistory( + zipFile: File, + zipStub: StirlingFileStub + ): Promise<{ success: boolean; extractedStubs: StirlingFileStub[]; errors: string[] }> { + const result = { + success: false, + extractedStubs: [] as StirlingFileStub[], + errors: [] as string[] + }; + + try { + // Extract PDF files from ZIP + const extractionResult = await this.extractPdfFiles(zipFile); + + if (!extractionResult.success || extractionResult.extractedFiles.length === 0) { + result.errors = extractionResult.errors; + return result; + } + + // Process each extracted file + for (const extractedFile of extractionResult.extractedFiles) { + try { + // Generate thumbnail + const thumbnail = await generateThumbnailForFile(extractedFile); + + // Create StirlingFile + const newStirlingFile = createStirlingFile(extractedFile); + + // Create StirlingFileStub with ZIP's history metadata + const stub: StirlingFileStub = { + id: newStirlingFile.fileId, + name: extractedFile.name, + size: extractedFile.size, + type: extractedFile.type, + lastModified: extractedFile.lastModified, + quickKey: newStirlingFile.quickKey, + createdAt: Date.now(), + isLeaf: true, + // Preserve ZIP's history - unzipping is NOT a tool operation + originalFileId: zipStub.originalFileId, + parentFileId: zipStub.parentFileId, + versionNumber: zipStub.versionNumber, + toolHistory: zipStub.toolHistory || [], + thumbnailUrl: thumbnail + }; + + // Store in IndexedDB + await fileStorage.storeStirlingFile(newStirlingFile, stub); + + result.extractedStubs.push(stub); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + result.errors.push(`Failed to process "${extractedFile.name}": ${errorMessage}`); + } + } + + result.success = result.extractedStubs.length > 0; + return result; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + result.errors.push(`Failed to extract ZIP file: ${errorMessage}`); + return result; + } + } } // Export singleton instance