Feature/v2/automate (#4248)

* automate feature
* Moved all providers to app level to simplify homepage 
* Circular dependency fixes
* You will see that now toolRegistry gets a tool config and a tool
settings object. These enable automate to run the tools using as much
static code as possible.

---------

Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
ConnorYoh
2025-08-22 14:40:27 +01:00
committed by GitHub
co-authored by Connor Yoh
parent 7d9c0b0298
commit 23d86deae7
84 changed files with 4784 additions and 572 deletions
+43 -34
View File
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
/**
* NavigationContext - Complete navigation management system
@@ -9,32 +10,13 @@ import { useNavigationUrlSync } from '../hooks/useUrlSync';
* 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
interface NavigationState {
currentMode: ModeType;
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;
selectedToolKey: string | null; // Add tool selection to navigation state
}
// Navigation actions
@@ -42,7 +24,8 @@ type NavigationAction =
| { type: 'SET_MODE'; payload: { mode: ModeType } }
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
| { 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
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
@@ -59,6 +42,9 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
case 'SHOW_NAVIGATION_WARNING':
return { ...state, showNavigationWarning: action.payload.show };
case 'SET_SELECTED_TOOL':
return { ...state, selectedToolKey: action.payload.toolKey };
default:
return state;
}
@@ -66,10 +52,11 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
// Initial state
const initialState: NavigationState = {
currentMode: 'pageEditor',
currentMode: getDefaultMode(),
hasUnsavedChanges: false,
pendingNavigation: null,
showNavigationWarning: false
showNavigationWarning: false,
selectedToolKey: null
};
// Navigation context actions interface
@@ -80,6 +67,9 @@ export interface NavigationContextActions {
requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void;
cancelNavigation: () => void;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void;
handleToolSelect: (toolId: string) => void;
}
// Split context values
@@ -88,6 +78,7 @@ export interface NavigationContextStateValue {
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;
selectedToolKey: string | null;
}
export interface NavigationContextActionsValue {
@@ -145,6 +136,31 @@ export const NavigationProvider: React.FC<{
// Clear navigation without executing
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
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,
hasUnsavedChanges: state.hasUnsavedChanges,
pendingNavigation: state.pendingNavigation,
showNavigationWarning: state.showNavigationWarning
showNavigationWarning: state.showNavigationWarning,
selectedToolKey: state.selectedToolKey
};
const actionsValue: NavigationContextActionsValue = {
@@ -212,16 +229,8 @@ export const useNavigationGuard = () => {
};
};
// 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'
];
return validModes.includes(mode as ModeType);
};
export const getDefaultMode = (): ModeType => 'pageEditor';
// Re-export utility functions from types for backward compatibility
export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation';
// TODO: This will be expanded for URL-based routing system
// - URL parsing utilities
+85 -32
View File
@@ -8,6 +8,7 @@ import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
import { useNavigationActions, useNavigationState } from './NavigationContext';
// State interface
interface ToolWorkflowState {
@@ -100,23 +101,23 @@ const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(
// Provider component
interface ToolWorkflowProviderProps {
children: React.ReactNode;
/** Handler for view changes (passed from parent) */
onViewChange?: (view: string) => void;
/** Enable URL synchronization for tool selection */
enableUrlSync?: boolean;
}
export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = true }: ToolWorkflowProviderProps) {
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
// Navigation actions and state are available since we're inside NavigationProvider
const { actions } = useNavigationActions();
const navigationState = useNavigationState();
// Tool management hook
const {
selectedToolKey,
selectedTool,
toolRegistry,
selectTool,
clearToolSelection,
getSelectedTool,
} = useToolManagement();
// Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
// UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => {
@@ -145,28 +146,30 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => {
// Special-case: if tool is a dedicated reader tool, enter reader mode and do not go to toolContent
if (toolId === 'read' || toolId === 'view-pdf') {
setReaderMode(true);
setLeftPanelView('toolPicker');
clearToolSelection();
setSearchQuery('');
return;
}
selectTool(toolId);
onViewChange?.('fileEditor');
setLeftPanelView('toolContent');
setReaderMode(false);
// Clear search so the tool content becomes visible immediately
actions.handleToolSelect(toolId);
// Clear search query when selecting a tool
setSearchQuery('');
}, [selectTool, onViewChange, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]);
// Handle view switching logic
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
setLeftPanelView('toolPicker');
if (toolId === 'read' || toolId === 'view-pdf') {
setReaderMode(true);
} else {
setReaderMode(false);
}
} else {
setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools
}
}, [actions, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
setReaderMode(false);
clearToolSelection();
}, [setLeftPanelView, setReaderMode, clearToolSelection]);
actions.clearToolSelection();
}, [setLeftPanelView, setReaderMode, actions]);
const handleReaderToggle = useCallback(() => {
setReaderMode(true);
@@ -186,13 +189,13 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
);
// Enable URL synchronization for tool selection
useToolWorkflowUrlSync(selectedToolKey, selectTool, clearToolSelection, enableUrlSync);
useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true);
// Simple context value with basic memoization
// Properly memoized context value
const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State
...state,
selectedToolKey,
selectedToolKey: navigationState.selectedToolKey,
selectedTool,
toolRegistry,
@@ -203,8 +206,8 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
selectTool,
clearToolSelection,
selectTool: actions.selectTool,
clearToolSelection: actions.clearToolSelection,
// Workflow Actions
handleToolSelect,
@@ -214,7 +217,25 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
// Computed
filteredTools,
isPanelVisible,
}), [state, selectedToolKey, selectedTool, toolRegistry, filteredTools, isPanelVisible]);
}), [
state,
navigationState.selectedToolKey,
selectedTool,
toolRegistry,
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
actions.selectTool,
actions.clearToolSelection,
handleToolSelect,
handleBackToTools,
handleReaderToggle,
filteredTools,
isPanelVisible,
]);
return (
<ToolWorkflowContext.Provider value={contextValue}>
@@ -227,6 +248,38 @@ export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = t
export function useToolWorkflow(): ToolWorkflowContextValue {
const context = useContext(ToolWorkflowContext);
if (!context) {
// During development hot reload, temporarily return a safe fallback
if (false && process.env.NODE_ENV === 'development') {
console.warn('ToolWorkflowContext temporarily unavailable during hot reload, using fallback');
// Return minimal safe fallback to prevent crashes
return {
sidebarsVisible: true,
leftPanelView: 'toolPicker',
readerMode: false,
previewFile: null,
pageEditorFunctions: null,
searchQuery: '',
selectedToolKey: null,
selectedTool: null,
toolRegistry: {},
filteredTools: [],
isPanelVisible: true,
setSidebarsVisible: () => {},
setLeftPanelView: () => {},
setReaderMode: () => {},
setPreviewFile: () => {},
setPageEditorFunctions: () => {},
setSearchQuery: () => {},
selectTool: () => {},
clearToolSelection: () => {},
handleToolSelect: () => {},
handleBackToTools: () => {},
handleReaderToggle: () => {}
} as ToolWorkflowContextValue;
}
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
}
return context;