Files
Stirling-PDF/frontend/src/core/contexts/ToolWorkflowContext.tsx
T
a5e2b54274 Feature/v2/compare tool (#4751)
# Description of Changes

- Addition of the compare tool
- 
---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <[email protected]>
2025-11-12 14:54:01 +00:00

424 lines
14 KiB
TypeScript

/**
* ToolWorkflowContext - Manages tool selection, UI state, and workflow coordination
* Eliminates prop drilling with a single, simple context
*/
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react';
import { useToolManagement } from '@app/hooks/useToolManagement';
import { PageEditorFunctions } from '@app/types/pageEditor';
import { ToolRegistryEntry, ToolRegistry } from '@app/data/toolsTaxonomy';
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
import { ToolId, isValidToolId } from '@app/types/toolId';
import { WorkbenchType, getDefaultWorkbench, isBaseWorkbench } from '@app/types/workbench';
import { useNavigationUrlSync } from '@app/hooks/useUrlSync';
import { filterToolRegistryByQuery } from '@app/utils/toolSearch';
import { useToolHistory } from '@app/hooks/tools/useUserToolActivity';
import {
ToolWorkflowState,
createInitialState,
toolWorkflowReducer,
} from '@app/contexts/toolWorkflow/toolWorkflowState';
import type { ToolPanelMode } from '@app/constants/toolPanel';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useToolRegistry } from '@app/contexts/ToolRegistryContext';
// State interface
// Types and reducer/state moved to './toolWorkflow/state'
// Context value interface
export interface CustomWorkbenchViewRegistration {
id: string;
workbenchId: WorkbenchType;
label: string;
icon?: React.ReactNode;
component: React.ComponentType<{ data: any }>;
}
export interface CustomWorkbenchViewInstance extends CustomWorkbenchViewRegistration {
data: any;
}
interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook)
selectedToolKey: ToolId | null;
selectedTool: ToolRegistryEntry | null;
toolRegistry: Partial<ToolRegistry>;
getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null;
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
setReaderMode: (mode: boolean) => void;
setToolPanelMode: (mode: ToolPanelMode) => void;
setPreviewFile: (file: File | null) => void;
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
setSearchQuery: (query: string) => void;
selectTool: (toolId: ToolId | null) => void;
clearToolSelection: () => void;
// Tool Reset Actions
toolResetFunctions: Record<string, () => void>;
registerToolReset: (toolId: string, resetFunction: () => void) => void;
resetTool: (toolId: string) => void;
// Workflow Actions (compound actions)
handleToolSelect: (toolId: ToolId) => void;
handleBackToTools: () => void;
handleReaderToggle: () => void;
// Computed values
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
isPanelVisible: boolean;
// Tool History
favoriteTools: ToolId[];
toggleFavorite: (toolId: ToolId) => void;
isFavorite: (toolId: ToolId) => boolean;
customWorkbenchViews: CustomWorkbenchViewInstance[];
registerCustomWorkbenchView: (view: CustomWorkbenchViewRegistration) => void;
unregisterCustomWorkbenchView: (id: string) => void;
setCustomWorkbenchViewData: (id: string, data: any) => void;
clearCustomWorkbenchViewData: (id: string) => void;
}
// Ensure a single context instance across HMR to avoid provider/consumer mismatches
const __GLOBAL_CONTEXT_KEY__ = '__ToolWorkflowContext__';
const existingContext = (globalThis as any)[__GLOBAL_CONTEXT_KEY__] as React.Context<ToolWorkflowContextValue | undefined> | undefined;
const ToolWorkflowContext = existingContext ?? createContext<ToolWorkflowContextValue | undefined>(undefined);
if (!existingContext) {
(globalThis as any)[__GLOBAL_CONTEXT_KEY__] = ToolWorkflowContext;
}
// Provider component
interface ToolWorkflowProviderProps {
children: React.ReactNode;
}
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, undefined, createInitialState);
const { preferences, updatePreference } = usePreferences();
// Store reset functions for tools
const [toolResetFunctions, setToolResetFunctions] = React.useState<Record<string, () => void>>({});
const [customViewRegistry, setCustomViewRegistry] = React.useState<Record<string, CustomWorkbenchViewRegistration>>({});
const [customViewData, setCustomViewData] = React.useState<Record<string, any>>({});
// Navigation actions and state are available since we're inside NavigationProvider
const { actions } = useNavigationActions();
const navigationState = useNavigationState();
// Tool management hook
const { toolRegistry, getSelectedTool } = useToolManagement();
const { allTools } = useToolRegistry();
// Tool history hook
const {
favoriteTools,
toggleFavorite,
isFavorite,
} = useToolHistory();
// Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedTool);
// UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => {
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
}, []);
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent' | 'hidden') => {
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
}, []);
const setReaderMode = useCallback((mode: boolean) => {
if (mode) {
actions.setWorkbench('viewer');
actions.setSelectedTool('read');
}
dispatch({ type: 'SET_READER_MODE', payload: mode });
}, [actions]);
const setToolPanelMode = useCallback((mode: ToolPanelMode) => {
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode });
updatePreference('defaultToolPanelMode', mode);
}, [updatePreference]);
const setPreviewFile = useCallback((file: File | null) => {
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
if (file) {
actions.setWorkbench('viewer');
}
}, [actions]);
const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => {
dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions });
}, []);
const setSearchQuery = useCallback((query: string) => {
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
}, []);
const registerCustomWorkbenchView = useCallback((view: CustomWorkbenchViewRegistration) => {
setCustomViewRegistry(prev => ({ ...prev, [view.id]: view }));
}, []);
const unregisterCustomWorkbenchView = useCallback((id: string) => {
let removedView: CustomWorkbenchViewRegistration | undefined;
setCustomViewRegistry(prev => {
const existing = prev[id];
if (!existing) {
return prev;
}
removedView = existing;
const updated = { ...prev };
delete updated[id];
return updated;
});
setCustomViewData(prev => {
if (!(id in prev)) {
return prev;
}
const updated = { ...prev };
delete updated[id];
return updated;
});
if (removedView && navigationState.workbench === removedView.workbenchId) {
actions.setWorkbench(getDefaultWorkbench());
}
}, [actions, navigationState.workbench]);
const setCustomWorkbenchViewData = useCallback((id: string, data: any) => {
setCustomViewData(prev => ({ ...prev, [id]: data }));
}, []);
const clearCustomWorkbenchViewData = useCallback((id: string) => {
setCustomViewData(prev => {
if (!(id in prev)) {
return prev;
}
const updated = { ...prev };
delete updated[id];
return updated;
});
}, []);
const customWorkbenchViews = useMemo<CustomWorkbenchViewInstance[]>(() => {
return Object.values(customViewRegistry).map(view => ({
...view,
data: Object.prototype.hasOwnProperty.call(customViewData, view.id) ? customViewData[view.id] : null,
}));
}, [customViewRegistry, customViewData]);
useEffect(() => {
if (isBaseWorkbench(navigationState.workbench)) {
return;
}
const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === navigationState.workbench);
if (!currentCustomView || currentCustomView.data == null) {
actions.setWorkbench(getDefaultWorkbench());
}
}, [actions, customWorkbenchViews, navigationState.workbench]);
// Persisted via PreferencesContext; no direct localStorage writes needed here
// Keep tool panel mode in sync with user preference. This ensures the
// Config setting (Default tool picker mode) immediately affects the app
// and persists across reloads.
useEffect(() => {
const preferredMode = preferences.defaultToolPanelMode;
if (preferredMode !== state.toolPanelMode) {
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: preferredMode });
}
}, [preferences.defaultToolPanelMode, state.toolPanelMode]);
// Tool reset methods
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
}, []);
const resetTool = useCallback((toolId: string) => {
// Use the current state directly instead of depending on the state in the closure
setToolResetFunctions(current => {
const resetFunction = current[toolId];
if (resetFunction) {
resetFunction();
}
return current; // Return the same state to avoid unnecessary updates
});
}, []); // Empty dependency array makes this stable
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: ToolId) => {
// If we're currently on a custom workbench (e.g., Validate Signature report),
// selecting any tool should take the user back to the default file manager view.
const wasInCustomWorkbench = !isBaseWorkbench(navigationState.workbench);
// Handle read tool selection - should behave exactly like QuickAccessBar read button
if (toolId === 'read') {
setReaderMode(true);
actions.setSelectedTool('read');
actions.setWorkbench(wasInCustomWorkbench ? getDefaultWorkbench() : 'viewer');
setSearchQuery('');
return;
}
// Handle multiTool selection - enable page editor workbench
if (toolId === 'multiTool') {
setReaderMode(false);
setLeftPanelView('hidden');
actions.setSelectedTool('multiTool');
actions.setWorkbench(wasInCustomWorkbench ? getDefaultWorkbench() : 'pageEditor');
setSearchQuery('');
return;
}
// Set the selected tool and determine the appropriate workbench
const validToolId = isValidToolId(toolId) ? toolId : null;
actions.setSelectedTool(validToolId);
// Get the tool from registry to determine workbench
const tool = getSelectedTool(toolId);
if (wasInCustomWorkbench) {
actions.setWorkbench(getDefaultWorkbench());
} else if (tool && tool.workbench) {
actions.setWorkbench(tool.workbench);
} else {
actions.setWorkbench(getDefaultWorkbench());
}
// Clear search query when selecting a tool
setSearchQuery('');
setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools
}, [actions, getSelectedTool, navigationState.workbench, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
setReaderMode(false);
actions.setSelectedTool(null);
}, [setLeftPanelView, setReaderMode, actions.setSelectedTool]);
const handleReaderToggle = useCallback(() => {
setReaderMode(true);
}, [setReaderMode]);
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
const filteredTools = useMemo(() => {
if (!toolRegistry) return [];
return filterToolRegistryByQuery(toolRegistry, state.searchQuery);
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
state.sidebarsVisible && !state.readerMode && state.leftPanelView !== 'hidden',
[state.sidebarsVisible, state.readerMode, state.leftPanelView]
);
useNavigationUrlSync(
navigationState.selectedTool,
handleToolSelect,
handleBackToTools,
allTools,
true
);
// Properly memoized context value
const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State
...state,
selectedToolKey: navigationState.selectedTool,
selectedTool,
toolRegistry,
getSelectedTool,
// Actions
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
setToolPanelMode,
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
selectTool: actions.setSelectedTool,
clearToolSelection: () => actions.setSelectedTool(null),
// Tool Reset Actions
toolResetFunctions,
registerToolReset,
resetTool,
// Workflow Actions
handleToolSelect,
handleBackToTools,
handleReaderToggle,
// Computed
filteredTools,
isPanelVisible,
// Tool History
favoriteTools,
toggleFavorite,
isFavorite,
// Custom workbench views
customWorkbenchViews,
registerCustomWorkbenchView,
unregisterCustomWorkbenchView,
setCustomWorkbenchViewData,
clearCustomWorkbenchViewData,
}), [
state,
navigationState.selectedTool,
selectedTool,
toolRegistry,
getSelectedTool,
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
setToolPanelMode,
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
actions.setSelectedTool,
registerToolReset,
resetTool,
handleToolSelect,
handleBackToTools,
handleReaderToggle,
filteredTools,
isPanelVisible,
favoriteTools,
toggleFavorite,
isFavorite,
customWorkbenchViews,
registerCustomWorkbenchView,
unregisterCustomWorkbenchView,
setCustomWorkbenchViewData,
clearCustomWorkbenchViewData,
]);
return (
<ToolWorkflowContext.Provider value={contextValue}>
{children}
</ToolWorkflowContext.Provider>
);
}
// Custom hook to use the context
export function useToolWorkflow(): ToolWorkflowContextValue {
const context = useContext(ToolWorkflowContext);
if (!context) {
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
}
return context;
}