remove the 'convenience exports' from ToolWorkflowContext and standardise toolButton for tool picker. + Other general improvements

This commit is contained in:
EthanHealy01 2025-08-15 19:34:18 +01:00
parent f2b5a4a1f8
commit 681f0abfd8
12 changed files with 144 additions and 124 deletions

View File

@ -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 = () => {

View File

@ -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<SearchResultsProps> = ({ filteredTools, onSelect }
const { searchGroups } = useToolSections(filteredTools);
if (searchGroups.length === 0) {
return (
<Text c="dimmed" size="sm" p="sm">
{t('toolPicker.noToolsFound', 'No tools found')}
</Text>
);
return <NoToolsFound />;
}
return (

View File

@ -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 (
<div

View File

@ -6,6 +6,7 @@ import ToolButton from "./toolPicker/ToolButton";
import "./toolPicker/ToolPicker.css";
import { useToolSections } from "../../hooks/useToolSections";
import SubcategoryHeader from "./shared/SubcategoryHeader";
import NoToolsFound from "./shared/NoToolsFound";
interface ToolPickerProps {
selectedToolKey: string | null;
@ -14,6 +15,31 @@ interface ToolPickerProps {
isSearching?: boolean;
}
// Helper function to render tool buttons for a subcategory
const renderToolButtons = (
subcategory: any,
selectedToolKey: string | null,
onSelect: (id: string) => void,
showSubcategoryHeader: boolean = true
) => (
<Box key={subcategory.subcategory} w="100%">
{showSubcategoryHeader && (
<SubcategoryHeader label={subcategory.subcategory} />
)}
<Stack gap="xs">
{subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={selectedToolKey === id}
onSelect={onSelect}
/>
))}
</Stack>
</Box>
);
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 ? (
<Stack p="sm" gap="xs">
{searchGroups.length === 0 ? (
<Text c="dimmed" size="sm" p="sm">
{t("toolPicker.noToolsFound", "No tools found")}
</Text>
<NoToolsFound />
) : (
searchGroups.map(group => (
<Box key={group.subcategory} w="100%">
<SubcategoryHeader label={group.subcategory} />
<Stack gap="xs">
{group.tools.map(({ id, tool }) => (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={selectedToolKey === id}
onSelect={onSelect}
/>
))}
</Stack>
</Box>
))
searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect))
)}
</Stack>
) : (
@ -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
<Box ref={quickAccessRef} w="100%">
<Stack p="sm" gap="xs">
{quickSection?.subcategories.map(sc => (
<Box key={sc.subcategory} w="100%">
{quickSection?.subcategories.length > 1 && (
<SubcategoryHeader label={sc.subcategory} />
)}
<Stack gap="xs">
{sc.tools.map(({ id, tool }) => (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={selectedToolKey === id}
onSelect={onSelect}
/>
))}
</Stack>
</Box>
))}
{quickSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, quickSection?.subcategories.length === 1)
)}
</Stack>
</Box>
</>
@ -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
<Box ref={allToolsRef} w="100%">
<Stack p="sm" gap="xs">
{allSection?.subcategories.map(sc => (
<Box key={sc.subcategory} w="100%">
{allSection?.subcategories.length > 1 && (
<SubcategoryHeader label={sc.subcategory} />
)}
<Stack gap="xs">
{sc.tools.map(({ id, tool }) => (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={selectedToolKey === id}
onSelect={onSelect}
/>
))}
</Stack>
</Box>
))}
{allSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, allSection?.subcategories.length === 1)
)}
</Stack>
</Box>
</>
)}
{!quickSection && !allSection && (
<Text c="dimmed" size="sm" p="sm">
{t("toolPicker.noToolsFound", "No tools found")}
</Text>
)}
{!quickSection && !allSection && <NoToolsFound />}
{/* bottom spacer to allow scrolling past the last row */}
<div aria-hidden style={{ height: 200 }} />

View File

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

View File

@ -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 (
<Text c="dimmed" size="sm" p="sm">
{t("toolPicker.noToolsFound", "No tools found")}
</Text>
);
};
export default NoToolsFound;

View File

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

View File

@ -225,8 +225,3 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
}
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;

View File

@ -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<string, string> = {
'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: <span className="material-symbols-rounded">zoom_in_map</span>,
@ -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"
},
};
}
@ -682,3 +681,56 @@ export const toolEndpoints: Record<string, string[]> = {
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, Record<string, string>>): string[] => {
const endpoints = new Set<string>();
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, Record<string, string>>
): string[] => {
const toolEndpoints = getAllEndpoints(registry);
const conversionEndpoints = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
return Array.from(new Set([...toolEndpoints, ...conversionEndpoints]));
};

View File

@ -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<string[]>([]);
// Build endpoints list from registry entries with fallback to legacy mapping
const baseRegistry = useFlatToolRegistry();
const baseRegistry = useFlatToolRegistry();
const registryDerivedEndpoints = useMemo(() => {
const endpointsByTool: Record<string, string[]> = {};
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 => {

View File

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

View File

@ -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(() => {