From 3090a85726d7cf915656942c806e6e2e386c4ba6 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Wed, 8 Oct 2025 17:18:05 +0100 Subject: [PATCH] Assign shortcuts by default to only the quick access items (#4622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes Change shortcuts to just be a limited set for Quick Access tools rather than for everything to avoid breaking browser key commands by default. Also fixes a bunch of types of variables that were representing `ToolId`s (I stopped at `automate` because there's loads in there so I've just introduced some `any` casts for now 😭) --- .../public/locales/en-GB/translation.json | 3 +- .../src/components/hotkeys/HotkeyDisplay.tsx | 2 +- .../config/configSections/HotkeysSection.tsx | 14 ++- .../src/components/tools/SearchResults.tsx | 9 +- frontend/src/components/tools/ToolPicker.tsx | 3 +- .../src/components/tools/ToolRenderer.tsx | 3 +- .../tools/automate/ToolSelector.tsx | 7 +- .../tools/shared/renderToolButtons.tsx | 5 +- .../tools/toolPicker/ToolButton.tsx | 21 ++-- frontend/src/contexts/HotkeyContext.tsx | 104 +++++++++--------- frontend/src/contexts/ToolWorkflowContext.tsx | 8 +- .../src/data/useTranslatedToolRegistry.tsx | 2 +- frontend/src/hooks/useSidebarNavigation.ts | 9 +- frontend/src/hooks/useToolManagement.tsx | 15 +-- frontend/src/hooks/useToolSections.ts | 15 +-- frontend/src/utils/toolSearch.ts | 19 ++-- 16 files changed, 125 insertions(+), 114 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 01a8278f6..1be334754 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -283,7 +283,8 @@ "capturing": "Press keys… (Esc to cancel)", "change": "Change shortcut", "reset": "Reset", - "shortcut": "Shortcut" + "shortcut": "Shortcut", + "noShortcut": "No shortcut set" } }, "changeCreds": { diff --git a/frontend/src/components/hotkeys/HotkeyDisplay.tsx b/frontend/src/components/hotkeys/HotkeyDisplay.tsx index e240f987e..1b5953a63 100644 --- a/frontend/src/components/hotkeys/HotkeyDisplay.tsx +++ b/frontend/src/components/hotkeys/HotkeyDisplay.tsx @@ -55,4 +55,4 @@ export const HotkeyDisplay: React.FC = ({ binding, size = 's ); }; -export default HotkeyDisplay; \ No newline at end of file +export default HotkeyDisplay; diff --git a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx index e0240f55a..9630d6f05 100644 --- a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx +++ b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx @@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next'; import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext'; import { useHotkeys } from '../../../../contexts/HotkeyContext'; import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay'; -import { bindingEquals, eventToBinding } from '../../../../utils/hotkeys'; +import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys'; +import { ToolId } from 'src/types/toolId'; +import { ToolRegistryEntry } from 'src/data/toolsTaxonomy'; const rowStyle: React.CSSProperties = { display: 'flex', @@ -24,10 +26,10 @@ const HotkeysSection: React.FC = () => { const { t } = useTranslation(); const { toolRegistry } = useToolWorkflow(); const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys(); - const [editingTool, setEditingTool] = useState(null); + const [editingTool, setEditingTool] = useState(null); const [error, setError] = useState(null); - const tools = useMemo(() => Object.entries(toolRegistry), [toolRegistry]); + const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]); useEffect(() => { if (!editingTool) { @@ -64,7 +66,7 @@ const HotkeysSection: React.FC = () => { return; } - const conflictEntry = Object.entries(hotkeys).find(([toolId, existing]) => ( + const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => ( toolId !== editingTool && bindingEquals(existing, binding) )); @@ -85,7 +87,7 @@ const HotkeysSection: React.FC = () => { }; }, [editingTool, hotkeys, toolRegistry, updateHotkey, t]); - const handleStartCapture = (toolId: string) => { + const handleStartCapture = (toolId: ToolId) => { setEditingTool(toolId); setError(null); }; @@ -168,4 +170,4 @@ const HotkeysSection: React.FC = () => { ); }; -export default HotkeysSection; \ No newline at end of file +export default HotkeysSection; diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index 5bd6036cc..aca9f88bc 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -7,9 +7,10 @@ import { useToolSections } from '../../hooks/useToolSections'; import SubcategoryHeader from './shared/SubcategoryHeader'; import NoToolsFound from './shared/NoToolsFound'; import "./toolPicker/ToolPicker.css"; +import { ToolId } from 'src/types/toolId'; interface SearchResultsProps { - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; onSelect: (id: string) => void; searchQuery?: string; } @@ -40,13 +41,13 @@ const SearchResults: React.FC = ({ filteredTools, onSelect, {group.tools.map(({ id, tool }) => { const matchedText = matchedTextMap.get(id); // Check if the match was from synonyms and show the actual synonym that matched - const isSynonymMatch = matchedText && tool.synonyms?.some(synonym => + const isSynonymMatch = matchedText && tool.synonyms?.some(synonym => matchedText.toLowerCase().includes(synonym.toLowerCase()) ); - const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym => + const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym => matchedText.toLowerCase().includes(synonym.toLowerCase()) ) : undefined; - + return ( void; - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; isSearching?: boolean; } diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx index fbd8100d1..97353ba09 100644 --- a/frontend/src/components/tools/ToolRenderer.tsx +++ b/frontend/src/components/tools/ToolRenderer.tsx @@ -2,9 +2,10 @@ import { Suspense } from "react"; import { useToolWorkflow } from "../../contexts/ToolWorkflowContext"; import { BaseToolProps } from "../../types/tool"; import ToolLoadingFallback from "./ToolLoadingFallback"; +import { ToolId } from "src/types/toolId"; interface ToolRendererProps extends BaseToolProps { - selectedToolKey: string; + selectedToolKey: ToolId; } diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 5cbc4b3ab..02fea2c97 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -6,6 +6,7 @@ import { useToolSections } from '../../../hooks/useToolSections'; import { renderToolButtons } from '../shared/renderToolButtons'; import ToolSearch from '../toolPicker/ToolSearch'; import ToolButton from '../toolPicker/ToolButton'; +import { ToolId } from 'src/types/toolId'; interface ToolSelectorProps { onSelect: (toolKey: string) => void; @@ -30,7 +31,7 @@ export default function ToolSelector({ // Filter out excluded tools (like 'automate' itself) and tools that don't support automation const baseFilteredTools = useMemo(() => { - return Object.entries(toolRegistry).filter(([key, tool]) => + return (Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]).filter(([key, tool]) => !excludeTools.includes(key) && getToolSupportsAutomate(tool) ); }, [toolRegistry, excludeTools]); @@ -66,7 +67,7 @@ export default function ToolSelector({ }, [filteredTools]); // Use the same tool sections logic as the main ToolPicker - const { sections, searchGroups } = useToolSections(transformedFilteredTools); + const { sections, searchGroups } = useToolSections(transformedFilteredTools as any /* FIX ME */); // Determine what to display: search results or organized sections const isSearching = searchTerm.trim().length > 0; @@ -156,7 +157,7 @@ export default function ToolSelector({ // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
- {}} rounded={true} disableNavigation={true}>
) : ( diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx index a4aadf20b..659ffefca 100644 --- a/frontend/src/components/tools/shared/renderToolButtons.tsx +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -5,13 +5,14 @@ import SubcategoryHeader from './SubcategoryHeader'; import { getSubcategoryLabel } from "../../../data/toolsTaxonomy"; import { TFunction } from 'i18next'; import { SubcategoryGroup } from '../../../hooks/useToolSections'; +import { ToolId } from 'src/types/toolId'; // Helper function to render tool buttons for a subcategory export const renderToolButtons = ( t: TFunction, subcategory: SubcategoryGroup, selectedToolKey: string | null, - onSelect: (id: string) => void, + onSelect: (id: ToolId) => void, showSubcategoryHeader: boolean = true, disableNavigation: boolean = false, searchResults?: Array<{ item: [string, any]; matchedText?: string }> @@ -32,7 +33,7 @@ export const renderToolButtons = (
{subcategory.tools.map(({ id, tool }) => { const matchedSynonym = matchedTextMap.get(id); - + return ( void; + onSelect: (id: ToolId) => void; rounded?: boolean; disableNavigation?: boolean; matchedSynonym?: string; @@ -28,7 +29,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, const binding = hotkeys[id]; const { getToolNavigation } = useToolNavigation(); - const handleClick = (id: string) => { + const handleClick = (id: ToolId) => { if (isUnavailable) return; if (tool.link) { // Open external link in new tab @@ -47,12 +48,16 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, : (
{tool.description} - {binding && ( -
+
+ {binding ? ( + <> {t('settings.hotkeys.shortcut', 'Shortcut')} -
+ + ) : ( + {t('settings.hotkeys.noShortcut', 'No shortcut set')} )} +
); @@ -102,7 +107,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, fullWidth justify="flex-start" className="tool-button" - styles={{ + styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' }, label: { overflow: 'visible' } }} @@ -123,7 +128,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, fullWidth justify="flex-start" className="tool-button" - styles={{ + styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' }, label: { overflow: 'visible' } }} diff --git a/frontend/src/contexts/HotkeyContext.tsx b/frontend/src/contexts/HotkeyContext.tsx index fe9cbb600..c5a0f2649 100644 --- a/frontend/src/contexts/HotkeyContext.tsx +++ b/frontend/src/contexts/HotkeyContext.tsx @@ -2,14 +2,17 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys'; import { useToolWorkflow } from './ToolWorkflowContext'; import { ToolId } from '../types/toolId'; +import { ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy'; + +type Bindings = Partial>; interface HotkeyContextValue { - hotkeys: Record; - defaults: Record; + hotkeys: Bindings; + defaults: Bindings; isMac: boolean; - updateHotkey: (toolId: string, binding: HotkeyBinding) => void; - resetHotkey: (toolId: string) => void; - isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: string) => boolean; + updateHotkey: (toolId: ToolId, binding: HotkeyBinding) => void; + resetHotkey: (toolId: ToolId) => void; + isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: ToolId) => boolean; pauseHotkeys: () => void; resumeHotkeys: () => void; areHotkeysPaused: boolean; @@ -20,46 +23,29 @@ const HotkeyContext = createContext(undefined); const STORAGE_KEY = 'stirlingpdf.hotkeys'; -const KEY_ORDER: string[] = [ - 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', - 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP', - 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL', - 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM', - 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', -]; +const generateDefaultHotkeys = (toolEntries: [ToolId, ToolRegistryEntry][], macLike: boolean): Bindings => { + const defaults: Bindings = {}; -const generateDefaultHotkeys = (toolIds: string[], macLike: boolean): Record => { - const defaults: Record = {}; - let index = 0; - let useShift = false; + // Get Quick Access tools (RECOMMENDED_TOOLS category) from registry + const quickAccessTools = toolEntries + .filter(([_, tool]) => tool.categoryId === ToolCategoryId.RECOMMENDED_TOOLS) + .map(([toolId, _]) => toolId); - const nextBinding = (): HotkeyBinding => { - if (index >= KEY_ORDER.length) { - index = 0; - if (!useShift) { - useShift = true; - } else { - // If we somehow run out of combinations, wrap back around (unlikely given tool count) - useShift = false; - } + // Assign Cmd+Option+Number (Mac) or Ctrl+Alt+Number (Windows) to Quick Access tools + quickAccessTools.forEach((toolId, index) => { + if (index < 9) { // Limit to Digit1-9 + const digitNumber = index + 1; + defaults[toolId] = { + code: `Digit${digitNumber}`, + alt: true, + shift: false, + meta: macLike, + ctrl: !macLike, + }; } - - const code = KEY_ORDER[index]; - index += 1; - - return { - code, - alt: true, - shift: useShift, - meta: macLike, - ctrl: !macLike, - }; - }; - - toolIds.forEach(toolId => { - defaults[toolId] = nextBinding(); }); + // All other tools have no default (will be undefined in the record) return defaults; }; @@ -74,7 +60,7 @@ const shouldIgnoreTarget = (target: EventTarget | null): boolean => { export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { toolRegistry, handleToolSelect } = useToolWorkflow(); const isMac = useMemo(() => isMacLike(), []); - const [customBindings, setCustomBindings] = useState>(() => { + const [customBindings, setCustomBindings] = useState(() => { if (typeof window === 'undefined') { return {}; } @@ -82,16 +68,16 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr }); const [areHotkeysPaused, setHotkeysPaused] = useState(false); - const toolIds = useMemo(() => Object.keys(toolRegistry), [toolRegistry]); + const toolEntries = useMemo(() => Object.entries(toolRegistry), [toolRegistry]) as [ToolId, ToolRegistryEntry][]; - const defaults = useMemo(() => generateDefaultHotkeys(toolIds, isMac), [toolIds, isMac]); + const defaults = useMemo(() => generateDefaultHotkeys(toolEntries, isMac), [toolRegistry, isMac]); // Remove bindings for tools that are no longer present useEffect(() => { setCustomBindings(prev => { - const next: Record = {}; + const next: Bindings = {}; let changed = false; - Object.entries(prev).forEach(([toolId, binding]) => { + (Object.entries(prev) as [ToolId, HotkeyBinding][]).forEach(([toolId, binding]) => { if (toolRegistry[toolId]) { next[toolId] = binding; } else { @@ -103,13 +89,21 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr }, [toolRegistry]); const resolved = useMemo(() => { - const merged: Record = {}; - toolIds.forEach(toolId => { + const merged: Bindings = {}; + toolEntries.forEach(([toolId, _]) => { const custom = customBindings[toolId]; - merged[toolId] = custom ? normalizeBinding(custom) : defaults[toolId]; + const defaultBinding = defaults[toolId]; + + // Only add to resolved if there's a custom binding or a default binding + if (custom) { + merged[toolId] = normalizeBinding(custom); + } else if (defaultBinding) { + merged[toolId] = defaultBinding; + } + // If neither exists, don't add to merged (tool has no hotkey) }); return merged; - }, [customBindings, defaults, toolIds]); + }, [customBindings, defaults, toolEntries]); useEffect(() => { if (typeof window === 'undefined') { @@ -118,7 +112,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings)); }, [customBindings]); - const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: string) => { + const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: ToolId) => { const normalized = normalizeBinding(binding); return Object.entries(resolved).every(([toolId, existing]) => { if (toolId === excludeToolId) { @@ -128,7 +122,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr }); }, [resolved]); - const updateHotkey = useCallback((toolId: string, binding: HotkeyBinding) => { + const updateHotkey = useCallback((toolId: ToolId, binding: HotkeyBinding) => { setCustomBindings(prev => { const normalized = normalizeBinding(binding); const defaultsForTool = defaults[toolId]; @@ -142,7 +136,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr }); }, [defaults]); - const resetHotkey = useCallback((toolId: string) => { + const resetHotkey = useCallback((toolId: ToolId) => { setCustomBindings(prev => { if (!(toolId in prev)) { return prev; @@ -165,12 +159,12 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr if (event.repeat) return; if (shouldIgnoreTarget(event.target)) return; - const entries = Object.entries(resolved) as Array<[string, HotkeyBinding]>; + const entries = Object.entries(resolved) as [ToolId, HotkeyBinding][]; for (const [toolId, binding] of entries) { if (bindingMatchesEvent(binding, event)) { event.preventDefault(); event.stopPropagation(); - handleToolSelect(toolId as ToolId); + handleToolSelect(toolId); break; } } @@ -208,4 +202,4 @@ export const useHotkeys = (): HotkeyContextValue => { throw new Error('useHotkeys must be used within a HotkeyProvider'); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 98a0fd37d..73ef058bf 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -73,10 +73,10 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio // Context value interface interface ToolWorkflowContextValue extends ToolWorkflowState { // Tool management (from hook) - selectedToolKey: string | null; + selectedToolKey: ToolId | null; selectedTool: ToolRegistryEntry | null; - toolRegistry: Record; - getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null; + toolRegistry: Partial; + getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null; // UI Actions setSidebarsVisible: (visible: boolean) => void; @@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { handleReaderToggle: () => void; // Computed values - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search isPanelVisible: boolean; } diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 745738566..fc500aaee 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -824,7 +824,7 @@ export function useFlatToolRegistry(): ToolRegistry { name: t("home.compare.title", "Compare"), component: null, description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), - categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + categoryId: ToolCategoryId.STANDARD_TOOLS /* TODO: Change to RECOMMENDED_TOOLS when component is implemented */, subcategoryId: SubcategoryId.GENERAL, synonyms: getSynonyms(t, "compare"), supportsAutomate: false, diff --git a/frontend/src/hooks/useSidebarNavigation.ts b/frontend/src/hooks/useSidebarNavigation.ts index a65ea96de..42f92e0f1 100644 --- a/frontend/src/hooks/useSidebarNavigation.ts +++ b/frontend/src/hooks/useSidebarNavigation.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { useToolNavigation } from './useToolNavigation'; import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; import { handleUnlessSpecialClick } from '../utils/clickHandlers'; +import { ToolId } from 'src/types/toolId'; export interface SidebarNavigationProps { /** Full URL for the navigation (for href attribute) */ @@ -16,7 +17,7 @@ export interface SidebarNavigationProps { */ export function useSidebarNavigation(): { getHomeNavigation: () => SidebarNavigationProps; - getToolNavigation: (toolId: string) => SidebarNavigationProps | null; + getToolNavigation: (toolId: ToolId) => SidebarNavigationProps | null; } { const { getToolNavigation: getToolNavProps } = useToolNavigation(); const { getSelectedTool } = useToolWorkflow(); @@ -32,14 +33,14 @@ export function useSidebarNavigation(): { return { href, onClick: defaultNavClick }; }, [defaultNavClick]); - const getToolNavigation = useCallback((toolId: string): SidebarNavigationProps | null => { + const getToolNavigation = useCallback((toolId: ToolId): SidebarNavigationProps | null => { // Handle special nav sections that aren't tools if (toolId === 'read') return { href: '/read', onClick: defaultNavClick }; if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick }; const tool = getSelectedTool(toolId); if (!tool) return null; - + // Delegate to useToolNavigation for true tools return getToolNavProps(toolId, tool); }, [getToolNavProps, getSelectedTool, defaultNavClick]); @@ -48,4 +49,4 @@ export function useSidebarNavigation(): { getHomeNavigation, getToolNavigation }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index 3239cbaaa..054e99304 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -1,16 +1,17 @@ import { useState, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; -import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy"; +import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { FileId } from '../types/file'; +import { ToolId } from 'src/types/toolId'; interface ToolManagementResult { selectedTool: ToolRegistryEntry | null; toolSelectedFileIds: FileId[]; - toolRegistry: Record; + toolRegistry: Partial; setToolSelectedFileIds: (fileIds: FileId[]) => void; - getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null; + getSelectedTool: (toolKey: ToolId | null) => ToolRegistryEntry | null; } export const useToolManagement = (): ToolManagementResult => { @@ -30,9 +31,9 @@ export const useToolManagement = (): ToolManagementResult => { return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true); }, [endpointsLoading, endpointStatus, baseRegistry]); - const toolRegistry: Record = useMemo(() => { - const availableToolRegistry: Record = {}; - Object.keys(baseRegistry).forEach(toolKey => { + const toolRegistry: Partial = useMemo(() => { + const availableToolRegistry: Partial = {}; + (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => { if (isToolAvailable(toolKey)) { const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry]; availableToolRegistry[toolKey] = { @@ -45,7 +46,7 @@ export const useToolManagement = (): ToolManagementResult => { return availableToolRegistry; }, [isToolAvailable, t, baseRegistry]); - const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => { + const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => { return toolKey ? toolRegistry[toolKey] || null : null; }, [toolRegistry]); diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index 5c5be8773..40eac31e6 100644 --- a/frontend/src/hooks/useToolSections.ts +++ b/frontend/src/hooks/useToolSections.ts @@ -2,9 +2,10 @@ import { useMemo } from 'react'; import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy'; import { useTranslation } from 'react-i18next'; +import { ToolId } from 'src/types/toolId'; type SubcategoryIdMap = { - [subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>; + [subcategoryId in SubcategoryId]: Array<{ id: ToolId; tool: ToolRegistryEntry }>; } type GroupedTools = { @@ -14,7 +15,7 @@ type GroupedTools = { export interface SubcategoryGroup { subcategoryId: SubcategoryId; tools: { - id: string /* FIX ME: Should be ToolId */; + id: ToolId; tool: ToolRegistryEntry; }[]; }; @@ -28,7 +29,7 @@ export interface ToolSection { }; export function useToolSections( - filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>, + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>, searchQuery?: string ) { const { t } = useTranslation(); @@ -37,7 +38,7 @@ export function useToolSections( if (!filteredTools || !Array.isArray(filteredTools)) { return {} as GroupedTools; } - + const grouped = {} as GroupedTools; filteredTools.forEach(({ item: [id, tool] }) => { const categoryId = tool.categoryId; @@ -105,11 +106,11 @@ export function useToolSections( if (!filteredTools || !Array.isArray(filteredTools)) { return []; } - + const subMap = {} as SubcategoryIdMap; - const seen = new Set(); + const seen = new Set(); filteredTools.forEach(({ item: [id, tool] }) => { - const toolId = id as string /* FIX ME: Should be ToolId */; + const toolId = id as ToolId; if (seen.has(toolId)) return; seen.add(toolId); const sub = tool.subcategoryId; diff --git a/frontend/src/utils/toolSearch.ts b/frontend/src/utils/toolSearch.ts index dda5749a8..8bd963564 100644 --- a/frontend/src/utils/toolSearch.ts +++ b/frontend/src/utils/toolSearch.ts @@ -1,8 +1,9 @@ +import { ToolId } from "src/types/toolId"; import { ToolRegistryEntry } from "../data/toolsTaxonomy"; import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch"; export interface RankedToolItem { - item: [string, ToolRegistryEntry]; + item: [ToolId, ToolRegistryEntry]; matchedText?: string; } @@ -10,18 +11,18 @@ export function filterToolRegistryByQuery( toolRegistry: Record, query: string ): RankedToolItem[] { - const entries = Object.entries(toolRegistry); + const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]; if (!query.trim()) { - return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); + return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] })); } const nq = normalizeForSearch(query); const threshold = minScoreForQuery(query); - const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = []; - const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = []; - const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = []; - const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = []; + const exactName: Array<{ id: ToolId; tool: ToolRegistryEntry; pos: number }> = []; + const exactSyn: Array<{ id: ToolId; tool: ToolRegistryEntry; text: string; pos: number }> = []; + const fuzzyName: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = []; + const fuzzySyn: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = []; for (const [id, tool] of entries) { const nameNorm = normalizeForSearch(tool.name || ''); @@ -78,7 +79,7 @@ export function filterToolRegistryByQuery( const seen = new Set(); const ordered: RankedToolItem[] = []; - const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => { + const push = (id: ToolId, tool: ToolRegistryEntry, matchedText?: string) => { if (seen.has(id)) return; seen.add(id); ordered.push({ item: [id, tool], matchedText }); @@ -92,7 +93,7 @@ export function filterToolRegistryByQuery( if (ordered.length > 0) return ordered; // Fallback: return everything unchanged - return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); + return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] })); }