From a741a338a622f1252592d73e8a2b42ae65a163da Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Thu, 7 Aug 2025 15:16:09 +0100 Subject: [PATCH] Optimisation --- frontend/src/components/layout/Workbench.tsx | 62 ++-- .../src/components/shared/QuickAccessBar.tsx | 37 +-- frontend/src/components/tools/ToolPanel.tsx | 72 ++--- frontend/src/contexts/ToolWorkflowContext.tsx | 299 ++++++++++++++++++ frontend/src/pages/HomePage.tsx | 86 ++--- 5 files changed, 372 insertions(+), 184 deletions(-) create mode 100644 frontend/src/contexts/ToolWorkflowContext.tsx diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 229da12ff..8ce2d87f2 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; -import { ToolConfiguration } from '../../types/tool'; -import { PageEditorFunctions } from '../../types/pageEditor'; +import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext'; import TopControls from '../shared/TopControls'; import FileEditor from '../fileEditor/FileEditor'; @@ -18,28 +17,8 @@ interface WorkbenchProps { activeFiles: File[]; /** Current view mode */ currentView: string; - /** Currently selected tool key */ - selectedToolKey: string | null; - /** Selected tool configuration */ - selectedTool: ToolConfiguration | null; - /** Whether sidebars are visible */ - sidebarsVisible: boolean; - /** Function to set sidebars visibility */ - setSidebarsVisible: (visible: boolean) => void; - /** File to preview */ - previewFile: File | null; - /** Function to clear preview file */ - setPreviewFile: (file: File | null) => void; - /** Page editor functions */ - pageEditorFunctions: PageEditorFunctions | null; - /** Function to set page editor functions */ - setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; /** Handler for view changes */ onViewChange: (view: string) => void; - /** Handler for tool selection */ - onToolSelect: (toolId: string) => void; - /** Handler for setting left panel view */ - onSetLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; /** Handler for adding files to active files */ onAddToActiveFiles: (file: File) => void; } @@ -47,39 +26,36 @@ interface WorkbenchProps { export default function Workbench({ activeFiles, currentView, - selectedToolKey, - selectedTool, - sidebarsVisible, - setSidebarsVisible, - previewFile, - setPreviewFile, - pageEditorFunctions, - setPageEditorFunctions, onViewChange, - onToolSelect, - onSetLeftPanelView, onAddToActiveFiles }: WorkbenchProps) { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); + + // Use context-based hooks to eliminate prop drilling + const { + previewFile, + pageEditorFunctions, + sidebarsVisible, + setPreviewFile, + setPageEditorFunctions, + setSidebarsVisible + } = useWorkbenchState(); + + const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection(); const handlePreviewClose = () => { setPreviewFile(null); const previousMode = sessionStorage.getItem('previousMode'); if (previousMode === 'split') { - onToolSelect('split'); - onViewChange('split'); - onSetLeftPanelView('toolContent'); + // Use context's handleToolSelect which coordinates tool selection and view changes + handleToolSelect('split'); sessionStorage.removeItem('previousMode'); } else if (previousMode === 'compress') { - onToolSelect('compress'); - onViewChange('compress'); - onSetLeftPanelView('toolContent'); + handleToolSelect('compress'); sessionStorage.removeItem('previousMode'); } else if (previousMode === 'convert') { - onToolSelect('convert'); - onViewChange('convert'); - onSetLeftPanelView('toolContent'); + handleToolSelect('convert'); sessionStorage.removeItem('previousMode'); } else { onViewChange('fileEditor'); @@ -124,9 +100,7 @@ export default function Workbench({ sidebarsVisible={sidebarsVisible} setSidebarsVisible={setSidebarsVisible} previewFile={previewFile} - {...(previewFile && { - onClose: handlePreviewClose - })} + onClose={handlePreviewClose} /> ); diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 2f78a0a9f..feef717ea 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -12,16 +12,10 @@ import rainbowStyles from '../../styles/rainbow.module.css'; import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import './QuickAccessBar.css'; -interface QuickAccessBarProps { - onToolsClick: () => void; - onReaderToggle: () => void; - selectedToolKey?: string; - toolRegistry: any; - leftPanelView: 'toolPicker' | 'toolContent'; - readerMode: boolean; -} +// No props needed - component uses context interface ButtonConfig { id: string; @@ -36,15 +30,12 @@ interface ButtonConfig { function NavHeader({ activeButton, - setActiveButton, - onReaderToggle, - onToolsClick + setActiveButton }: { activeButton: string; setActiveButton: (id: string) => void; - onReaderToggle: () => void; - onToolsClick: () => void; }) { + const { handleReaderToggle, handleBackToTools } = useToolWorkflow(); return ( <>
@@ -80,8 +71,8 @@ function NavHeader({ variant="subtle" onClick={() => { setActiveButton('tools'); - onReaderToggle(); - onToolsClick(); + handleReaderToggle(); + handleBackToTools(); }} style={{ backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)', @@ -104,16 +95,10 @@ function NavHeader({ ); } -const QuickAccessBar = ({ - onToolsClick, - onReaderToggle, - selectedToolKey, - toolRegistry, - leftPanelView, - readerMode, -}: QuickAccessBarProps) => { +const QuickAccessBar = () => { const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); + const { handleReaderToggle } = useToolWorkflow(); const [configModalOpen, setConfigModalOpen] = useState(false); const [activeButton, setActiveButton] = useState('tools'); const scrollableRef = useRef(null); @@ -134,7 +119,7 @@ const QuickAccessBar = ({ type: 'navigation', onClick: () => { setActiveButton('read'); - onReaderToggle(); + handleReaderToggle(); } }, { @@ -240,9 +225,7 @@ const QuickAccessBar = ({
diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index a8005f2d5..eeedd8f3f 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,52 +1,30 @@ -import React, { useState } from 'react'; -import { Button, TextInput } from '@mantine/core'; +import React from 'react'; +import { TextInput } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; -import { ToolRegistry, ToolConfiguration } from '../../types/tool'; +import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; import ToolRenderer from './ToolRenderer'; import rainbowStyles from '../../styles/rainbow.module.css'; -interface ToolPanelProps { - /** Whether the tool panel is visible */ - visible: boolean; - /** Whether reader mode is active (hides the panel) */ - readerMode: boolean; - /** Current view mode: 'toolPicker' or 'toolContent' */ - leftPanelView: 'toolPicker' | 'toolContent'; - /** Currently selected tool key */ - selectedToolKey: string | null; - /** Selected tool configuration */ - selectedTool: ToolConfiguration | null; - /** Tool registry with all available tools */ - toolRegistry: ToolRegistry; - /** Handler for tool selection */ - onToolSelect: (toolId: string) => void; - /** Handler for back to tools navigation */ - onBackToTools: () => void; - /** Handler for file preview */ - onPreviewFile?: (file: File | null) => void; -} +// No props needed - component uses context -export default function ToolPanel({ - visible, - readerMode, - leftPanelView, - selectedToolKey, - selectedTool, - toolRegistry, - onToolSelect, - onBackToTools, - onPreviewFile -}: ToolPanelProps) { +export default function ToolPanel() { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); - const [search, setSearch] = useState(""); - - // Filter tools based on search - const filteredTools = Object.entries(toolRegistry).filter(([_, { name }]) => - name.toLowerCase().includes(search.toLowerCase()) - ); + + // Use context-based hooks to eliminate prop drilling + const { + leftPanelView, + isPanelVisible, + searchQuery, + filteredTools, + setSearchQuery, + handleBackToTools + } = useToolPanelState(); + + const { selectedToolKey, handleToolSelect } = useToolSelection(); + const { setPreviewFile } = useWorkbenchState(); return (
setSearch(e.currentTarget.value)} + value={searchQuery} + onChange={(e) => setSearchQuery(e.currentTarget.value)} autoComplete="off" size="sm" /> @@ -83,7 +61,7 @@ export default function ToolPanel({
@@ -94,7 +72,7 @@ export default function ToolPanel({
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx new file mode 100644 index 000000000..b6e480a7d --- /dev/null +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -0,0 +1,299 @@ +/** + * ToolWorkflowContext - Manages tool selection, UI state, and workflow coordination + * Reduces prop drilling and improves performance through selective subscriptions + */ + +import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react'; +import { useToolManagement } from '../hooks/useToolManagement'; +import { ToolConfiguration } from '../types/tool'; +import { PageEditorFunctions } from '../types/pageEditor'; + +// State interface +interface ToolWorkflowState { + // UI State + sidebarsVisible: boolean; + leftPanelView: 'toolPicker' | 'toolContent'; + readerMode: boolean; + + // File/Preview State + previewFile: File | null; + pageEditorFunctions: PageEditorFunctions | null; + + // Search State + searchQuery: string; +} + +// Actions +type ToolWorkflowAction = + | { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean } + | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' } + | { type: 'SET_READER_MODE'; payload: boolean } + | { type: 'SET_PREVIEW_FILE'; payload: File | null } + | { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null } + | { type: 'SET_SEARCH_QUERY'; payload: string } + | { type: 'RESET_UI_STATE' }; + +// Initial state +const initialState: ToolWorkflowState = { + sidebarsVisible: true, + leftPanelView: 'toolPicker', + readerMode: false, + previewFile: null, + pageEditorFunctions: null, + searchQuery: '', +}; + +// Reducer +function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState { + switch (action.type) { + case 'SET_SIDEBARS_VISIBLE': + return { ...state, sidebarsVisible: action.payload }; + case 'SET_LEFT_PANEL_VIEW': + return { ...state, leftPanelView: action.payload }; + case 'SET_READER_MODE': + return { ...state, readerMode: action.payload }; + case 'SET_PREVIEW_FILE': + return { ...state, previewFile: action.payload }; + case 'SET_PAGE_EDITOR_FUNCTIONS': + return { ...state, pageEditorFunctions: action.payload }; + case 'SET_SEARCH_QUERY': + return { ...state, searchQuery: action.payload }; + case 'RESET_UI_STATE': + return { ...initialState, searchQuery: state.searchQuery }; // Preserve search + default: + return state; + } +} + +// Context value interface +interface ToolWorkflowContextValue extends ToolWorkflowState { + // Tool management (from hook) + selectedToolKey: string | null; + selectedTool: ToolConfiguration | null; + toolRegistry: any; // From useToolManagement + + // UI Actions + setSidebarsVisible: (visible: boolean) => void; + setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; + setReaderMode: (mode: boolean) => void; + setPreviewFile: (file: File | null) => void; + setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; + setSearchQuery: (query: string) => void; + + // Tool Actions + selectTool: (toolId: string) => void; + clearToolSelection: () => void; + + // Workflow Actions (compound actions) + handleToolSelect: (toolId: string) => void; + handleBackToTools: () => void; + handleReaderToggle: () => void; + + // Computed values + filteredTools: [string, any][]; // Filtered by search + isPanelVisible: boolean; +} + +const ToolWorkflowContext = createContext(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) { + const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); + + // Tool management hook + const { + selectedToolKey, + selectedTool, + toolRegistry, + selectTool, + clearToolSelection, + } = useToolManagement(); + + // UI Action creators + const setSidebarsVisible = useCallback((visible: boolean) => { + dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible }); + }, []); + + const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => { + dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view }); + }, []); + + const setReaderMode = useCallback((mode: boolean) => { + dispatch({ type: 'SET_READER_MODE', payload: mode }); + }, []); + + const setPreviewFile = useCallback((file: File | null) => { + dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); + }, []); + + 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 }); + }, []); + + // Workflow actions (compound actions that coordinate multiple state changes) + const handleToolSelect = useCallback((toolId: string) => { + selectTool(toolId); + onViewChange?.('fileEditor'); + setLeftPanelView('toolContent'); + setReaderMode(false); + }, [selectTool, onViewChange, setLeftPanelView, setReaderMode]); + + const handleBackToTools = useCallback(() => { + setLeftPanelView('toolPicker'); + setReaderMode(false); + clearToolSelection(); + }, [setLeftPanelView, setReaderMode, clearToolSelection]); + + const handleReaderToggle = useCallback(() => { + setReaderMode(true); + }, [setReaderMode]); + + // Computed values (memoized to prevent unnecessary recalculations) + // Separate filteredTools memoization to avoid context re-renders + const filteredTools = useMemo(() => { + if (!toolRegistry) return []; + return Object.entries(toolRegistry).filter(([_, { name }]) => + name.toLowerCase().includes(state.searchQuery.toLowerCase()) + ); + }, [toolRegistry, state.searchQuery]); + + const isPanelVisible = useMemo(() => + state.sidebarsVisible && !state.readerMode, + [state.sidebarsVisible, state.readerMode] + ); + + // Context value (memoized to prevent unnecessary rerenders) + const contextValue = useMemo((): ToolWorkflowContextValue => ({ + // State - destructure to avoid object reference changes + sidebarsVisible: state.sidebarsVisible, + leftPanelView: state.leftPanelView, + readerMode: state.readerMode, + previewFile: state.previewFile, + pageEditorFunctions: state.pageEditorFunctions, + searchQuery: state.searchQuery, + + // Tool state + selectedToolKey, + selectedTool, + toolRegistry, + + // Actions + setSidebarsVisible, + setLeftPanelView, + setReaderMode, + setPreviewFile, + setPageEditorFunctions, + setSearchQuery, + selectTool, + clearToolSelection, + + // Workflow Actions + handleToolSelect, + handleBackToTools, + handleReaderToggle, + + // Computed + filteredTools, + isPanelVisible, + }), [ + // State values (not the state object itself) + state.sidebarsVisible, + state.leftPanelView, + state.readerMode, + state.previewFile, + state.pageEditorFunctions, + state.searchQuery, + + // Tool state (toolRegistry removed from deps - it's passed through but doesn't affect memoization) + selectedToolKey, + selectedTool, + + // Actions are stable due to useCallback + setSidebarsVisible, + setLeftPanelView, + setReaderMode, + setPreviewFile, + setPageEditorFunctions, + setSearchQuery, + selectTool, + clearToolSelection, + handleToolSelect, + handleBackToTools, + handleReaderToggle, + + // Computed values + filteredTools, + isPanelVisible, + ]); + + return ( + + {children} + + ); +} + +// Custom hook to use the context +export function useToolWorkflow(): ToolWorkflowContextValue { + const context = useContext(ToolWorkflowContext); + if (!context) { + throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); + } + return context; +} + +// Selective hooks for performance (only subscribe to specific parts of state) +export function useToolSelection() { + const { selectedToolKey, selectedTool, selectTool, clearToolSelection, handleToolSelect } = useToolWorkflow(); + return { selectedToolKey, selectedTool, selectTool, clearToolSelection, handleToolSelect }; +} + +export function useToolPanelState() { + const { + leftPanelView, + isPanelVisible, + searchQuery, + filteredTools, + setLeftPanelView, + setSearchQuery, + handleBackToTools + } = useToolWorkflow(); + return { + leftPanelView, + isPanelVisible, + searchQuery, + filteredTools, + setLeftPanelView, + setSearchQuery, + handleBackToTools + }; +} + +export function useWorkbenchState() { + const { + previewFile, + pageEditorFunctions, + sidebarsVisible, + setPreviewFile, + setPageEditorFunctions, + setSidebarsVisible + } = useToolWorkflow(); + return { + previewFile, + pageEditorFunctions, + sidebarsVisible, + setPreviewFile, + setPageEditorFunctions, + setSidebarsVisible + }; +} \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 6df32611f..5e01df805 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,11 +1,10 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; +import React, { useCallback, useEffect } from "react"; import { useTranslation } from 'react-i18next'; import { useFileContext } from "../contexts/FileContext"; import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext"; -import { useToolManagement } from "../hooks/useToolManagement"; +import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext"; import { useFileHandler } from "../hooks/useFileHandler"; import { Group } from "@mantine/core"; -import { PageEditorFunctions } from "../types/pageEditor"; import ToolPanel from "../components/tools/ToolPanel"; import Workbench from "../components/layout/Workbench"; @@ -20,19 +19,7 @@ function HomePageContent() { const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection(); const { addToActiveFiles } = useFileHandler(); - const { - selectedToolKey, - selectedTool, - toolRegistry, - selectTool, - clearToolSelection, - } = useToolManagement(); - - const [sidebarsVisible, setSidebarsVisible] = useState(true); - const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); - const [readerMode, setReaderMode] = useState(false); - const [pageEditorFunctions, setPageEditorFunctions] = useState(null); - const [previewFile, setPreviewFile] = useState(null); + const { selectedTool, selectedToolKey } = useToolSelection(); // Update file selection context when tool changes useEffect(() => { @@ -46,25 +33,8 @@ function HomePageContent() { } }, [selectedTool, setMaxFiles, setIsToolMode, setSelectedFiles]); - const handleToolSelect = useCallback( - (id: string) => { - selectTool(id); - setCurrentView('fileEditor'); // Tools use fileEditor view for file selection - setLeftPanelView('toolContent'); - setReaderMode(false); - }, - [selectTool, setCurrentView] - ); - - const handleQuickAccessTools = useCallback(() => { - setLeftPanelView('toolPicker'); - setReaderMode(false); - clearToolSelection(); - }, [clearToolSelection]); - - const handleReaderToggle = useCallback(() => { - setReaderMode(true); - }, [readerMode]); + // These handlers are now provided by the context + // The context handles the coordination between tool selection and UI state const handleViewChange = useCallback((view: string) => { setCurrentView(view as any); @@ -76,41 +46,14 @@ function HomePageContent() { gap={0} className="min-h-screen w-screen overflow-hidden flex-nowrap flex" > - + - + @@ -120,11 +63,22 @@ function HomePageContent() { ); } -// Main HomePage component wrapped with FileSelectionProvider +// HomePage wrapper that connects context to file context +function HomePageWrapper() { + const { setCurrentView } = useFileContext(); + + return ( + + + + ); +} + +// Main HomePage component wrapped with providers export default function HomePage() { return ( - + ); }