From 2684fe55737b3bff2855c60c1fce2fb23403ce08 Mon Sep 17 00:00:00 2001 From: Reece Date: Thu, 2 Oct 2025 16:31:54 +0100 Subject: [PATCH] Save and continue option when leaving page editor --- .../src/components/pageEditor/PageEditor.tsx | 108 ++++++++---------- .../shared/NavigationWarningModal.tsx | 52 +++++---- .../components/viewer/SignatureAPIBridge.tsx | 2 +- frontend/src/services/fileStubHelpers.ts | 34 ++++++ frontend/src/services/pdfExportHelpers.ts | 46 ++++++++ 5 files changed, 161 insertions(+), 81 deletions(-) create mode 100644 frontend/src/services/fileStubHelpers.ts create mode 100644 frontend/src/services/pdfExportHelpers.ts diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index b24fb819a..ee77d142d 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -5,6 +5,8 @@ import { useNavigationGuard } from "../../contexts/NavigationContext"; import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor"; import { pdfExportService } from "../../services/pdfExportService"; import { documentManipulationService } from "../../services/documentManipulationService"; +import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers"; +import { createStirlingFilesAndStubs } from "../../services/fileStubHelpers"; // Thumbnail generation is now handled by individual PageThumbnail components import './PageEditor.module.css'; import PageThumbnail from './PageThumbnail'; @@ -524,66 +526,38 @@ const PageEditor = ({ try { // Step 1: Apply DOM changes to document state first const processedDocuments = documentManipulationService.applyDOMChangesToDocument( - mergedPdfDocument || displayDocument, // Original order - displayDocument, // Current display order (includes reordering) - splitPositions // Position-based splits + mergedPdfDocument || displayDocument, + displayDocument, + splitPositions ); - // Step 2: Check if we have multiple documents (splits) or single document - if (Array.isArray(processedDocuments)) { - // Multiple documents (splits) - export as ZIP - const blobs: Blob[] = []; - const filenames: string[] = []; + // Step 2: Export to files + const sourceFiles = getSourceFiles(); + const exportFilename = getExportFilename(); + const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename); - const sourceFiles = getSourceFiles(); - const baseExportFilename = getExportFilename(); - const baseName = baseExportFilename.replace(/\.pdf$/i, ''); - - for (let i = 0; i < processedDocuments.length; i++) { - const doc = processedDocuments[i]; - const partFilename = `${baseName}_part_${i + 1}.pdf`; - - const result = sourceFiles - ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename }) - : await pdfExportService.exportPDF(doc, [], { filename: partFilename }); - blobs.push(result.blob); - filenames.push(result.filename); - } - - // Create ZIP file + // Step 3: Download + if (files.length > 1) { + // Multiple files - create ZIP const JSZip = await import('jszip'); const zip = new JSZip.default(); - blobs.forEach((blob, index) => { - zip.file(filenames[index], blob); + files.forEach((file) => { + zip.file(file.name, file); }); const zipBlob = await zip.generateAsync({ type: 'blob' }); - const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip'); + const exportFilename = getExportFilename(); + const zipFilename = exportFilename.replace(/\.pdf$/i, '.zip'); pdfExportService.downloadFile(zipBlob, zipFilename); - setHasUnsavedChanges(false); // Clear unsaved changes after successful export } else { - // Single document - regular export - const sourceFiles = getSourceFiles(); - const exportFilename = getExportFilename(); - const result = sourceFiles - ? await pdfExportService.exportPDFMultiFile( - processedDocuments, - sourceFiles, - [], - { selectedOnly: false, filename: exportFilename } - ) - : await pdfExportService.exportPDF( - processedDocuments, - [], - { selectedOnly: false, filename: exportFilename } - ); - - pdfExportService.downloadFile(result.blob, result.filename); - setHasUnsavedChanges(false); // Clear unsaved changes after successful export + // Single file - download directly + const file = files[0]; + pdfExportService.downloadFile(file, file.name); } + setHasUnsavedChanges(false); setExportLoading(false); } catch (error) { console.error('Export failed:', error); @@ -592,21 +566,39 @@ const PageEditor = ({ }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); // Apply DOM changes to document state using dedicated service - const applyChanges = useCallback(() => { + const applyChanges = useCallback(async () => { if (!displayDocument) return; - // Pass current display document (which includes reordering) to get both reordering AND DOM changes - const processedDocuments = documentManipulationService.applyDOMChangesToDocument( - mergedPdfDocument || displayDocument, // Original order - displayDocument, // Current display order (includes reordering) - splitPositions // Position-based splits - ); + setExportLoading(true); + try { + // Step 1: Apply DOM changes to document state first + const processedDocuments = documentManipulationService.applyDOMChangesToDocument( + mergedPdfDocument || displayDocument, + displayDocument, + splitPositions + ); - // For apply changes, we only set the first document if it's an array (splits shouldn't affect document state) - const documentToSet = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments; - setEditedDocument(documentToSet); + // Step 2: Export to files + const sourceFiles = getSourceFiles(); + const exportFilename = getExportFilename(); + const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename); - }, [displayDocument, mergedPdfDocument, splitPositions]); + // Step 3: Create StirlingFiles and stubs for version history + const parentStub = selectors.getStirlingFileStub(activeFileIds[0]); + if (!parentStub) throw new Error('Parent stub not found'); + + const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool'); + + // Step 4: Consume files (replace in context) + await actions.consumeFiles(activeFileIds, stirlingFiles, stubs); + + setHasUnsavedChanges(false); + setExportLoading(false); + } catch (error) { + console.error('Apply changes failed:', error); + setExportLoading(false); + } + }, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]); const closePdf = useCallback(() => { @@ -793,7 +785,7 @@ const PageEditor = ({ { - applyChanges(); + await applyChanges(); }} onExportAndContinue={async () => { await onExportAll(); diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx index 203e66ff7..b3964edf3 100644 --- a/frontend/src/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/components/shared/NavigationWarningModal.tsx @@ -8,7 +8,7 @@ interface NavigationWarningModalProps { } const NavigationWarningModal = ({ - onApplyAndContinue: _onApplyAndContinue, + onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => { @@ -30,6 +30,13 @@ const NavigationWarningModal = ({ confirmNavigation(); }; + const handleApplyAndContinue = async () => { + if (onApplyAndContinue) { + await onApplyAndContinue(); + } + setHasUnsavedChanges(false); + confirmNavigation(); + }; const handleExportAndContinue = async () => { if (onExportAndContinue) { @@ -49,7 +56,7 @@ const NavigationWarningModal = ({ onClose={handleKeepWorking} title={t("unsavedChangesTitle", "Unsaved Changes")} centered - size="lg" + size="xl" closeOnClickOutside={false} closeOnEscape={false} > @@ -58,17 +65,16 @@ const NavigationWarningModal = ({ {t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")} - - - + + + - {/* TODO:: Add this back in when it works */} - {/* {_onApplyAndContinue && ( - - )} */} - + {onExportAndContinue && ( )} + + {onApplyAndContinue && ( + + )} diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx index 59fbe43e6..6ff048575 100644 --- a/frontend/src/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx @@ -28,7 +28,7 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI useEffect(() => { if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return; - const handleKeyDown = (event: KeyboardEvent) => { + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Delete' || event.key === 'Backspace') { const selectedAnnotation = annotationApi.getSelectedAnnotation?.(); diff --git a/frontend/src/services/fileStubHelpers.ts b/frontend/src/services/fileStubHelpers.ts new file mode 100644 index 000000000..5f5545e76 --- /dev/null +++ b/frontend/src/services/fileStubHelpers.ts @@ -0,0 +1,34 @@ +import { StirlingFile, StirlingFileStub } from '../types/fileContext'; +import { createChildStub, generateProcessedFileMetadata } from '../contexts/file/fileActions'; +import { createStirlingFile } from '../types/fileContext'; +import { ToolId } from '../types/toolId'; + +/** + * Create StirlingFiles and StirlingFileStubs from exported files + * Used when saving page editor changes to create version history + */ +export async function createStirlingFilesAndStubs( + files: File[], + parentStub: StirlingFileStub, + toolId: ToolId +): Promise<{ stirlingFiles: StirlingFile[], stubs: StirlingFileStub[] }> { + const stirlingFiles: StirlingFile[] = []; + const stubs: StirlingFileStub[] = []; + + for (const file of files) { + const processedFileMetadata = await generateProcessedFileMetadata(file); + const childStub = createChildStub( + parentStub, + { toolId, timestamp: Date.now() }, + file, + processedFileMetadata?.thumbnailUrl, + processedFileMetadata + ); + + const stirlingFile = createStirlingFile(file, childStub.id); + stirlingFiles.push(stirlingFile); + stubs.push(childStub); + } + + return { stirlingFiles, stubs }; +} diff --git a/frontend/src/services/pdfExportHelpers.ts b/frontend/src/services/pdfExportHelpers.ts new file mode 100644 index 000000000..fa6d4775f --- /dev/null +++ b/frontend/src/services/pdfExportHelpers.ts @@ -0,0 +1,46 @@ +import { PDFDocument } from '../types/pageEditor'; +import { pdfExportService } from './pdfExportService'; +import { FileId } from '../types/file'; + +/** + * Export processed documents to File objects + * Handles both single documents and split documents (multiple PDFs) + */ +export async function exportProcessedDocumentsToFiles( + processedDocuments: PDFDocument | PDFDocument[], + sourceFiles: Map | null, + exportFilename: string +): Promise { + console.log('exportProcessedDocumentsToFiles called with:', { + isArray: Array.isArray(processedDocuments), + numDocs: Array.isArray(processedDocuments) ? processedDocuments.length : 1, + hasSourceFiles: sourceFiles !== null, + sourceFilesSize: sourceFiles?.size + }); + + if (Array.isArray(processedDocuments)) { + // Multiple documents (splits) + const files: File[] = []; + const baseName = exportFilename.replace(/\.pdf$/i, ''); + + for (let i = 0; i < processedDocuments.length; i++) { + const doc = processedDocuments[i]; + const partFilename = `${baseName}_part_${i + 1}.pdf`; + + const result = sourceFiles + ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { selectedOnly: false, filename: partFilename }) + : await pdfExportService.exportPDF(doc, [], { selectedOnly: false, filename: partFilename }); + + files.push(new File([result.blob], result.filename, { type: 'application/pdf' })); + } + + return files; + } else { + // Single document + const result = sourceFiles + ? await pdfExportService.exportPDFMultiFile(processedDocuments, sourceFiles, [], { selectedOnly: false, filename: exportFilename }) + : await pdfExportService.exportPDF(processedDocuments, [], { selectedOnly: false, filename: exportFilename }); + + return [new File([result.blob], result.filename, { type: 'application/pdf' })]; + } +}