mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Implement navigation warning modal and unsaved changes management
This commit is contained in:
parent
9b63bffb36
commit
33420c7e80
@ -6,7 +6,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileContext, useCurrentFile } from "../../contexts/FileContext";
|
||||
import { ViewType } from "../../types/fileContext";
|
||||
import { ViewType, ToolType } from "../../types/fileContext";
|
||||
import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
||||
@ -26,6 +26,7 @@ import PageThumbnail from './PageThumbnail';
|
||||
import BulkSelectionPanel from './BulkSelectionPanel';
|
||||
import DragDropGrid from './DragDropGrid';
|
||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
||||
|
||||
export interface PageEditorProps {
|
||||
// Optional callbacks to expose internal functions for PageEditorControls
|
||||
@ -63,7 +64,8 @@ const PageEditor = ({
|
||||
selectedPageNumbers,
|
||||
setSelectedPages,
|
||||
updateProcessedFile,
|
||||
setCurrentView: originalSetCurrentView,
|
||||
setHasUnsavedChanges,
|
||||
hasUnsavedChanges,
|
||||
isProcessing: globalProcessing,
|
||||
processingProgress,
|
||||
clearAllFiles
|
||||
@ -71,24 +73,11 @@ const PageEditor = ({
|
||||
|
||||
// Edit state management
|
||||
const [editedDocument, setEditedDocument] = useState<PDFDocument | null>(null);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
|
||||
const [hasUnsavedDraft, setHasUnsavedDraft] = useState(false);
|
||||
const [showResumeModal, setShowResumeModal] = useState(false);
|
||||
const [foundDraft, setFoundDraft] = useState<any>(null);
|
||||
const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null);
|
||||
const autoSaveTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Override setCurrentView to check for unsaved changes
|
||||
const setCurrentView = useCallback((view: ViewType) => {
|
||||
if (hasUnsavedChanges && view !== 'pageEditor') {
|
||||
// Show warning modal instead of immediately switching views
|
||||
setPendingNavigation(() => () => originalSetCurrentView(view));
|
||||
setShowUnsavedModal(true);
|
||||
} else {
|
||||
originalSetCurrentView(view);
|
||||
}
|
||||
}, [hasUnsavedChanges, originalSetCurrentView]);
|
||||
|
||||
// Simple computed document from processed files (no caching needed)
|
||||
const mergedPdfDocument = useMemo(() => {
|
||||
if (activeFiles.length === 0) return null;
|
||||
@ -152,14 +141,6 @@ const PageEditor = ({
|
||||
|
||||
const [filename, setFilename] = useState<string>("");
|
||||
|
||||
// Debug render performance
|
||||
const renderStartTime = useRef(performance.now());
|
||||
|
||||
useEffect(() => {
|
||||
const renderTime = performance.now() - renderStartTime.current;
|
||||
console.log('PageEditor: Component render:', renderTime.toFixed(2) + 'ms');
|
||||
renderStartTime.current = performance.now();
|
||||
});
|
||||
|
||||
// Page editor state (use context for selectedPages)
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
@ -546,19 +527,23 @@ const PageEditor = ({
|
||||
|
||||
// Update local edit state for immediate visual feedback
|
||||
setEditedDocument(updatedDoc);
|
||||
setHasUnsavedChanges(true);
|
||||
setHasUnsavedChanges(true); // Use global state
|
||||
setHasUnsavedDraft(true); // Mark that we have unsaved draft changes
|
||||
|
||||
// Auto-save to drafts (debounced)
|
||||
// Auto-save to drafts (debounced) - only if we have new changes
|
||||
if (autoSaveTimer.current) {
|
||||
clearTimeout(autoSaveTimer.current);
|
||||
}
|
||||
|
||||
autoSaveTimer.current = setTimeout(() => {
|
||||
if (hasUnsavedDraft) {
|
||||
saveDraftToIndexedDB(updatedDoc);
|
||||
}, 2000); // Auto-save after 2 seconds of inactivity
|
||||
setHasUnsavedDraft(false); // Mark draft as saved
|
||||
}
|
||||
}, 30000); // Auto-save after 30 seconds of inactivity
|
||||
|
||||
return updatedDoc;
|
||||
}, []);
|
||||
}, [setHasUnsavedChanges, hasUnsavedDraft]);
|
||||
|
||||
// Save draft to separate IndexedDB location
|
||||
const saveDraftToIndexedDB = useCallback(async (doc: PDFDocument) => {
|
||||
@ -591,27 +576,36 @@ const PageEditor = ({
|
||||
}
|
||||
}, [activeFiles]);
|
||||
|
||||
// Clean up draft from IndexedDB
|
||||
const cleanupDraft = useCallback(async () => {
|
||||
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');
|
||||
const store = transaction.objectStore('drafts');
|
||||
store.delete(draftKey);
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup draft:', error);
|
||||
}
|
||||
}, [mergedPdfDocument]);
|
||||
|
||||
// Apply changes to create new processed file
|
||||
const applyChanges = useCallback(async () => {
|
||||
if (!editedDocument || !mergedPdfDocument) return;
|
||||
|
||||
console.log('Applying changes - creating new processed file');
|
||||
|
||||
// Create new filename with (edited) suffix
|
||||
const originalName = mergedPdfDocument.name.replace(/\.pdf$/i, '');
|
||||
const newName = `${originalName}(edited).pdf`;
|
||||
|
||||
try {
|
||||
// Convert edited document back to processedFiles format
|
||||
if (activeFiles.length === 1) {
|
||||
// Single file - update the existing processed file
|
||||
const file = activeFiles[0];
|
||||
const currentProcessedFile = processedFiles.get(file);
|
||||
|
||||
if (currentProcessedFile) {
|
||||
const updatedProcessedFile = {
|
||||
...currentProcessedFile,
|
||||
id: `${currentProcessedFile.id}-edited`,
|
||||
id: `${currentProcessedFile.id}-edited-${Date.now()}`,
|
||||
pages: editedDocument.pages.map(page => ({
|
||||
...page,
|
||||
rotation: page.rotation || 0,
|
||||
@ -621,28 +615,27 @@ const PageEditor = ({
|
||||
lastModified: Date.now()
|
||||
};
|
||||
|
||||
// Use the proper FileContext action to update
|
||||
updateProcessedFile(file, updatedProcessedFile);
|
||||
|
||||
// Also save the updated file to IndexedDB for persistence
|
||||
await fileStorage.storeProcessedFile(file, updatedProcessedFile);
|
||||
}
|
||||
} else if (activeFiles.length > 1) {
|
||||
setStatus('Apply changes for multiple files not yet supported');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear edit state
|
||||
// Wait for the processed file update to complete before clearing edit state
|
||||
setTimeout(() => {
|
||||
setEditedDocument(null);
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Clean up auto-save draft
|
||||
setHasUnsavedDraft(false);
|
||||
cleanupDraft();
|
||||
|
||||
setStatus('Changes applied successfully');
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes:', error);
|
||||
setStatus('Failed to apply changes');
|
||||
}
|
||||
}, [editedDocument, mergedPdfDocument, processedFiles, activeFiles, updateProcessedFile]);
|
||||
}, [editedDocument, mergedPdfDocument, processedFiles, activeFiles, updateProcessedFile, setHasUnsavedChanges, setStatus, cleanupDraft]);
|
||||
|
||||
const animateReorder = useCallback((pageNumber: number, targetIndex: number) => {
|
||||
if (!displayDocument || isAnimating) return;
|
||||
@ -947,18 +940,12 @@ const PageEditor = ({
|
||||
}, [redo]);
|
||||
|
||||
const closePdf = useCallback(() => {
|
||||
if (hasUnsavedChanges) {
|
||||
// Show warning modal instead of immediately closing
|
||||
setPendingNavigation(() => () => {
|
||||
// Use global navigation guard system
|
||||
fileContext.requestNavigation(() => {
|
||||
clearAllFiles(); // This now handles all cleanup centrally (including merged docs)
|
||||
setSelectedPages([]);
|
||||
});
|
||||
setShowUnsavedModal(true);
|
||||
} else {
|
||||
clearAllFiles(); // This now handles all cleanup centrally (including merged docs)
|
||||
setSelectedPages([]);
|
||||
}
|
||||
}, [hasUnsavedChanges, clearAllFiles, setSelectedPages]);
|
||||
}, [fileContext, clearAllFiles, setSelectedPages]);
|
||||
|
||||
// PageEditorControls needs onExportSelected and onExportAll
|
||||
const onExportSelected = useCallback(() => showExportPreview(true), [showExportPreview]);
|
||||
@ -1005,64 +992,19 @@ const PageEditor = ({
|
||||
// Show loading or empty state instead of blocking
|
||||
const showLoading = !mergedPdfDocument && (globalProcessing || activeFiles.length > 0);
|
||||
const showEmpty = !mergedPdfDocument && !globalProcessing && activeFiles.length === 0;
|
||||
|
||||
// Clean up draft from IndexedDB
|
||||
const cleanupDraft = useCallback(async () => {
|
||||
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');
|
||||
const store = transaction.objectStore('drafts');
|
||||
store.delete(draftKey);
|
||||
console.log('Draft cleaned up from IndexedDB');
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup draft:', error);
|
||||
}
|
||||
}, [mergedPdfDocument]);
|
||||
|
||||
// Export and continue
|
||||
const exportAndContinue = useCallback(async () => {
|
||||
if (!editedDocument) return;
|
||||
|
||||
// First apply changes
|
||||
// Functions for global NavigationWarningModal
|
||||
const handleApplyAndContinue = useCallback(async () => {
|
||||
if (editedDocument) {
|
||||
await applyChanges();
|
||||
}
|
||||
}, [editedDocument, applyChanges]);
|
||||
|
||||
// Then export
|
||||
const handleExportAndContinue = useCallback(async () => {
|
||||
if (editedDocument) {
|
||||
await applyChanges();
|
||||
await handleExport(false);
|
||||
|
||||
// Continue with navigation if pending
|
||||
if (pendingNavigation) {
|
||||
pendingNavigation();
|
||||
setPendingNavigation(null);
|
||||
}
|
||||
|
||||
setShowUnsavedModal(false);
|
||||
}, [editedDocument, applyChanges, handleExport, pendingNavigation]);
|
||||
|
||||
// Discard changes
|
||||
const discardChanges = useCallback(() => {
|
||||
setEditedDocument(null);
|
||||
setHasUnsavedChanges(false);
|
||||
cleanupDraft();
|
||||
|
||||
if (pendingNavigation) {
|
||||
pendingNavigation();
|
||||
setPendingNavigation(null);
|
||||
}
|
||||
|
||||
setShowUnsavedModal(false);
|
||||
setStatus('Changes discarded');
|
||||
}, [cleanupDraft, pendingNavigation]);
|
||||
|
||||
// Keep working (stay on page editor)
|
||||
const keepWorking = useCallback(() => {
|
||||
setShowUnsavedModal(false);
|
||||
setPendingNavigation(null);
|
||||
}, []);
|
||||
}, [editedDocument, applyChanges, handleExport]);
|
||||
|
||||
// Check for existing drafts
|
||||
const checkForDrafts = useCallback(async () => {
|
||||
@ -1145,6 +1087,24 @@ const PageEditor = ({
|
||||
}
|
||||
}, [mergedPdfDocument, editedDocument, hasUnsavedChanges, checkForDrafts]);
|
||||
|
||||
// Global navigation intercept - listen for navigation events
|
||||
useEffect(() => {
|
||||
if (!hasUnsavedChanges) return;
|
||||
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
|
||||
return 'You have unsaved changes. Are you sure you want to leave?';
|
||||
};
|
||||
|
||||
// Intercept browser navigation
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
// Display all pages - use edited or original document
|
||||
const displayedPages = displayDocument?.pages || [];
|
||||
|
||||
@ -1393,61 +1353,11 @@ const PageEditor = ({
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Unsaved Changes Modal */}
|
||||
<Modal
|
||||
opened={showUnsavedModal}
|
||||
onClose={keepWorking}
|
||||
title="Unsaved Changes"
|
||||
centered
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
You have unsaved changes to your PDF. What would you like to do?
|
||||
</Text>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={keepWorking}
|
||||
>
|
||||
Keep Working
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={discardChanges}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={async () => {
|
||||
await applyChanges();
|
||||
if (pendingNavigation) {
|
||||
pendingNavigation();
|
||||
setPendingNavigation(null);
|
||||
}
|
||||
setShowUnsavedModal(false);
|
||||
}}
|
||||
>
|
||||
Apply & Continue
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={exportAndContinue}
|
||||
>
|
||||
Export & Continue
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
{/* Global Navigation Warning Modal */}
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={handleApplyAndContinue}
|
||||
onExportAndContinue={handleExportAndContinue}
|
||||
/>
|
||||
|
||||
{/* Resume Work Modal */}
|
||||
<Modal
|
||||
|
@ -98,11 +98,13 @@ const FileGrid = ({
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* File Count Badge */}3
|
||||
gap: 'md'
|
||||
}
|
||||
}}
|
||||
h="30rem" style={{ overflowY: "auto", width: "100%" }}
|
||||
{/* File Grid */}
|
||||
<Flex
|
||||
direction="row"
|
||||
wrap="wrap"
|
||||
gap="md"
|
||||
h="30rem"
|
||||
style={{ overflowY: "auto", width: "100%" }}
|
||||
>
|
||||
{displayFiles.map((file, idx) => {
|
||||
const originalIdx = files.findIndex(f => (f.id || f.name) === (file.id || file.name));
|
||||
|
106
frontend/src/components/shared/NavigationWarningModal.tsx
Normal file
106
frontend/src/components/shared/NavigationWarningModal.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||
import { useFileContext } from '../../contexts/FileContext';
|
||||
|
||||
interface NavigationWarningModalProps {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
onExportAndContinue?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NavigationWarningModal = ({
|
||||
onApplyAndContinue,
|
||||
onExportAndContinue
|
||||
}: NavigationWarningModalProps) => {
|
||||
const {
|
||||
showNavigationWarning,
|
||||
hasUnsavedChanges,
|
||||
confirmNavigation,
|
||||
cancelNavigation,
|
||||
setHasUnsavedChanges
|
||||
} = useFileContext();
|
||||
|
||||
const handleKeepWorking = () => {
|
||||
cancelNavigation();
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleApplyAndContinue = async () => {
|
||||
if (onApplyAndContinue) {
|
||||
await onApplyAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleExportAndContinue = async () => {
|
||||
if (onExportAndContinue) {
|
||||
await onExportAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
if (!hasUnsavedChanges) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={showNavigationWarning}
|
||||
onClose={handleKeepWorking}
|
||||
title="Unsaved Changes"
|
||||
centered
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
You have unsaved changes to your PDF. What would you like to do?
|
||||
</Text>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={handleKeepWorking}
|
||||
>
|
||||
Keep Working
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={handleDiscardChanges}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
|
||||
{onApplyAndContinue && (
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={handleApplyAndContinue}
|
||||
>
|
||||
Apply & Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onExportAndContinue && (
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleExportAndContinue}
|
||||
>
|
||||
Export & Continue
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationWarningModal;
|
@ -42,7 +42,10 @@ const initialState: FileContextState = {
|
||||
viewerConfig: initialViewerConfig,
|
||||
isProcessing: false,
|
||||
processingProgress: 0,
|
||||
lastExportConfig: undefined
|
||||
lastExportConfig: undefined,
|
||||
hasUnsavedChanges: false,
|
||||
pendingNavigation: null,
|
||||
showNavigationWarning: false
|
||||
};
|
||||
|
||||
// Action types
|
||||
@ -62,6 +65,9 @@ type FileContextAction =
|
||||
| { type: 'ADD_PAGE_OPERATIONS'; payload: { fileId: string; operations: PageOperation[] } }
|
||||
| { type: 'ADD_FILE_OPERATION'; payload: FileOperation }
|
||||
| { type: 'SET_EXPORT_CONFIG'; payload: FileContextState['lastExportConfig'] }
|
||||
| { type: 'SET_UNSAVED_CHANGES'; payload: boolean }
|
||||
| { type: 'SET_PENDING_NAVIGATION'; payload: (() => void) | null }
|
||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: boolean }
|
||||
| { type: 'RESET_CONTEXT' }
|
||||
| { type: 'LOAD_STATE'; payload: Partial<FileContextState> };
|
||||
|
||||
@ -182,6 +188,24 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
||||
lastExportConfig: action.payload
|
||||
};
|
||||
|
||||
case 'SET_UNSAVED_CHANGES':
|
||||
return {
|
||||
...state,
|
||||
hasUnsavedChanges: action.payload
|
||||
};
|
||||
|
||||
case 'SET_PENDING_NAVIGATION':
|
||||
return {
|
||||
...state,
|
||||
pendingNavigation: action.payload
|
||||
};
|
||||
|
||||
case 'SHOW_NAVIGATION_WARNING':
|
||||
return {
|
||||
...state,
|
||||
showNavigationWarning: action.payload
|
||||
};
|
||||
|
||||
case 'RESET_CONTEXT':
|
||||
return {
|
||||
...initialState
|
||||
@ -471,29 +495,54 @@ export function FileContextProvider({
|
||||
dispatch({ type: 'CLEAR_SELECTIONS' });
|
||||
}, [cleanupAllFiles]);
|
||||
|
||||
// Navigation guard system functions
|
||||
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: hasChanges });
|
||||
}, []);
|
||||
|
||||
const requestNavigation = useCallback((navigationFn: () => void): boolean => {
|
||||
if (state.hasUnsavedChanges) {
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: navigationFn });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: true });
|
||||
return false;
|
||||
} else {
|
||||
navigationFn();
|
||||
return true;
|
||||
}
|
||||
}, [state.hasUnsavedChanges]);
|
||||
|
||||
const confirmNavigation = useCallback(() => {
|
||||
if (state.pendingNavigation) {
|
||||
state.pendingNavigation();
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: null });
|
||||
}
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: false });
|
||||
}, [state.pendingNavigation]);
|
||||
|
||||
const cancelNavigation = useCallback(() => {
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: null });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: false });
|
||||
}, []);
|
||||
|
||||
const setCurrentView = useCallback((view: ViewType) => {
|
||||
// Update view immediately for instant UI response
|
||||
requestNavigation(() => {
|
||||
dispatch({ type: 'SET_CURRENT_VIEW', payload: view });
|
||||
|
||||
// REMOVED: Aggressive cleanup on view switch
|
||||
// This was destroying cached processed files and causing re-processing
|
||||
// We should only cleanup when files are actually removed or app closes
|
||||
|
||||
// Optional: Light memory pressure relief only for very large docs
|
||||
if (state.currentView !== view && state.activeFiles.length > 0) {
|
||||
// Only hint at garbage collection, don't destroy caches
|
||||
if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) {
|
||||
window.requestIdleCallback(() => {
|
||||
// Very light cleanup - just GC hint, no cache destruction
|
||||
window.gc();
|
||||
}, { timeout: 5000 });
|
||||
}
|
||||
}
|
||||
}, [state.currentView, state.activeFiles]);
|
||||
});
|
||||
}, [requestNavigation, state.currentView, state.activeFiles]);
|
||||
|
||||
const setCurrentTool = useCallback((tool: ToolType) => {
|
||||
requestNavigation(() => {
|
||||
dispatch({ type: 'SET_CURRENT_TOOL', payload: tool });
|
||||
}, []);
|
||||
});
|
||||
}, [requestNavigation]);
|
||||
|
||||
const setSelectedFiles = useCallback((fileIds: string[]) => {
|
||||
dispatch({ type: 'SET_SELECTED_FILES', payload: fileIds });
|
||||
@ -646,6 +695,12 @@ export function FileContextProvider({
|
||||
loadContext,
|
||||
resetContext,
|
||||
|
||||
// Navigation guard system
|
||||
setHasUnsavedChanges,
|
||||
requestNavigation,
|
||||
confirmNavigation,
|
||||
cancelNavigation,
|
||||
|
||||
// Memory management
|
||||
trackBlobUrl,
|
||||
trackPdfDocument,
|
||||
|
@ -58,6 +58,11 @@ export interface FileContextState {
|
||||
selectedOnly: boolean;
|
||||
splitDocuments: boolean;
|
||||
};
|
||||
|
||||
// Navigation guard system
|
||||
hasUnsavedChanges: boolean;
|
||||
pendingNavigation: (() => void) | null;
|
||||
showNavigationWarning: boolean;
|
||||
}
|
||||
|
||||
export interface FileContextActions {
|
||||
@ -100,6 +105,12 @@ export interface FileContextActions {
|
||||
loadContext: () => Promise<void>;
|
||||
resetContext: () => void;
|
||||
|
||||
// Navigation guard system
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
requestNavigation: (navigationFn: () => void) => boolean;
|
||||
confirmNavigation: () => void;
|
||||
cancelNavigation: () => void;
|
||||
|
||||
// Memory management
|
||||
trackBlobUrl: (url: string) => void;
|
||||
trackPdfDocument: (fileId: string, pdfDoc: any) => void;
|
||||
|
Loading…
Reference in New Issue
Block a user