Circular dependencies

This commit is contained in:
Connor Yoh 2025-08-19 16:50:55 +01:00
parent 03df9a260b
commit a85ab4a832
12 changed files with 168 additions and 49 deletions

View File

@ -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() {
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<FilesModalProvider>
<FileSelectionProvider>
<ToolWorkflowProvider>
<SidebarProvider>
<HomePage />
</SidebarProvider>
</ToolWorkflowProvider>
<ToolNavigationProvider>
<ToolWorkflowProvider>
<SidebarProvider>
<HomePage />
</SidebarProvider>
</ToolWorkflowProvider>
</ToolNavigationProvider>
</FileSelectionProvider>
</FilesModalProvider>
</FileContextProvider>

View File

@ -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<string, ToolRegistryEntry>; // 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<AutomationTool[]>([]);
@ -157,6 +156,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
<ToolSelector
onSelect={addTool}
excludeTools={['automate']}
toolRegistry={toolRegistry}
/>
{/* Selected Tools */}

View File

@ -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<any>({});
const [isValid, setIsValid] = useState(true);

View File

@ -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<string, ToolRegistryEntry>; // 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(() => {

View File

@ -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 (
<Stack gap="md">

View File

@ -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;

View File

@ -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<ToolNavigationContextValue | undefined>(undefined);
// Provider component
interface ToolNavigationProviderProps {
children: React.ReactNode;
}
export function ToolNavigationProvider({ children }: ToolNavigationProviderProps) {
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(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 (
<ToolNavigationContext.Provider value={contextValue}>
{children}
</ToolNavigationContext.Provider>
);
}
// 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;
}

View File

@ -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 {
@ -99,13 +100,14 @@ const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(
// 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 (
<ToolWorkflowContext.Provider value={contextValue}>
@ -221,6 +223,40 @@ 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;

View File

@ -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: <span className="material-symbols-rounded">automation</span>,
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,

View File

@ -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<AddWatermarkParameters>(defaultWatermarkParameters);

View File

@ -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<SuggestedTool, 'navigate'>[] = [
}
];
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]);
}

View File

@ -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<any>({});
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}
/>
);