From 7d9c0b0298c831cb9be93dda31a315f58e0b9071 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 22 Aug 2025 13:53:06 +0100 Subject: [PATCH] V2 Fix subcategory names in All Tools (and search) pane (#4252) # Description of Changes Because we used string typing for IDs and names, it was really easy to make mistakes where variables named like `subcategory` would be stored as an ID in one file, but then read assuming it's a name in another file. This PR changes the code to consistently use enum cases when referring to IDs of categories, subcategories, and tools (at least in as many places as I can find them, ~I had to add a `ToolId` enum for this work~ I originally added a `ToolId` type for this work, but it caused too many issues when merging with #4222 so I've pulled it back out for now). Making that change made it obvious where we were inconsistently passing IDs and reading them as names etc. allowing me to fix rendering issues in the All Tools pane, where the subcategory IDs were being rendered directly (instead of being translated) or where IDs were being translated into names, but were then being re-translated, causing warnings in the log. --- .../public/locales/en-GB/translation.json | 27 ++- .../src/components/shared/QuickAccessBar.tsx | 10 +- .../src/components/tools/SearchResults.tsx | 6 +- frontend/src/components/tools/ToolPicker.tsx | 26 +- .../tools/shared/SuggestedToolsSection.tsx | 2 +- .../tools/toolPicker/ToolButton.tsx | 6 +- frontend/src/contexts/ToolWorkflowContext.tsx | 4 +- frontend/src/data/toolsTaxonomy.ts | 142 +++++------ .../src/data/useTranslatedToolRegistry.tsx | 223 +++++++++--------- frontend/src/hooks/useSuggestedTools.ts | 20 +- frontend/src/hooks/useToolManagement.tsx | 2 +- frontend/src/hooks/useToolSections.ts | 101 +++++--- 12 files changed, 299 insertions(+), 270 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e09f874ac..0a03fd01a 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1925,18 +1925,23 @@ "noToolsFound": "No tools found", "allTools": "ALL TOOLS", "quickAccess": "QUICK ACCESS", + "categories": { + "standardTools": "Standard Tools", + "advancedTools": "Advanced Tools", + "recommendedTools": "Recommended Tools" + }, "subcategories": { - "Signing": "Signing", - "Document Security": "Document Security", - "Verification": "Verification", - "Document Review": "Document Review", - "Page Formatting": "Page Formatting", - "Extraction": "Extraction", - "Removal": "Removal", - "Automation": "Automation", - "General": "General", - "Advanced Formatting": "Advanced Formatting", - "Developer Tools": "Developer Tools" + "signing": "Signing", + "documentSecurity": "Document Security", + "verification": "Verification", + "documentReview": "Document Review", + "pageFormatting": "Page Formatting", + "extraction": "Extraction", + "removal": "Removal", + "automation": "Automation", + "general": "General", + "advancedFormatting": "Advanced Formatting", + "developerTools": "Developer Tools" } }, "quickAccess": { diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index bc041a923..80ef86c83 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -13,9 +13,9 @@ import { ButtonConfig } from '../../types/sidebar'; import './quickAccessBar/QuickAccessBar.css'; import AllToolsNavButton from './AllToolsNavButton'; import ActiveToolButton from "./quickAccessBar/ActiveToolButton"; -import { - isNavButtonActive, - getNavButtonStyle, +import { + isNavButtonActive, + getNavButtonStyle, getActiveNavButton, } from './quickAccessBar/QuickAccessBar'; @@ -39,7 +39,7 @@ const QuickAccessBar = forwardRef(({ openFilesModal(); }; - + const buttonConfigs: ButtonConfig[] = [ { id: 'read', @@ -226,4 +226,4 @@ const QuickAccessBar = forwardRef(({ ); }); -export default QuickAccessBar; \ No newline at end of file +export default QuickAccessBar; diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index 949bd2f64..6ecb64b7e 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { Box, Stack, Text } from '@mantine/core'; -import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy'; import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; import { useToolSections } from '../../hooks/useToolSections'; @@ -23,8 +23,8 @@ const SearchResults: React.FC = ({ filteredTools, onSelect } return ( {searchGroups.map(group => ( - - + + {group.tools.map(({ id, tool }) => ( void, showSubcategoryHeader: boolean = true ) => ( - + {showSubcategoryHeader && ( - + )} {subcategory.tools.map(({ id, tool }: { id: string; tool: any }) => ( @@ -69,11 +71,11 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const { sections: visibleSections } = useToolSections(filteredTools); const quickSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'quick'), + () => visibleSections.find(s => s.key === 'quick'), [visibleSections] ); const allSection = useMemo( - () => visibleSections.find(s => (s as any).key === 'all'), + () => visibleSections.find(s => s.key === 'all'), [visibleSections] ); @@ -120,7 +122,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa {searchGroups.length === 0 ? ( ) : ( - searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect)) + searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect)) )} ) : ( @@ -164,8 +166,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {quickSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, false) + {quickSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, false) )} @@ -210,8 +212,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa - {allSection?.subcategories.map(sc => - renderToolButtons(sc, selectedToolKey, onSelect, true) + {allSection?.subcategories.map(sc => + renderToolButtons(t, sc, selectedToolKey, onSelect, true) )} diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index b1f91fc39..da0418571 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -21,7 +21,7 @@ export function SuggestedToolsSection(): React.ReactElement { const IconComponent = tool.icon; return ( = ({ id, tool, isSelected, onSelect }) => { const handleClick = (id: string) => { if (tool.link) { - // Open external link in new tab + // Open external link in new tab window.open(tool.link, '_blank', 'noopener,noreferrer'); - return; + return; } // Normal tool selection onSelect(id); @@ -47,4 +47,4 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect ); }; -export default ToolButton; \ No newline at end of file +export default ToolButton; diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 4ca66e61c..1ee211312 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -72,7 +72,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { selectedToolKey: string | null; selectedTool: ToolRegistryEntry | null; toolRegistry: any; // From useToolManagement - + // UI Actions setSidebarsVisible: (visible: boolean) => void; setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; @@ -230,4 +230,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue { throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); } return context; -} \ No newline at end of file +} diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index a76752b87..2dff2b8e4 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -3,101 +3,101 @@ import React from 'react'; import { BaseToolProps } from '../types/tool'; export enum SubcategoryId { - SIGNING = 'signing', - DOCUMENT_SECURITY = 'documentSecurity', - VERIFICATION = 'verification', - DOCUMENT_REVIEW = 'documentReview', - PAGE_FORMATTING = 'pageFormatting', - EXTRACTION = 'extraction', - REMOVAL = 'removal', - AUTOMATION = 'automation', - GENERAL = 'general', - ADVANCED_FORMATTING = 'advancedFormatting', - DEVELOPER_TOOLS = 'developerTools' + SIGNING = 'signing', + DOCUMENT_SECURITY = 'documentSecurity', + VERIFICATION = 'verification', + DOCUMENT_REVIEW = 'documentReview', + PAGE_FORMATTING = 'pageFormatting', + EXTRACTION = 'extraction', + REMOVAL = 'removal', + AUTOMATION = 'automation', + GENERAL = 'general', + ADVANCED_FORMATTING = 'advancedFormatting', + DEVELOPER_TOOLS = 'developerTools' } -export enum ToolCategory { - STANDARD_TOOLS = 'Standard Tools', - ADVANCED_TOOLS = 'Advanced Tools', - RECOMMENDED_TOOLS = 'Recommended Tools' +export enum ToolCategoryId { + STANDARD_TOOLS = 'standardTools', + ADVANCED_TOOLS = 'advancedTools', + RECOMMENDED_TOOLS = 'recommendedTools' } export type ToolRegistryEntry = { - icon: React.ReactNode; - name: string; - component: React.ComponentType | null; - view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external'; - description: string; - category: ToolCategory; - subcategory: SubcategoryId; - maxFiles?: number; - supportedFormats?: string[]; - endpoints?: string[]; - link?: string; - type?: string; + icon: React.ReactNode; + name: string; + component: React.ComponentType | null; + view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external'; + description: string; + categoryId: ToolCategoryId; + subcategoryId: SubcategoryId; + maxFiles?: number; + supportedFormats?: string[]; + endpoints?: string[]; + link?: string; + type?: string; } -export type ToolRegistry = Record; +export type ToolRegistry = Record; export const SUBCATEGORY_ORDER: SubcategoryId[] = [ - SubcategoryId.SIGNING, - SubcategoryId.DOCUMENT_SECURITY, - SubcategoryId.VERIFICATION, - SubcategoryId.DOCUMENT_REVIEW, - SubcategoryId.PAGE_FORMATTING, - SubcategoryId.EXTRACTION, - SubcategoryId.REMOVAL, - SubcategoryId.AUTOMATION, - SubcategoryId.GENERAL, - SubcategoryId.ADVANCED_FORMATTING, - SubcategoryId.DEVELOPER_TOOLS, + SubcategoryId.SIGNING, + SubcategoryId.DOCUMENT_SECURITY, + SubcategoryId.VERIFICATION, + SubcategoryId.DOCUMENT_REVIEW, + SubcategoryId.PAGE_FORMATTING, + SubcategoryId.EXTRACTION, + SubcategoryId.REMOVAL, + SubcategoryId.AUTOMATION, + SubcategoryId.GENERAL, + SubcategoryId.ADVANCED_FORMATTING, + SubcategoryId.DEVELOPER_TOOLS, ]; export const SUBCATEGORY_COLOR_MAP: Record = { - [SubcategoryId.SIGNING]: '#FF7892', - [SubcategoryId.DOCUMENT_SECURITY]: '#FF7892', - [SubcategoryId.VERIFICATION]: '#1BB1D4', - [SubcategoryId.DOCUMENT_REVIEW]: '#48BD54', - [SubcategoryId.PAGE_FORMATTING]: '#7882FF', - [SubcategoryId.EXTRACTION]: '#1BB1D4', - [SubcategoryId.REMOVAL]: '#7882FF', - [SubcategoryId.AUTOMATION]: '#69DC95', - [SubcategoryId.GENERAL]: '#69DC95', - [SubcategoryId.ADVANCED_FORMATTING]: '#F55454', - [SubcategoryId.DEVELOPER_TOOLS]: '#F55454', + [SubcategoryId.SIGNING]: '#FF7892', + [SubcategoryId.DOCUMENT_SECURITY]: '#FF7892', + [SubcategoryId.VERIFICATION]: '#1BB1D4', + [SubcategoryId.DOCUMENT_REVIEW]: '#48BD54', + [SubcategoryId.PAGE_FORMATTING]: '#7882FF', + [SubcategoryId.EXTRACTION]: '#1BB1D4', + [SubcategoryId.REMOVAL]: '#7882FF', + [SubcategoryId.AUTOMATION]: '#69DC95', + [SubcategoryId.GENERAL]: '#69DC95', + [SubcategoryId.ADVANCED_FORMATTING]: '#F55454', + [SubcategoryId.DEVELOPER_TOOLS]: '#F55454', }; -export const getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF'; - +export const getCategoryLabel = (t: TFunction, id: ToolCategoryId): string => t(`toolPicker.categories.${id}`, id); export const getSubcategoryLabel = (t: TFunction, id: SubcategoryId): string => t(`toolPicker.subcategories.${id}`, id); +export const getSubcategoryColor = (subcategory: SubcategoryId): string => SUBCATEGORY_COLOR_MAP[subcategory] || '#7882FF'; export const getAllEndpoints = (registry: ToolRegistry): string[] => { - const lists: string[][] = []; - Object.values(registry).forEach(entry => { - if (entry.endpoints && entry.endpoints.length > 0) { - lists.push(entry.endpoints); - } - }); - return Array.from(new Set(lists.flat())); + const lists: string[][] = []; + Object.values(registry).forEach(entry => { + if (entry.endpoints && entry.endpoints.length > 0) { + lists.push(entry.endpoints); + } + }); + return Array.from(new Set(lists.flat())); }; 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); + const endpoints = new Set(); + Object.values(extensionToEndpoint).forEach(toEndpoints => { + Object.values(toEndpoints).forEach(endpoint => { + endpoints.add(endpoint); + }); + }); + return Array.from(endpoints); }; export const getAllApplicationEndpoints = ( - registry: ToolRegistry, - extensionToEndpoint?: Record> + registry: ToolRegistry, + extensionToEndpoint?: Record> ): string[] => { - const toolEp = getAllEndpoints(registry); - const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; - return Array.from(new Set([...toolEp, ...convEp])); + const toolEp = getAllEndpoints(registry); + const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; + return Array.from(new Set([...toolEp, ...convEp])); }; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index f27fbd5c7..0426a7bf6 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useTranslation } from 'react-i18next'; import SplitPdfPanel from "../tools/Split"; import CompressPdfPanel from "../tools/Compress"; @@ -8,7 +7,7 @@ import Sanitize from '../tools/Sanitize'; import AddPassword from '../tools/AddPassword'; import ChangePermissions from '../tools/ChangePermissions'; import RemovePassword from '../tools/RemovePassword'; -import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; +import { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; import SingleLargePage from '../tools/SingleLargePage'; @@ -30,8 +29,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "sign", description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.SIGNING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.SIGNING }, "sign": { icon: signature, @@ -39,8 +38,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "sign", description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.SIGNING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.SIGNING }, @@ -52,8 +51,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: AddPassword, view: "security", description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"] }, @@ -64,8 +63,8 @@ export function useFlatToolRegistry(): ToolRegistry { view: "format", maxFiles: -1, description: t("home.watermark.desc", "Add a custom watermark to your PDF document."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"] }, "add-stamp": { @@ -74,8 +73,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "sanitize": { icon: cleaning_services, @@ -83,8 +82,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: Sanitize, view: "security", maxFiles: -1, - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), endpoints: ["sanitize-pdf"] }, @@ -94,8 +93,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "unlock-pdf-forms": { icon: preview_off, @@ -103,8 +102,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: UnlockPdfForms, view: "security", description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"] }, @@ -114,8 +113,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "security", description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY }, "change-permissions": { icon: lock, @@ -123,8 +122,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: ChangePermissions, view: "security", description: t("home.changePermissions.desc", "Change document restrictions and permissions"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"] }, @@ -136,8 +135,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.VERIFICATION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.VERIFICATION }, "validate-pdf-signature": { icon: verified, @@ -145,8 +144,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "security", description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.VERIFICATION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.VERIFICATION }, @@ -158,8 +157,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "view", description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_REVIEW + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_REVIEW }, "change-metadata": { icon: assignment, @@ -167,8 +166,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_REVIEW + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.DOCUMENT_REVIEW }, // Page Formatting @@ -178,8 +177,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "rotate": { icon: rotate_right, @@ -187,8 +186,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.rotate.desc", "Easily rotate your PDFs."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "splitPdf": { icon: content_cut, @@ -196,8 +195,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: SplitPdfPanel, view: "split", description: t("home.split.desc", "Split PDFs into multiple documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "reorganize-pages": { icon: move_down, @@ -205,8 +204,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "pageEditor", description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "adjust-page-size-scale": { icon: crop_free, @@ -214,8 +213,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "addPageNumbers": { icon: 123, @@ -223,8 +222,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "multi-page-layout": { icon: dashboard, @@ -232,8 +231,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING }, "single-large-page": { icon: looks_one, @@ -241,8 +240,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: SingleLargePage, view: "format", description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"] }, @@ -252,8 +251,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.PAGE_FORMATTING, }, @@ -265,8 +264,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.EXTRACTION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.EXTRACTION }, "extract-images": { icon: filter, @@ -274,8 +273,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.extractImages.desc", "Extract images from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.EXTRACTION + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.EXTRACTION }, @@ -287,8 +286,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removePages.desc", "Remove specific pages from a PDF document"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-blank-pages": { icon: scan_delete, @@ -296,8 +295,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-annotations": { icon: thread_unread, @@ -305,8 +304,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "remove", description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-image": { icon: remove_selection, @@ -314,8 +313,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL }, "remove-password": { icon: lock_open_right, @@ -323,8 +322,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: RemovePassword, view: "security", description: t("home.removePassword.desc", "Remove password protection from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, @@ -335,8 +334,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: RemoveCertificateSign, view: "security", description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), - category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL, + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-certificate-sign"] }, @@ -350,8 +349,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-rename-pdf-file": { icon: match_word, @@ -359,8 +358,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-pages": { icon: split_scene_right, @@ -368,8 +367,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, "auto-split-by-size-count": { icon: content_cut, @@ -377,8 +376,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.AUTOMATION }, @@ -390,8 +389,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "repair": { icon: build, @@ -399,8 +398,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: Repair, view: "format", description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"] }, @@ -410,8 +409,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "overlay-pdfs": { icon: layers, @@ -419,8 +418,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "replace-and-invert-color": { icon: format_color_fill, @@ -428,8 +427,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "add-image": { icon: image, @@ -437,8 +436,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.addImage.desc", "Add images to PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "edit-table-of-contents": { icon: bookmark_add, @@ -446,8 +445,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, "scanner-effect": { icon: scanner, @@ -455,8 +454,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING }, @@ -468,8 +467,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "extract", description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS }, "dev-api": { icon: open_in_new, @@ -477,8 +476,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devApi.desc", "Link to API documentation"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" }, "dev-folder-scanning": { @@ -487,8 +486,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" }, "dev-sso-guide": { @@ -497,8 +496,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devSsoGuide.desc", "Link to SSO guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", }, "dev-airgapped": { @@ -507,8 +506,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "external", description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), - category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.DEVELOPER_TOOLS, + categoryId: ToolCategoryId.ADVANCED_TOOLS, + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Pro/#activation" }, @@ -520,8 +519,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "format", description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL }, "compress": { icon: zoom_in_map, @@ -529,8 +528,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: CompressPdfPanel, view: "compress", description: t("home.compress.desc", "Compress PDFs to reduce their file size."), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "convert": { @@ -539,8 +538,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: ConvertPanel, view: "convert", description: t("home.convert.desc", "Convert files to and from PDF format"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: [ "pdf-to-img", @@ -583,8 +582,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "merge", description: t("home.merge.desc", "Merge multiple PDFs into a single document"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "multi-tool": { @@ -593,8 +592,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "pageEditor", description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "ocr": { @@ -603,8 +602,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: OCRPanel, view: "convert", description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL, + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, maxFiles: -1 }, "redact": { @@ -613,8 +612,8 @@ export function useFlatToolRegistry(): ToolRegistry { component: null, view: "redact", description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), - category: ToolCategory.RECOMMENDED_TOOLS, - subcategory: SubcategoryId.GENERAL + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL }, }; diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index ae6d77035..effbe2397 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -9,7 +9,7 @@ import CropIcon from '@mui/icons-material/Crop'; import TextFieldsIcon from '@mui/icons-material/TextFields'; export interface SuggestedTool { - name: string; + id: string /* FIX ME: Should be ToolId */; title: string; icon: React.ComponentType; navigate: () => void; @@ -17,27 +17,27 @@ export interface SuggestedTool { const ALL_SUGGESTED_TOOLS: Omit[] = [ { - name: 'compress', + id: 'compress', title: 'Compress', icon: CompressIcon }, { - name: 'convert', + id: 'convert', title: 'Convert', icon: SwapHorizIcon }, { - name: 'sanitize', + id: 'sanitize', title: 'Sanitize', icon: CleaningServicesIcon }, { - name: 'split', + id: 'split', title: 'Split', icon: CropIcon }, { - name: 'ocr', + id: 'ocr', title: 'OCR', icon: TextFieldsIcon } @@ -48,12 +48,12 @@ export function useSuggestedTools(): SuggestedTool[] { return useMemo(() => { // Filter out the current tool - const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.name !== selectedToolKey); - + const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey); + // Add navigation function to each tool return filteredTools.map(tool => ({ ...tool, - navigate: () => handleToolSelect(tool.name) + navigate: () => handleToolSelect(tool.id) })); }, [selectedToolKey, handleToolSelect]); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index e991677a9..a4c481fcc 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -17,7 +17,7 @@ interface ToolManagementResult { export const useToolManagement = (): ToolManagementResult => { const { t } = useTranslation(); - const [selectedToolKey, setSelectedToolKey] = useState(null); + const [selectedToolKey, setSelectedToolKey] = useState(null); const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); // Build endpoints list from registry entries with fallback to legacy mapping diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index 4c1a0c05d..41762a8e1 100644 --- a/frontend/src/hooks/useToolSections.ts +++ b/frontend/src/hooks/useToolSections.ts @@ -1,65 +1,87 @@ import { useMemo } from 'react'; -import { SUBCATEGORY_ORDER, ToolCategory, ToolRegistryEntry } from '../data/toolsTaxonomy'; +import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy'; import { useTranslation } from 'react-i18next'; +type SubcategoryIdMap = { + [subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>; +} + type GroupedTools = { - [category: string]: { - [subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>; - }; + [categoryId in ToolCategoryId]: SubcategoryIdMap; }; -export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { +export interface SubcategoryGroup { + subcategoryId: SubcategoryId; + tools: { + id: string /* FIX ME: Should be ToolId */; + tool: ToolRegistryEntry; + }[]; +}; + +export type ToolSectionKey = 'quick' | 'all'; + +export interface ToolSection { + key: ToolSectionKey; + title: string; + subcategories: SubcategoryGroup[]; +}; + +export function useToolSections(filteredTools: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry][]) { const { t } = useTranslation(); const groupedTools = useMemo(() => { - const grouped: GroupedTools = {}; + const grouped = {} as GroupedTools; filteredTools.forEach(([id, tool]) => { - 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 }); + const categoryId = tool.categoryId; + const subcategoryId = tool.subcategoryId; + if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap; + if (!grouped[categoryId][subcategoryId]) grouped[categoryId][subcategoryId] = []; + grouped[categoryId][subcategoryId].push({ id, tool }); }); return grouped; }, [filteredTools]); - const sections = useMemo(() => { - const getOrderIndex = (name: string) => { - const idx = SUBCATEGORY_ORDER.indexOf(name as any); + const sections: ToolSection[] = useMemo(() => { + const getOrderIndex = (id: SubcategoryId) => { + const idx = SUBCATEGORY_ORDER.indexOf(id); return idx === -1 ? Number.MAX_SAFE_INTEGER : idx; }; - const quick: Record> = {}; - const all: Record> = {}; + const quick = {} as SubcategoryIdMap; + const all = {} as SubcategoryIdMap; - Object.entries(groupedTools).forEach(([origCat, subs]) => { - const upperCat = origCat.toUpperCase(); + Object.entries(groupedTools).forEach(([c, subs]) => { + const categoryId = c as ToolCategoryId; - Object.entries(subs).forEach(([sub, tools]) => { - if (!all[sub]) all[sub] = []; - all[sub].push(...tools); + Object.entries(subs).forEach(([s, tools]) => { + const subcategoryId = s as SubcategoryId; + if (!all[subcategoryId]) all[subcategoryId] = []; + all[subcategoryId].push(...tools); }); - if (upperCat === ToolCategory.RECOMMENDED_TOOLS.toUpperCase()) { - Object.entries(subs).forEach(([sub, tools]) => { - if (!quick[sub]) quick[sub] = []; - quick[sub].push(...tools); + if (categoryId === ToolCategoryId.RECOMMENDED_TOOLS) { + Object.entries(subs).forEach(([s, tools]) => { + const subcategoryId = s as SubcategoryId; + if (!quick[subcategoryId]) quick[subcategoryId] = []; + quick[subcategoryId].push(...tools); }); } }); - const sortSubs = (obj: Record>) => + const sortSubs = (obj: SubcategoryIdMap) => Object.entries(obj) .sort(([a], [b]) => { - const ai = getOrderIndex(a); - const bi = getOrderIndex(b); + const aId = a as SubcategoryId; + const bId = b as SubcategoryId; + const ai = getOrderIndex(aId); + const bi = getOrderIndex(bId); if (ai !== bi) return ai - bi; - return a.localeCompare(b); + return aId.localeCompare(bId); }) - .map(([subcategory, tools]) => ({ subcategory, tools })); + .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup)); - const built = [ + const built: ToolSection[] = [ { key: 'quick', title: t('toolPicker.quickAccess', 'QUICK ACCESS'), subcategories: sortSubs(quick) }, { key: 'all', title: t('toolPicker.allTools', 'ALL TOOLS'), subcategories: sortSubs(all) } ]; @@ -67,19 +89,20 @@ export function useToolSections(filteredTools: [string, ToolRegistryEntry][]) { return built.filter(section => section.subcategories.some(sc => sc.tools.length > 0)); }, [groupedTools]); - const searchGroups = useMemo(() => { - const subMap: Record> = {}; - const seen = new Set(); + const searchGroups: SubcategoryGroup[] = useMemo(() => { + const subMap = {} as SubcategoryIdMap; + const seen = new Set(); filteredTools.forEach(([id, tool]) => { - if (seen.has(id)) return; - seen.add(id); - const sub = tool.subcategory; + const toolId = id as string /* FIX ME: Should be ToolId */; + if (seen.has(toolId)) return; + seen.add(toolId); + const sub = tool.subcategoryId; if (!subMap[sub]) subMap[sub] = []; - subMap[sub].push({ id, tool }); + subMap[sub].push({ id: toolId, tool }); }); return Object.entries(subMap) .sort(([a], [b]) => a.localeCompare(b)) - .map(([subcategory, tools]) => ({ subcategory, tools })); + .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup)); }, [filteredTools]); return { sections, searchGroups };