Pop up moving away from page editor

This commit is contained in:
Connor Yoh 2025-09-26 15:56:10 +01:00
parent b1cec32ef9
commit f70e45cee4
3 changed files with 67 additions and 31 deletions

View File

@ -1,6 +1,7 @@
import { useState, useCallback, useRef, useEffect } from "react"; import { useState, useCallback, useRef, useEffect } from "react";
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
import { useFileState, useFileActions } from "../../contexts/FileContext"; import { useFileState, useFileActions } from "../../contexts/FileContext";
import { useNavigationGuard } from "../../contexts/NavigationContext";
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor"; import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
import { pdfExportService } from "../../services/pdfExportService"; import { pdfExportService } from "../../services/pdfExportService";
import { documentManipulationService } from "../../services/documentManipulationService"; import { documentManipulationService } from "../../services/documentManipulationService";
@ -36,6 +37,9 @@ const PageEditor = ({
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();
const { actions } = useFileActions(); const { actions } = useFileActions();
// Navigation guard for unsaved changes
const { setHasUnsavedChanges } = useNavigationGuard();
// Prefer IDs + selectors to avoid array identity churn // Prefer IDs + selectors to avoid array identity churn
const activeFileIds = state.files.ids; const activeFileIds = state.files.ids;
@ -82,6 +86,12 @@ const PageEditor = ({
updateUndoRedoState(); updateUndoRedoState();
}, [updateUndoRedoState]); }, [updateUndoRedoState]);
// Wrapper for executeCommand to track unsaved changes
const executeCommandWithTracking = useCallback((command: any) => {
undoManagerRef.current.executeCommand(command);
setHasUnsavedChanges(true);
}, [setHasUnsavedChanges]);
// Watch for container size changes to update split line positions // Watch for container size changes to update split line positions
useEffect(() => { useEffect(() => {
const container = gridContainerRef.current; const container = gridContainerRef.current;
@ -138,17 +148,16 @@ const PageEditor = ({
// DOM-first command handlers // DOM-first command handlers
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => { const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation); const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
undoManagerRef.current.executeCommand(bulkRotateCommand); executeCommandWithTracking(bulkRotateCommand);
}, []); }, [executeCommandWithTracking]);
// Command factory functions for PageThumbnail // Command factory functions for PageThumbnail
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({ const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
execute: () => { execute: () => {
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation); const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
executeCommandWithTracking(bulkRotateCommand);
undoManagerRef.current.executeCommand(bulkRotateCommand);
} }
}), []); }), [executeCommandWithTracking]);
const createDeleteCommand = useCallback((pageIds: string[]) => ({ const createDeleteCommand = useCallback((pageIds: string[]) => ({
execute: () => { execute: () => {
@ -174,10 +183,10 @@ const PageEditor = ({
() => getPageNumbersFromIds(selectedPageIds), () => getPageNumbersFromIds(selectedPageIds),
closePdf closePdf
); );
undoManagerRef.current.executeCommand(deleteCommand); executeCommandWithTracking(deleteCommand);
} }
} }
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]); }), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
const createSplitCommand = useCallback((position: number) => ({ const createSplitCommand = useCallback((position: number) => ({
execute: () => { execute: () => {
@ -186,9 +195,9 @@ const PageEditor = ({
() => splitPositions, () => splitPositions,
setSplitPositions setSplitPositions
); );
undoManagerRef.current.executeCommand(splitCommand); executeCommandWithTracking(splitCommand);
} }
}), [splitPositions]); }), [splitPositions, executeCommandWithTracking]);
// Command executor for PageThumbnail // Command executor for PageThumbnail
const executeCommand = useCallback((command: any) => { const executeCommand = useCallback((command: any) => {
@ -232,8 +241,8 @@ const PageEditor = ({
() => selectedPageNumbers, () => selectedPageNumbers,
closePdf closePdf
); );
undoManagerRef.current.executeCommand(deleteCommand); executeCommandWithTracking(deleteCommand);
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]); }, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]);
const handleDeletePage = useCallback((pageNumber: number) => { const handleDeletePage = useCallback((pageNumber: number) => {
if (!displayDocument) return; if (!displayDocument) return;
@ -251,8 +260,8 @@ const PageEditor = ({
() => getPageNumbersFromIds(selectedPageIds), () => getPageNumbersFromIds(selectedPageIds),
closePdf closePdf
); );
undoManagerRef.current.executeCommand(deleteCommand); executeCommandWithTracking(deleteCommand);
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]); }, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
const handleSplit = useCallback(() => { const handleSplit = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return; if (!displayDocument || selectedPageIds.length === 0) return;
@ -298,8 +307,8 @@ const PageEditor = ({
: `Add ${selectedPositions.length - existingSplitsCount} split(s)` : `Add ${selectedPositions.length - existingSplitsCount} split(s)`
}; };
undoManagerRef.current.executeCommand(smartSplitCommand); executeCommandWithTracking(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]); }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
const handleSplitAll = useCallback(() => { const handleSplitAll = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return; if (!displayDocument || selectedPageIds.length === 0) return;
@ -344,8 +353,8 @@ const PageEditor = ({
: `Add ${selectedPositions.length - existingSplitsCount} split(s)` : `Add ${selectedPositions.length - existingSplitsCount} split(s)`
}; };
undoManagerRef.current.executeCommand(smartSplitCommand); executeCommandWithTracking(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]); }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
const handlePageBreak = useCallback(() => { const handlePageBreak = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return; if (!displayDocument || selectedPageIds.length === 0) return;
@ -358,8 +367,8 @@ const PageEditor = ({
() => displayDocument, () => displayDocument,
setEditedDocument setEditedDocument
); );
undoManagerRef.current.executeCommand(pageBreakCommand); executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]); }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
const handlePageBreakAll = useCallback(() => { const handlePageBreakAll = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return; if (!displayDocument || selectedPageIds.length === 0) return;
@ -372,8 +381,8 @@ const PageEditor = ({
() => displayDocument, () => displayDocument,
setEditedDocument setEditedDocument
); );
undoManagerRef.current.executeCommand(pageBreakCommand); executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]); }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => { const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
if (!displayDocument || files.length === 0) return; if (!displayDocument || files.length === 0) return;
@ -416,8 +425,8 @@ const PageEditor = ({
() => displayDocument, () => displayDocument,
setEditedDocument setEditedDocument
); );
undoManagerRef.current.executeCommand(reorderCommand); executeCommandWithTracking(reorderCommand);
}, [displayDocument, getPageNumbersFromIds]); }, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
// Helper function to collect source files for multi-file export // Helper function to collect source files for multi-file export
const getSourceFiles = useCallback((): Map<FileId, File> | null => { const getSourceFiles = useCallback((): Map<FileId, File> | null => {
@ -499,13 +508,14 @@ const PageEditor = ({
// Step 4: Download the result // Step 4: Download the result
pdfExportService.downloadFile(result.blob, result.filename); pdfExportService.downloadFile(result.blob, result.filename);
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
setExportLoading(false); setExportLoading(false);
} catch (error) { } catch (error) {
console.error('Export failed:', error); console.error('Export failed:', error);
setExportLoading(false); setExportLoading(false);
} }
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]); }, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
const onExportAll = useCallback(async () => { const onExportAll = useCallback(async () => {
if (!displayDocument) return; if (!displayDocument) return;
@ -552,6 +562,7 @@ const PageEditor = ({
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip'); const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
pdfExportService.downloadFile(zipBlob, zipFilename); pdfExportService.downloadFile(zipBlob, zipFilename);
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
} else { } else {
// Single document - regular export // Single document - regular export
const sourceFiles = getSourceFiles(); const sourceFiles = getSourceFiles();
@ -570,6 +581,7 @@ const PageEditor = ({
); );
pdfExportService.downloadFile(result.blob, result.filename); pdfExportService.downloadFile(result.blob, result.filename);
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
} }
setExportLoading(false); setExportLoading(false);
@ -577,7 +589,7 @@ const PageEditor = ({
console.error('Export failed:', error); console.error('Export failed:', error);
setExportLoading(false); setExportLoading(false);
} }
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]); }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
// Apply DOM changes to document state using dedicated service // Apply DOM changes to document state using dedicated service
const applyChanges = useCallback(() => { const applyChanges = useCallback(() => {
@ -779,7 +791,14 @@ const PageEditor = ({
)} )}
<NavigationWarningModal /> <NavigationWarningModal
onApplyAndContinue={async () => {
applyChanges();
}}
onExportAndContinue={async () => {
await onExportAll();
}}
/>
</Box> </Box>
); );
}; };

View File

@ -109,16 +109,34 @@ export const NavigationProvider: React.FC<{
const actions: NavigationContextActions = { const actions: NavigationContextActions = {
setWorkbench: useCallback((workbench: WorkbenchType) => { setWorkbench: useCallback((workbench: WorkbenchType) => {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); // If we're leaving pageEditor workbench and have unsaved changes, request navigation
}, []), if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
const performWorkbenchChange = () => {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
};
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
} else {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
}
}, [state.workbench, state.hasUnsavedChanges]),
setSelectedTool: useCallback((toolId: ToolId | null) => { setSelectedTool: useCallback((toolId: ToolId | null) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } }); dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
}, []), }, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => { setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); // If we're leaving pageEditor workbench and have unsaved changes, request navigation
}, []), if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
const performWorkbenchChange = () => {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
};
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
} else {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
}
}, [state.workbench, state.hasUnsavedChanges]),
setHasUnsavedChanges: useCallback((hasChanges: boolean) => { setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }); dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });

View File

@ -216,7 +216,6 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setSearchQuery(''); setSearchQuery('');
setLeftPanelView('toolContent'); setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools setReaderMode(false); // Disable read mode when selecting tools
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]); }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => { const handleBackToTools = useCallback(() => {