From a85ab4a832da1c7e78899b7aa2aa9a3dbf072dfe Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 19 Aug 2025 16:50:55 +0100 Subject: [PATCH] Circular dependencies --- frontend/src/App.tsx | 13 +-- .../tools/automate/AutomationCreation.tsx | 8 +- .../tools/automate/ToolConfigurationModal.tsx | 3 - .../tools/automate/ToolSelector.tsx | 5 +- .../tools/shared/SuggestedToolsSection.tsx | 5 +- .../src/constants/addWatermarkConstants.ts | 14 ++- .../src/contexts/ToolNavigationContext.tsx | 85 +++++++++++++++++++ frontend/src/contexts/ToolWorkflowContext.tsx | 54 ++++++++++-- .../src/data/useTranslatedToolRegistry.tsx | 3 +- .../addWatermark/useAddWatermarkParameters.ts | 17 +--- frontend/src/hooks/useSuggestedTools.ts | 7 +- frontend/src/tools/Automate.tsx | 3 + 12 files changed, 168 insertions(+), 49 deletions(-) create mode 100644 frontend/src/contexts/ToolNavigationContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 429b03308..e1f264c66 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { FileContextProvider } from "./contexts/FileContext"; import { FilesModalProvider } from "./contexts/FilesModalContext"; import { FileSelectionProvider } from "./contexts/FileSelectionContext"; import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; +import { ToolNavigationProvider } from "./contexts/ToolNavigationContext"; import { SidebarProvider } from "./contexts/SidebarContext"; import ErrorBoundary from "./components/shared/ErrorBoundary"; import HomePage from "./pages/HomePage"; @@ -36,11 +37,13 @@ export default function App() { - - - - - + + + + + + + diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 8a51bc224..c72c66036 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -12,10 +12,9 @@ import { } from '@mantine/core'; import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from '@mui/icons-material/Settings'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; -import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import ToolConfigurationModal from './ToolConfigurationModal'; import ToolSelector from './ToolSelector'; @@ -24,6 +23,7 @@ interface AutomationCreationProps { existingAutomation?: any; onBack: () => void; onComplete: (automation: any) => void; + toolRegistry: Record; // Pass registry as prop to break circular dependency } interface AutomationTool { @@ -34,9 +34,8 @@ interface AutomationTool { parameters?: any; } -export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete }: AutomationCreationProps) { +export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) { const { t } = useTranslation(); - const toolRegistry = useFlatToolRegistry(); const [automationName, setAutomationName] = useState(''); const [selectedTools, setSelectedTools] = useState([]); @@ -157,6 +156,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o {/* Selected Tools */} diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx index 38754ca19..00ddb0cd6 100644 --- a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -14,8 +14,6 @@ import SettingsIcon from '@mui/icons-material/Settings'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import WarningIcon from '@mui/icons-material/Warning'; -import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext'; - interface ToolConfigurationModalProps { opened: boolean; tool: { @@ -30,7 +28,6 @@ interface ToolConfigurationModalProps { export default function ToolConfigurationModal({ opened, tool, onSave, onCancel }: ToolConfigurationModalProps) { const { t } = useTranslation(); - const { toolRegistry } = useToolWorkflow(); const [parameters, setParameters] = useState({}); const [isValid, setIsValid] = useState(true); diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 7ba4b2e15..8d49e4649 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -5,18 +5,17 @@ import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { useToolSections } from '../../../hooks/useToolSections'; import { renderToolButtons } from '../shared/renderToolButtons'; import ToolSearch from '../toolPicker/ToolSearch'; -import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; interface ToolSelectorProps { onSelect: (toolKey: string) => void; excludeTools?: string[]; + toolRegistry: Record; // Pass registry as prop to break circular dependency } -export default function ToolSelector({ onSelect, excludeTools = [] }: ToolSelectorProps) { +export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry }: ToolSelectorProps) { const { t } = useTranslation(); const [opened, setOpened] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - const toolRegistry = useFlatToolRegistry(); // Filter out excluded tools (like 'automate' itself) const baseFilteredTools = useMemo(() => { diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index b1f91fc39..18e7a0fc2 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { Stack, Text, Divider, Card, Group } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; +import { useToolNavigation } from '../../../contexts/ToolNavigationContext'; + export interface SuggestedToolsSectionProps {} export function SuggestedToolsSection(): React.ReactElement { const { t } = useTranslation(); - const suggestedTools = useSuggestedTools(); + const { handleToolSelect, selectedToolKey } = useToolNavigation(); + const suggestedTools = useSuggestedTools(selectedToolKey, handleToolSelect); return ( diff --git a/frontend/src/constants/addWatermarkConstants.ts b/frontend/src/constants/addWatermarkConstants.ts index 65d26e5ca..1e84afb37 100644 --- a/frontend/src/constants/addWatermarkConstants.ts +++ b/frontend/src/constants/addWatermarkConstants.ts @@ -1,4 +1,16 @@ -import { AddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters"; +export interface AddWatermarkParameters { + watermarkType?: 'text' | 'image'; + watermarkText: string; + watermarkImage?: File; + fontSize: number; // Used for both text size and image size + rotation: number; + opacity: number; + widthSpacer: number; + heightSpacer: number; + alphabet: string; + customColor: string; + convertPDFToImage: boolean; +} export interface AlphabetOption { value: string; diff --git a/frontend/src/contexts/ToolNavigationContext.tsx b/frontend/src/contexts/ToolNavigationContext.tsx new file mode 100644 index 000000000..1e82b6c6f --- /dev/null +++ b/frontend/src/contexts/ToolNavigationContext.tsx @@ -0,0 +1,85 @@ +/** + * ToolNavigationContext - Handles tool selection and navigation without tool registry + * This breaks the circular dependency by not importing the tool registry + */ + +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { useFileContext } from './FileContext'; + +// Navigation state interface +interface ToolNavigationState { + selectedToolKey: string | null; +} + +// Context value interface +interface ToolNavigationContextValue extends ToolNavigationState { + // Navigation Actions + selectTool: (toolKey: string) => void; + clearToolSelection: () => void; + handleToolSelect: (toolId: string) => void; +} + +const ToolNavigationContext = createContext(undefined); + +// Provider component +interface ToolNavigationProviderProps { + children: React.ReactNode; +} + +export function ToolNavigationProvider({ children }: ToolNavigationProviderProps) { + const [selectedToolKey, setSelectedToolKey] = useState(null); + const { setCurrentView } = useFileContext(); + + const selectTool = useCallback((toolKey: string) => { + setSelectedToolKey(toolKey); + }, []); + + const clearToolSelection = useCallback(() => { + setSelectedToolKey(null); + }, []); + + const handleToolSelect = useCallback((toolId: string) => { + // Handle special cases + if (toolId === 'allTools') { + clearToolSelection(); + return; + } + + selectTool(toolId); + setCurrentView('fileEditor'); + }, [selectTool, setCurrentView, clearToolSelection]); + + const contextValue: ToolNavigationContextValue = { + selectedToolKey, + selectTool, + clearToolSelection, + handleToolSelect + }; + + return ( + + {children} + + ); +} + +// Custom hook to use the context +export function useToolNavigation(): ToolNavigationContextValue { + const context = useContext(ToolNavigationContext); + if (!context) { + // During development hot reload, temporarily return a safe fallback + if (process.env.NODE_ENV === 'development') { + console.warn('ToolNavigationContext temporarily unavailable during hot reload, using fallback'); + + return { + selectedToolKey: null, + selectTool: () => {}, + clearToolSelection: () => {}, + handleToolSelect: () => {} + } as ToolNavigationContextValue; + } + + throw new Error('useToolNavigation must be used within a ToolNavigationProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 637906a22..7fce31aae 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -7,6 +7,7 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } fr import { useToolManagement } from '../hooks/useToolManagement'; import { PageEditorFunctions } from '../types/pageEditor'; import { ToolRegistryEntry } from '../data/toolsTaxonomy'; +import { useFileContext } from './FileContext'; // State interface interface ToolWorkflowState { @@ -71,7 +72,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { selectedToolKey: string | null; selectedTool: ToolRegistryEntry | null; toolRegistry: any; // From useToolManagement - + // UI Actions setSidebarsVisible: (visible: boolean) => void; setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; @@ -99,13 +100,14 @@ const ToolWorkflowContext = createContext( // Provider component interface ToolWorkflowProviderProps { children: React.ReactNode; - /** Handler for view changes (passed from parent) */ - onViewChange?: (view: string) => void; } -export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) { +export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); + // File context for view changes + const { setCurrentView } = useFileContext(); + // Tool management hook const { selectedToolKey, @@ -152,12 +154,12 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro } selectTool(toolId); - onViewChange?.('fileEditor'); + setCurrentView('fileEditor'); setLeftPanelView('toolContent'); setReaderMode(false); // Clear search so the tool content becomes visible immediately setSearchQuery(''); - }, [selectTool, onViewChange, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]); + }, [selectTool, setCurrentView, setLeftPanelView, setReaderMode, setSearchQuery, clearToolSelection]); const handleBackToTools = useCallback(() => { setLeftPanelView('toolPicker'); @@ -183,7 +185,7 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro ); // Simple context value with basic memoization - const contextValue = useMemo((): ToolWorkflowContextValue => ({ + const contextValue : ToolWorkflowContextValue ={ // State ...state, selectedToolKey, @@ -208,7 +210,7 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro // Computed filteredTools, isPanelVisible, - }), [state, selectedToolKey, selectedTool, toolRegistry, filteredTools, isPanelVisible]); + }; return ( @@ -221,7 +223,41 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro 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 { + state: { + 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; -} \ No newline at end of file +} diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 7be8431df..4ceababe2 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -10,7 +10,6 @@ import ChangePermissions from '../tools/ChangePermissions'; import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; -import Automate from '../tools/Automate'; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -336,7 +335,7 @@ export function useFlatToolRegistry(): ToolRegistry { "automate": { icon: automation, name: t("home.automate.title", "Automate"), - component: Automate, + component: React.lazy(() => import('../tools/Automate')), view: "format", description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), category: ToolCategory.ADVANCED_TOOLS, diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts index aa2f3bdda..574a22598 100644 --- a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts +++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts @@ -1,20 +1,5 @@ import { useState, useCallback } from 'react'; -import { defaultWatermarkParameters } from '../../../constants/addWatermarkConstants'; - -export interface AddWatermarkParameters { - watermarkType?: 'text' | 'image'; - watermarkText: string; - watermarkImage?: File; - fontSize: number; // Used for both text size and image size - rotation: number; - opacity: number; - widthSpacer: number; - heightSpacer: number; - alphabet: string; - customColor: string; - convertPDFToImage: boolean; -} - +import { defaultWatermarkParameters, AddWatermarkParameters } from '../../../constants/addWatermarkConstants'; export const useAddWatermarkParameters = () => { const [parameters, setParameters] = useState(defaultWatermarkParameters); diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index ae6d77035..5f8891492 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; // Material UI Icons import CompressIcon from '@mui/icons-material/Compress'; @@ -43,9 +42,7 @@ const ALL_SUGGESTED_TOOLS: Omit[] = [ } ]; -export function useSuggestedTools(): SuggestedTool[] { - const { handleToolSelect, selectedToolKey } = useToolWorkflow(); - +export function useSuggestedTools(selectedToolKey?: string | null, handleToolSelect?: (toolId: string) => void): SuggestedTool[] { return useMemo(() => { // Filter out the current tool const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.name !== selectedToolKey); @@ -53,7 +50,7 @@ export function useSuggestedTools(): SuggestedTool[] { // Add navigation function to each tool return filteredTools.map(tool => ({ ...tool, - navigate: () => handleToolSelect(tool.name) + navigate: handleToolSelect ? () => handleToolSelect(tool.name) : () => {} })); }, [selectedToolKey, handleToolSelect]); } \ No newline at end of file diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 7d5232b50..5c826c33b 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -10,6 +10,7 @@ import ToolSequence from "../components/tools/automate/ToolSequence"; import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation"; import { BaseToolProps } from "../types/tool"; +import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); @@ -20,6 +21,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const [stepData, setStepData] = useState({}); const automateOperation = useAutomateOperation(); + const toolRegistry = useFlatToolRegistry(); const handleStepChange = (data: any) => { setStepData(data); @@ -51,6 +53,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { existingAutomation={stepData.automation} onBack={() => handleStepChange({ step: 'selection' })} onComplete={(automation: any) => handleStepChange({ step: 'sequence', automation })} + toolRegistry={toolRegistry} /> );