From 681f0abfd8414af25cce0f30fc6f61474473e137 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Fri, 15 Aug 2025 19:34:18 +0100 Subject: [PATCH] remove the 'convenience exports' from ToolWorkflowContext and standardise toolButton for tool picker. + Other general improvements --- frontend/src/components/layout/Workbench.tsx | 6 +- .../src/components/tools/SearchResults.tsx | 7 +- frontend/src/components/tools/ToolPanel.tsx | 8 +- frontend/src/components/tools/ToolPicker.tsx | 115 +++++++----------- .../tools/convert/ConvertSettings.tsx | 11 +- .../components/tools/shared/NoToolsFound.tsx | 15 +++ .../tools/toolPicker/ToolPicker.css | 4 +- frontend/src/contexts/ToolWorkflowContext.tsx | 7 +- frontend/src/data/toolRegistry.tsx | 72 +++++++++-- frontend/src/hooks/useToolManagement.tsx | 13 +- frontend/src/hooks/useToolSections.ts | 6 +- frontend/src/pages/HomePage.tsx | 4 +- 12 files changed, 144 insertions(+), 124 deletions(-) create mode 100644 frontend/src/components/tools/shared/NoToolsFound.tsx diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index e6f101803..732b37d7b 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; -import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import { useFileHandler } from '../../hooks/useFileHandler'; import { useFileContext } from '../../contexts/FileContext'; @@ -28,9 +28,9 @@ export default function Workbench() { setPreviewFile, setPageEditorFunctions, setSidebarsVisible - } = useWorkbenchState(); + } = useToolWorkflow(); - const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection(); + const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow(); const { addToActiveFiles } = useFileHandler(); const handlePreviewClose = () => { diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index 55755bf6f..d00fd176f 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -5,6 +5,7 @@ import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; import { useToolSections } from '../../hooks/useToolSections'; import SubcategoryHeader from './shared/SubcategoryHeader'; +import NoToolsFound from './shared/NoToolsFound'; interface SearchResultsProps { filteredTools: [string, ToolRegistryEntry][]; @@ -16,11 +17,7 @@ const SearchResults: React.FC = ({ filteredTools, onSelect } const { searchGroups } = useToolSections(filteredTools); if (searchGroups.length === 0) { - return ( - - {t('toolPicker.noToolsFound', 'No tools found')} - - ); + return ; } return ( diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 334433648..9d8575685 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; -import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; import SearchResults from './SearchResults'; import ToolRenderer from './ToolRenderer'; @@ -27,10 +27,10 @@ export default function ToolPanel() { toolRegistry, setSearchQuery, handleBackToTools - } = useToolPanelState(); + } = useToolWorkflow(); - const { selectedToolKey, handleToolSelect } = useToolSelection(); - const { setPreviewFile } = useWorkbenchState(); + const { selectedToolKey, handleToolSelect } = useToolWorkflow(); + const { setPreviewFile } = useToolWorkflow(); return (
void, + showSubcategoryHeader: boolean = true +) => ( + + {showSubcategoryHeader && ( + + )} + + {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( + + ))} + + +); + const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => { const { t } = useTranslation(); const [quickHeaderHeight, setQuickHeaderHeight] = useState(0); @@ -92,26 +118,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa {isSearching ? ( {searchGroups.length === 0 ? ( - - {t("toolPicker.noToolsFound", "No tools found")} - + ) : ( - searchGroups.map(group => ( - - - - {group.tools.map(({ id, tool }) => ( - - ))} - - - )) + searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect)) )} ) : ( @@ -124,8 +133,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa position: "sticky", top: 0, zIndex: 2, - borderTop: `1px solid var(--tool-header-border)`, - borderBottom: `1px solid var(--tool-header-border)`, + borderTop: `0.0625rem solid var(--tool-header-border)`, + borderBottom: `0.0625rem solid var(--tool-header-border)`, marginBottom: -1, padding: "0.5rem 1rem", fontWeight: 700, @@ -143,9 +152,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa style={{ background: "var(--tool-header-badge-bg)", color: "var(--tool-header-badge-text)", - borderRadius: 8, - padding: "2px 8px", - fontSize: 12, + borderRadius: ".5rem", + padding: "0.125rem 0.5rem", + fontSize: ".75rem", fontWeight: 700 }} > @@ -155,24 +164,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {quickSection?.subcategories.map(sc => ( - - {quickSection?.subcategories.length > 1 && ( - - )} - - {sc.tools.map(({ id, tool }) => ( - - ))} - - - ))} + {quickSection?.subcategories.map(sc => + renderToolButtons(sc, selectedToolKey, onSelect, quickSection?.subcategories.length === 1) + )} @@ -186,8 +180,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa position: "sticky", top: quickSection ? quickHeaderHeight - 1: 0, zIndex: 2, - borderTop: `1px solid var(--tool-header-border)`, - borderBottom: `1px solid var(--tool-header-border)`, + borderTop: `0.0625rem solid var(--tool-header-border)`, + borderBottom: `0.0625rem solid var(--tool-header-border)`, padding: "0.5rem 1rem", fontWeight: 700, background: "var(--tool-header-bg)", @@ -204,9 +198,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa style={{ background: "var(--tool-header-badge-bg)", color: "var(--tool-header-badge-text)", - borderRadius: 8, - padding: "2px 8px", - fontSize: 12, + borderRadius: ".5rem", + padding: "0.125rem 0.5rem", + fontSize: ".75rem", fontWeight: 700 }} > @@ -216,34 +210,15 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {allSection?.subcategories.map(sc => ( - - {allSection?.subcategories.length > 1 && ( - - )} - - {sc.tools.map(({ id, tool }) => ( - - ))} - - - ))} + {allSection?.subcategories.map(sc => + renderToolButtons(sc, selectedToolKey, onSelect, allSection?.subcategories.length === 1) + )} )} - {!quickSection && !allSection && ( - - {t("toolPicker.noToolsFound", "No tools found")} - - )} + {!quickSection && !allSection && } {/* bottom spacer to allow scrolling past the last row */}
diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx index a3051c88f..b0f413c64 100644 --- a/frontend/src/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertSettings.tsx @@ -4,6 +4,7 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { useTranslation } from "react-i18next"; import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig"; import { isImageFormat, isWebFormat } from "../../../utils/convertUtils"; +import { getConversionEndpoints } from "../../../data/toolRegistry"; import { useFileSelectionActions } from "../../../contexts/FileSelectionContext"; import { useFileContext } from "../../../contexts/FileContext"; import { detectFileExtension } from "../../../utils/fileUtils"; @@ -43,15 +44,7 @@ const ConvertSettings = ({ const { setSelectedFiles } = useFileSelectionActions(); const { activeFiles, setSelectedFiles: setContextSelectedFiles } = useFileContext(); - const allEndpoints = useMemo(() => { - const endpoints = new Set(); - Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => { - Object.values(toEndpoints).forEach(endpoint => { - endpoints.add(endpoint); - }); - }); - return Array.from(endpoints); - }, []); + const allEndpoints = useMemo(() => getConversionEndpoints(EXTENSION_TO_ENDPOINT), []); const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints); diff --git a/frontend/src/components/tools/shared/NoToolsFound.tsx b/frontend/src/components/tools/shared/NoToolsFound.tsx new file mode 100644 index 000000000..1b60f0cb7 --- /dev/null +++ b/frontend/src/components/tools/shared/NoToolsFound.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; + +const NoToolsFound: React.FC = () => { + const { t } = useTranslation(); + + return ( + + {t("toolPicker.noToolsFound", "No tools found")} + + ); +}; + +export default NoToolsFound; diff --git a/frontend/src/components/tools/toolPicker/ToolPicker.css b/frontend/src/components/tools/toolPicker/ToolPicker.css index b61f16a9a..3145661d5 100644 --- a/frontend/src/components/tools/toolPicker/ToolPicker.css +++ b/frontend/src/components/tools/toolPicker/ToolPicker.css @@ -6,7 +6,7 @@ } .tool-picker-scrollable::-webkit-scrollbar { - width: 6px; + width: 0.375rem; } .tool-picker-scrollable::-webkit-scrollbar-track { @@ -15,7 +15,7 @@ .tool-picker-scrollable::-webkit-scrollbar-thumb { background-color: var(--mantine-color-gray-4); - border-radius: 3px; + border-radius: 0.1875rem; } .tool-picker-scrollable::-webkit-scrollbar-thumb:hover { diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 1538b65dc..76360119f 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -224,9 +224,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue { throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); } return context; -} - -// Convenience exports for specific use cases (optional - components can use useToolWorkflow directly) -export const useToolSelection = useToolWorkflow; -export const useToolPanelState = useToolWorkflow; -export const useWorkbenchState = useToolWorkflow; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/data/toolRegistry.tsx b/frontend/src/data/toolRegistry.tsx index dff31806f..a86556f0c 100644 --- a/frontend/src/data/toolRegistry.tsx +++ b/frontend/src/data/toolRegistry.tsx @@ -12,7 +12,7 @@ export type ToolRegistryEntry = { view: string; description: string; category: string; - subcategory: string | null; + subcategory: string; // Optional custom props for tools maxFiles?: number; @@ -77,8 +77,7 @@ export const SUBCATEGORY_COLOR_MAP: Record = { 'Developer Tools': '#F55454', }; -export const getSubcategoryColor = (subcategory?: string | null): string => { - if (!subcategory) return '#7882FF'; +export const getSubcategoryColor = (subcategory: string): string => { return SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF'; }; @@ -575,7 +574,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "format", description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), category: "Recommended Tools", - subcategory: null + subcategory: "General" }, "compressPdfs": { icon: zoom_in_map, @@ -584,7 +583,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "compress", description: t("home.compressPdfs.desc", "Compress PDFs to reduce their file size."), category: "Recommended Tools", - subcategory: null, + subcategory: "General", maxFiles: -1 }, "convert": { @@ -594,7 +593,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "convert", description: t("home.fileToPDF.desc", "Convert files to and from PDF format"), category: "Recommended Tools", - subcategory: null, + subcategory: "General", maxFiles: -1, endpoints: [ "pdf-to-img", @@ -638,7 +637,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "merge", description: t("home.merge.desc", "Merge multiple PDFs into a single document"), category: "Recommended Tools", - subcategory: null, + subcategory: "General", maxFiles: -1 }, "multi-tool": { @@ -648,7 +647,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "pageEditor", description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), category: "Recommended Tools", - subcategory: null, + subcategory: "General", maxFiles: -1 }, "ocr": { @@ -658,7 +657,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "convert", description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"), category: "Recommended Tools", - subcategory: null, + subcategory: "General", maxFiles: -1 }, "redact": { @@ -668,7 +667,7 @@ export function useFlatToolRegistry(): ToolRegistry { view: "redact", description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), category: "Recommended Tools", - subcategory: null + subcategory: "General" }, }; } @@ -681,4 +680,57 @@ export const toolEndpoints: Record = { compressPdfs: ["compress-pdf"], merge: ["merge-pdfs"], // Add more endpoint mappings as needed +}; + +/** + * Get all endpoints from both registry entries and legacy toolEndpoints mapping + * This consolidates endpoint discovery logic in one place + */ +export const getAllEndpoints = (registry: ToolRegistry): string[] => { + const lists: string[][] = []; + + // Get endpoints from registry entries + Object.values(registry).forEach(entry => { + if (entry.endpoints && entry.endpoints.length > 0) { + lists.push(entry.endpoints); + } + }); + + // Get endpoints from legacy toolEndpoints mapping + Object.entries(toolEndpoints).forEach(([key, list]) => { + // Only add if not already covered by registry entries + if (!registry[key]?.endpoints) { + lists.push(list); + } + }); + + return Array.from(new Set(lists.flat())); +}; + +/** + * Get all endpoints from a conversion matrix (like EXTENSION_TO_ENDPOINT) + * This is useful for convert-specific endpoint discovery + */ +export const getConversionEndpoints = (extensionToEndpoint: Record>): string[] => { + const endpoints = new Set(); + Object.values(extensionToEndpoint).forEach(toEndpoints => { + Object.values(toEndpoints).forEach(endpoint => { + endpoints.add(endpoint); + }); + }); + return Array.from(endpoints); +}; + +/** + * Get all endpoints from both tool registry and conversion matrix + * This provides comprehensive endpoint coverage for the entire application + */ +export const getAllApplicationEndpoints = ( + registry: ToolRegistry, + extensionToEndpoint?: Record> +): string[] => { + const toolEndpoints = getAllEndpoints(registry); + const conversionEndpoints = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; + + return Array.from(new Set([...toolEndpoints, ...conversionEndpoints])); }; \ No newline at end of file diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index b4e8171a5..f6106f591 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useFlatToolRegistry, toolEndpoints, type ToolRegistryEntry } from "../data/toolRegistry"; +import { useFlatToolRegistry, toolEndpoints, getAllEndpoints, type ToolRegistryEntry } from "../data/toolRegistry"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; interface ToolManagementResult { @@ -20,7 +20,7 @@ export const useToolManagement = (): ToolManagementResult => { const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); // Build endpoints list from registry entries with fallback to legacy mapping - const baseRegistry = useFlatToolRegistry(); + const baseRegistry = useFlatToolRegistry(); const registryDerivedEndpoints = useMemo(() => { const endpointsByTool: Record = {}; Object.entries(baseRegistry).forEach(([key, entry]) => { @@ -31,14 +31,7 @@ export const useToolManagement = (): ToolManagementResult => { return endpointsByTool; }, [baseRegistry]); - const allEndpoints = useMemo(() => { - const lists: string[][] = []; - Object.values(registryDerivedEndpoints).forEach(list => lists.push(list)); - Object.entries(toolEndpoints).forEach(([key, list]) => { - if (!registryDerivedEndpoints[key]) lists.push(list); - }); - return Array.from(new Set(lists.flat())); - }, [registryDerivedEndpoints]); + const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]); const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints); const isToolAvailable = useCallback((toolKey: string): boolean => { diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index c79421d90..12478b643 100644 --- a/frontend/src/hooks/useToolSections.ts +++ b/frontend/src/hooks/useToolSections.ts @@ -14,8 +14,8 @@ export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { const groupedTools = useMemo(() => { const grouped: GroupedTools = {}; filteredTools.forEach(([id, tool]) => { - const category = tool?.category || 'OTHER'; - const subcategory = tool?.subcategory || 'General'; + const category = tool.category; + const subcategory = tool.subcategory; if (!grouped[category]) grouped[category] = {}; if (!grouped[category][subcategory]) grouped[category][subcategory] = []; grouped[category][subcategory].push({ id, tool }); @@ -72,7 +72,7 @@ export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { filteredTools.forEach(([id, tool]) => { if (seen.has(id)) return; seen.add(id); - const sub = tool?.subcategory || 'General'; + const sub = tool.subcategory; if (!subMap[sub]) subMap[sub] = []; subMap[sub].push({ id, tool }); }); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index d4f8de41e..d2914c7d0 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { useFileContext } from "../contexts/FileContext"; import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext"; -import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext"; +import { ToolWorkflowProvider, useToolWorkflow } from "../contexts/ToolWorkflowContext"; import { Group } from "@mantine/core"; import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext"; @@ -20,7 +20,7 @@ function HomePageContent() { const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection(); - const { selectedTool } = useToolSelection(); + const { selectedTool } = useToolWorkflow(); // Update file selection context when tool changes useEffect(() => {