import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react'; import { WorkbenchType, getDefaultWorkbench } from '@app/types/workbench'; import { ToolId, isValidToolId } from '@app/types/toolId'; import { useToolRegistry } from '@app/contexts/ToolRegistryContext'; /** * NavigationContext - Complete navigation management system * * Handles navigation modes, navigation guards for unsaved changes, * and breadcrumb/history navigation. Separated from FileContext to * maintain clear separation of concerns. */ // Navigation state interface NavigationContextState { workbench: WorkbenchType; selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; } // Navigation actions type NavigationAction = | { type: 'SET_WORKBENCH'; payload: { workbench: WorkbenchType } } | { type: 'SET_SELECTED_TOOL'; payload: { toolId: ToolId | null } } | { type: 'SET_TOOL_AND_WORKBENCH'; payload: { toolId: ToolId | null; workbench: WorkbenchType } } | { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } } | { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } } | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }; // Navigation reducer const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => { switch (action.type) { case 'SET_WORKBENCH': return { ...state, workbench: action.payload.workbench }; case 'SET_SELECTED_TOOL': return { ...state, selectedTool: action.payload.toolId }; case 'SET_TOOL_AND_WORKBENCH': return { ...state, selectedTool: action.payload.toolId, workbench: action.payload.workbench }; case 'SET_UNSAVED_CHANGES': return { ...state, hasUnsavedChanges: action.payload.hasChanges }; case 'SET_PENDING_NAVIGATION': return { ...state, pendingNavigation: action.payload.navigationFn }; case 'SHOW_NAVIGATION_WARNING': return { ...state, showNavigationWarning: action.payload.show }; default: return state; } }; // Initial state const initialState: NavigationContextState = { workbench: getDefaultWorkbench(), selectedTool: null, hasUnsavedChanges: false, pendingNavigation: null, showNavigationWarning: false }; // Navigation context actions interface export interface NavigationContextActions { setWorkbench: (workbench: WorkbenchType) => void; setSelectedTool: (toolId: ToolId | null) => void; setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; registerUnsavedChangesChecker: (checker: () => boolean) => void; unregisterUnsavedChangesChecker: () => void; showNavigationWarning: (show: boolean) => void; requestNavigation: (navigationFn: () => void) => void; confirmNavigation: () => void; cancelNavigation: () => void; clearToolSelection: () => void; handleToolSelect: (toolId: string) => void; } // Context state values export interface NavigationContextStateValue { workbench: WorkbenchType; selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; } export interface NavigationContextActionsValue { actions: NavigationContextActions; } // Create contexts const NavigationStateContext = createContext(undefined); const NavigationActionsContext = createContext(undefined); // Provider component export const NavigationProvider: React.FC<{ children: React.ReactNode; enableUrlSync?: boolean; }> = ({ children }) => { const [state, dispatch] = useReducer(navigationReducer, initialState); const { allTools: toolRegistry } = useToolRegistry(); const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null); // Memoize individual callbacks const setWorkbench = useCallback((workbench: WorkbenchType) => { // Check for unsaved changes using registered checker or state const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges; console.log('[NavigationContext] setWorkbench:', { from: state.workbench, to: workbench, hasChecker: !!unsavedChangesCheckerRef.current, hasUnsavedChanges }); // If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation const leavingWorkbenchWithChanges = (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) || (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) || (state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges); if (leavingWorkbenchWithChanges) { // Update state to reflect unsaved changes so modal knows if (!state.hasUnsavedChanges) { dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } }); } const performWorkbenchChange = () => { // When leaving a custom workbench, clear the selected tool console.log('[NavigationContext] performWorkbenchChange executing', { from: state.workbench, to: workbench, isCustom: state.workbench.startsWith('custom:') }); if (state.workbench.startsWith('custom:')) { console.log('[NavigationContext] Clearing tool and changing workbench to:', workbench); dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench } }); } else { console.log('[NavigationContext] Just changing workbench to:', workbench); 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]); const setSelectedTool = useCallback((toolId: ToolId | null) => { dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } }); }, []); const setToolAndWorkbench = useCallback((toolId: ToolId | null, workbench: WorkbenchType) => { // Check for unsaved changes using registered checker or state const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges; // If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation const leavingWorkbenchWithChanges = (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) || (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) || (state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges); if (leavingWorkbenchWithChanges) { 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]); const setHasUnsavedChanges = useCallback((hasChanges: boolean) => { dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }); }, []); const registerUnsavedChangesChecker = useCallback((checker: () => boolean) => { unsavedChangesCheckerRef.current = checker; }, []); const unregisterUnsavedChangesChecker = useCallback(() => { unsavedChangesCheckerRef.current = null; }, []); const showNavigationWarning = useCallback((show: boolean) => { dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } }); }, []); const requestNavigation = useCallback((navigationFn: () => void) => { if (!state.hasUnsavedChanges) { navigationFn(); return; } dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); }, [state.hasUnsavedChanges]); const confirmNavigation = useCallback(() => { console.log('[NavigationContext] confirmNavigation called', { hasPendingNav: !!state.pendingNavigation, currentWorkbench: state.workbench, currentTool: state.selectedTool }); if (state.pendingNavigation) { state.pendingNavigation(); } dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); console.log('[NavigationContext] confirmNavigation completed'); }, [state.pendingNavigation, state.workbench, state.selectedTool]); const cancelNavigation = useCallback(() => { dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); }, []); const clearToolSelection = useCallback(() => { dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); }, []); const handleToolSelect = useCallback((toolId: string) => { const performToolSelect = () => { if (toolId === 'allTools') { dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); return; } if (toolId === 'read' || toolId === 'view-pdf') { dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } }); return; } // Look up the tool in the registry to get its proper workbench const tool = isValidToolId(toolId)? toolRegistry[toolId] : null; const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench(); // Validate toolId and convert to ToolId type const validToolId = isValidToolId(toolId) ? toolId : null; dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } }); }; // Check for unsaved changes using registered checker or state const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges; // If switching away from current tool and have unsaved changes, show warning if (hasUnsavedChanges && state.selectedTool && state.selectedTool !== toolId) { dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performToolSelect } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); } else { performToolSelect(); } }, [toolRegistry, state.hasUnsavedChanges, state.selectedTool]); // Memoize the actions object to prevent unnecessary context updates // This is critical to avoid infinite loops when effects depend on actions const actions: NavigationContextActions = useMemo(() => ({ setWorkbench, setSelectedTool, setToolAndWorkbench, setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, showNavigationWarning, requestNavigation, confirmNavigation, cancelNavigation, clearToolSelection, handleToolSelect, }), [ setWorkbench, setSelectedTool, setToolAndWorkbench, setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, showNavigationWarning, requestNavigation, confirmNavigation, cancelNavigation, clearToolSelection, handleToolSelect, ]); const stateValue: NavigationContextStateValue = { workbench: state.workbench, selectedTool: state.selectedTool, hasUnsavedChanges: state.hasUnsavedChanges, pendingNavigation: state.pendingNavigation, showNavigationWarning: state.showNavigationWarning }; // Also memoize the context value to prevent unnecessary re-renders const actionsValue: NavigationContextActionsValue = useMemo(() => ({ actions }), [actions]); return ( {children} ); }; // Navigation hooks export const useNavigationState = () => { const context = useContext(NavigationStateContext); if (context === undefined) { throw new Error('useNavigationState must be used within NavigationProvider'); } return context; }; export const useNavigationActions = () => { const context = useContext(NavigationActionsContext); if (context === undefined) { throw new Error('useNavigationActions must be used within NavigationProvider'); } return context; }; // Combined hook for convenience export const useNavigation = () => { const state = useNavigationState(); const { actions } = useNavigationActions(); return { ...state, ...actions }; }; // Navigation guard hook (equivalent to old useFileNavigation) export const useNavigationGuard = () => { const state = useNavigationState(); const { actions } = useNavigationActions(); return { pendingNavigation: state.pendingNavigation, showNavigationWarning: state.showNavigationWarning, hasUnsavedChanges: state.hasUnsavedChanges, requestNavigation: actions.requestNavigation, confirmNavigation: actions.confirmNavigation, cancelNavigation: actions.cancelNavigation, setHasUnsavedChanges: actions.setHasUnsavedChanges, setShowNavigationWarning: actions.showNavigationWarning, registerUnsavedChangesChecker: actions.registerUnsavedChangesChecker, unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker }; };