Circular dependencies with navigation fixes, types broken out

This commit is contained in:
Connor Yoh 2025-08-22 12:53:06 +01:00
parent 263efa273c
commit ea7c8ee1c7
9 changed files with 173 additions and 122 deletions

View File

@ -1,5 +1,6 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react'; import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync'; import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
/** /**
* NavigationContext - Complete navigation management system * NavigationContext - Complete navigation management system
@ -9,32 +10,13 @@ import { useNavigationUrlSync } from '../hooks/useUrlSync';
* maintain clear separation of concerns. * maintain clear separation of concerns.
*/ */
// Navigation mode types - complete list to match fileContext.ts
export type ModeType =
| 'viewer'
| 'pageEditor'
| 'fileEditor'
| 'merge'
| 'split'
| 'compress'
| 'ocr'
| 'convert'
| 'sanitize'
| 'addPassword'
| 'changePermissions'
| 'addWatermark'
| 'removePassword'
| 'single-large-page'
| 'repair'
| 'unlockPdfForms'
| 'removeCertificateSign';
// Navigation state // Navigation state
interface NavigationState { interface NavigationState {
currentMode: ModeType; currentMode: ModeType;
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null; pendingNavigation: (() => void) | null;
showNavigationWarning: boolean; showNavigationWarning: boolean;
selectedToolKey: string | null; // Add tool selection to navigation state
} }
// Navigation actions // Navigation actions
@ -42,7 +24,8 @@ type NavigationAction =
| { type: 'SET_MODE'; payload: { mode: ModeType } } | { type: 'SET_MODE'; payload: { mode: ModeType } }
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } } | { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } } | { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }; | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }
| { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } };
// Navigation reducer // Navigation reducer
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => { const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
@ -59,6 +42,9 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
case 'SHOW_NAVIGATION_WARNING': case 'SHOW_NAVIGATION_WARNING':
return { ...state, showNavigationWarning: action.payload.show }; return { ...state, showNavigationWarning: action.payload.show };
case 'SET_SELECTED_TOOL':
return { ...state, selectedToolKey: action.payload.toolKey };
default: default:
return state; return state;
} }
@ -66,10 +52,11 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
// Initial state // Initial state
const initialState: NavigationState = { const initialState: NavigationState = {
currentMode: 'pageEditor', currentMode: getDefaultMode(),
hasUnsavedChanges: false, hasUnsavedChanges: false,
pendingNavigation: null, pendingNavigation: null,
showNavigationWarning: false showNavigationWarning: false,
selectedToolKey: null
}; };
// Navigation context actions interface // Navigation context actions interface
@ -80,6 +67,9 @@ export interface NavigationContextActions {
requestNavigation: (navigationFn: () => void) => void; requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void; confirmNavigation: () => void;
cancelNavigation: () => void; cancelNavigation: () => void;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void;
handleToolSelect: (toolId: string) => void;
} }
// Split context values // Split context values
@ -88,6 +78,7 @@ export interface NavigationContextStateValue {
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null; pendingNavigation: (() => void) | null;
showNavigationWarning: boolean; showNavigationWarning: boolean;
selectedToolKey: string | null;
} }
export interface NavigationContextActionsValue { export interface NavigationContextActionsValue {
@ -145,6 +136,31 @@ export const NavigationProvider: React.FC<{
// Clear navigation without executing // Clear navigation without executing
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
}, []),
selectTool: useCallback((toolKey: string) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } });
}, []),
clearToolSelection: useCallback(() => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
}, []),
handleToolSelect: useCallback((toolId: string) => {
// Handle special cases
if (toolId === 'allTools') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
return;
}
// Special-case: if tool is a dedicated reader tool, enter reader mode
if (toolId === 'read' || toolId === 'view-pdf') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
return;
}
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } });
dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } });
}, []) }, [])
}; };
@ -152,7 +168,8 @@ export const NavigationProvider: React.FC<{
currentMode: state.currentMode, currentMode: state.currentMode,
hasUnsavedChanges: state.hasUnsavedChanges, hasUnsavedChanges: state.hasUnsavedChanges,
pendingNavigation: state.pendingNavigation, pendingNavigation: state.pendingNavigation,
showNavigationWarning: state.showNavigationWarning showNavigationWarning: state.showNavigationWarning,
selectedToolKey: state.selectedToolKey
}; };
const actionsValue: NavigationContextActionsValue = { const actionsValue: NavigationContextActionsValue = {
@ -212,16 +229,8 @@ export const useNavigationGuard = () => {
}; };
}; };
// Utility functions for mode handling // Re-export utility functions from types for backward compatibility
export const isValidMode = (mode: string): mode is ModeType => { export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation';
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
];
return validModes.includes(mode as ModeType);
};
export const getDefaultMode = (): ModeType => 'pageEditor';
// TODO: This will be expanded for URL-based routing system // TODO: This will be expanded for URL-based routing system
// - URL parsing utilities // - URL parsing utilities

View File

@ -8,7 +8,7 @@ import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor'; import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync'; import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
import { useNavigationActions } from './NavigationContext'; import { useNavigationActions, useNavigationState } from './NavigationContext';
// State interface // State interface
interface ToolWorkflowState { interface ToolWorkflowState {
@ -106,22 +106,19 @@ interface ToolWorkflowProviderProps {
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
// File context for view changes // Navigation actions and state are available since we're inside NavigationProvider
const { actions } = useNavigationActions(); const { actions } = useNavigationActions();
// Wrapper to convert string to ModeType const navigationState = useNavigationState();
const handleViewChange = (view: string) => {
actions.setMode(view as any); // ToolWorkflowContext should validate this
};
// Tool management hook // Tool management hook
const { const {
selectedToolKey,
selectedTool,
toolRegistry, toolRegistry,
selectTool, getSelectedTool,
clearToolSelection,
} = useToolManagement(); } = useToolManagement();
// Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
// UI Action creators // UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => { const setSidebarsVisible = useCallback((visible: boolean) => {
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible }); dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
@ -149,28 +146,27 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes) // Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => { const handleToolSelect = useCallback((toolId: string) => {
// Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent actions.handleToolSelect(toolId);
// Clear search query when selecting a tool
setSearchQuery('');
// Handle view switching logic
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
setLeftPanelView('toolPicker');
if (toolId === 'read' || toolId === 'view-pdf') { if (toolId === 'read' || toolId === 'view-pdf') {
setReaderMode(true); setReaderMode(true);
setLeftPanelView('toolPicker');
clearToolSelection();
setSearchQuery('');
return;
} }
} else {
selectTool(toolId);
handleViewChange('fileEditor' as any); // ToolWorkflowContext should validate this
setLeftPanelView('toolContent'); setLeftPanelView('toolContent');
setReaderMode(false); }
// Clear search so the tool content becomes visible immediately }, [actions, setLeftPanelView, setReaderMode, setSearchQuery]);
setSearchQuery('');
}, [selectTool, handleViewChange, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]);
const handleBackToTools = useCallback(() => { const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker'); setLeftPanelView('toolPicker');
setReaderMode(false); setReaderMode(false);
clearToolSelection(); actions.clearToolSelection();
}, [setLeftPanelView, setReaderMode, clearToolSelection]); }, [setLeftPanelView, setReaderMode, actions]);
const handleReaderToggle = useCallback(() => { const handleReaderToggle = useCallback(() => {
setReaderMode(true); setReaderMode(true);
@ -190,13 +186,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
); );
// Enable URL synchronization for tool selection // Enable URL synchronization for tool selection
useToolWorkflowUrlSync(selectedToolKey, selectTool, clearToolSelection, true); useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true);
// Simple context value with basic memoization // Properly memoized context value
const contextValue : ToolWorkflowContextValue ={ const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State // State
...state, ...state,
selectedToolKey, selectedToolKey: navigationState.selectedToolKey,
selectedTool, selectedTool,
toolRegistry, toolRegistry,
@ -207,8 +203,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setPreviewFile, setPreviewFile,
setPageEditorFunctions, setPageEditorFunctions,
setSearchQuery, setSearchQuery,
selectTool, selectTool: actions.selectTool,
clearToolSelection, clearToolSelection: actions.clearToolSelection,
// Workflow Actions // Workflow Actions
handleToolSelect, handleToolSelect,
@ -218,7 +214,25 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Computed // Computed
filteredTools, filteredTools,
isPanelVisible, isPanelVisible,
}; }), [
state,
navigationState.selectedToolKey,
selectedTool,
toolRegistry,
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
actions.selectTool,
actions.clearToolSelection,
handleToolSelect,
handleBackToTools,
handleReaderToggle,
filteredTools,
isPanelVisible,
]);
return ( return (
<ToolWorkflowContext.Provider value={contextValue}> <ToolWorkflowContext.Provider value={contextValue}>

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
// Material UI Icons // Material UI Icons
import CompressIcon from '@mui/icons-material/Compress'; import CompressIcon from '@mui/icons-material/Compress';
@ -44,7 +44,8 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
]; ];
export function useSuggestedTools(): SuggestedTool[] { export function useSuggestedTools(): SuggestedTool[] {
const { handleToolSelect, selectedToolKey } = useToolWorkflow(); const { actions } = useNavigationActions();
const { selectedToolKey } = useNavigationState();
return useMemo(() => { return useMemo(() => {
// Filter out the current tool // Filter out the current tool
@ -53,7 +54,7 @@ export function useSuggestedTools(): SuggestedTool[] {
// Add navigation function to each tool // Add navigation function to each tool
return filteredTools.map(tool => ({ return filteredTools.map(tool => ({
...tool, ...tool,
navigate: () => handleToolSelect(tool.name) navigate: () => actions.handleToolSelect(tool.name)
})); }));
}, [selectedToolKey, handleToolSelect]); }, [selectedToolKey, actions]);
} }

View File

@ -5,19 +5,16 @@ import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
interface ToolManagementResult { interface ToolManagementResult {
selectedToolKey: string | null;
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: string[]; toolSelectedFileIds: string[];
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: Record<string, ToolRegistryEntry>;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void;
setToolSelectedFileIds: (fileIds: string[]) => void; setToolSelectedFileIds: (fileIds: string[]) => void;
getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null;
} }
export const useToolManagement = (): ToolManagementResult => { export const useToolManagement = (): ToolManagementResult => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]); const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
// Build endpoints list from registry entries with fallback to legacy mapping // Build endpoints list from registry entries with fallback to legacy mapping
@ -56,35 +53,15 @@ export const useToolManagement = (): ToolManagementResult => {
return availableToolRegistry; return availableToolRegistry;
}, [isToolAvailable, t, baseRegistry]); }, [isToolAvailable, t, baseRegistry]);
useEffect(() => { const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => {
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) { return toolKey ? toolRegistry[toolKey] || null : null;
const firstAvailableTool = Object.keys(toolRegistry)[0]; }, [toolRegistry]);
if (firstAvailableTool) {
setSelectedToolKey(firstAvailableTool);
} else {
setSelectedToolKey(null);
}
}
}, [endpointsLoading, selectedToolKey, toolRegistry]);
const selectTool = useCallback((toolKey: string) => {
setSelectedToolKey(toolKey);
}, []);
const clearToolSelection = useCallback(() => {
setSelectedToolKey(null);
}, []);
const selectedTool = selectedToolKey ? toolRegistry[selectedToolKey] : null;
return { return {
selectedToolKey, selectedTool: getSelectedTool(null), // This will be unused, kept for compatibility
selectedTool,
toolSelectedFileIds, toolSelectedFileIds,
toolRegistry, toolRegistry,
selectTool,
clearToolSelection,
setToolSelectedFileIds, setToolSelectedFileIds,
getSelectedTool,
}; };
}; };

View File

@ -3,7 +3,7 @@
*/ */
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback } from 'react';
import { ModeType } from '../contexts/NavigationContext'; import { ModeType } from '../types/navigation';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
/** /**

View File

@ -0,0 +1,42 @@
/**
* Shared navigation types to avoid circular dependencies
*/
// Navigation mode types - complete list to match contexts
export type ModeType =
| 'viewer'
| 'pageEditor'
| 'fileEditor'
| 'merge'
| 'split'
| 'compress'
| 'ocr'
| 'convert'
| 'sanitize'
| 'addPassword'
| 'changePermissions'
| 'addWatermark'
| 'removePassword'
| 'single-large-page'
| 'repair'
| 'unlockPdfForms'
| 'removeCertificateSign';
// Utility functions for mode handling
export const isValidMode = (mode: string): mode is ModeType => {
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions',
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
'repair', 'unlockPdfForms', 'removeCertificateSign'
];
return validModes.includes(mode as ModeType);
};
export const getDefaultMode = (): ModeType => 'pageEditor';
// Route parsing result
export interface ToolRoute {
mode: ModeType;
toolKey: string | null;
}

View File

@ -0,0 +1,21 @@
/**
* Navigation action interfaces to break circular dependencies
*/
import { ModeType } from './navigation';
export interface NavigationActions {
setMode: (mode: ModeType) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void;
cancelNavigation: () => void;
}
export interface NavigationState {
currentMode: ModeType;
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;
}

View File

@ -3,12 +3,7 @@
* Provides clean URL routing for the V2 tool system * Provides clean URL routing for the V2 tool system
*/ */
import { ModeType } from '../contexts/NavigationContext'; import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation';
export interface ToolRoute {
mode: ModeType;
toolKey?: string;
}
/** /**
* Parse the current URL to extract tool routing information * Parse the current URL to extract tool routing information
@ -45,7 +40,7 @@ export function parseToolRoute(): ToolRoute {
// Check for query parameter fallback (e.g., ?tool=split) // Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool'); const toolParam = searchParams.get('tool');
if (toolParam && isValidMode(toolParam)) { if (toolParam && isValidModeType(toolParam)) {
return { return {
mode: toolParam as ModeType, mode: toolParam as ModeType,
toolKey: toolParam toolKey: toolParam
@ -54,7 +49,8 @@ export function parseToolRoute(): ToolRoute {
// Default to page editor for home page // Default to page editor for home page
return { return {
mode: 'pageEditor' mode: getDefaultMode(),
toolKey: null
}; };
} }
@ -137,16 +133,7 @@ export function getToolDisplayName(toolKey: string): string {
return displayNames[toolKey] || toolKey; return displayNames[toolKey] || toolKey;
} }
/** // Note: isValidMode is now imported from types/navigation.ts
* Check if a mode is valid
*/
function isValidMode(mode: string): mode is ModeType {
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
];
return validModes.includes(mode as ModeType);
}
/** /**
* Generate shareable URL for current tool state * Generate shareable URL for current tool state