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.
This commit is contained in:
James Brunton 2025-08-22 13:53:06 +01:00 committed by GitHub
parent 949ffa01ad
commit 7d9c0b0298
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 299 additions and 270 deletions

View File

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

View File

@ -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<HTMLDivElement>(({
openFilesModal();
};
const buttonConfigs: ButtonConfig[] = [
{
id: 'read',
@ -226,4 +226,4 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
);
});
export default QuickAccessBar;
export default QuickAccessBar;

View File

@ -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<SearchResultsProps> = ({ filteredTools, onSelect }
return (
<Stack p="sm" gap="xs">
{searchGroups.map(group => (
<Box key={group.subcategory} w="100%">
<SubcategoryHeader label={t(`toolPicker.subcategories.${group.subcategory}`, group.subcategory)} />
<Box key={group.subcategoryId} w="100%">
<SubcategoryHeader label={getSubcategoryLabel(t, group.subcategoryId)} />
<Stack gap="xs">
{group.tools.map(({ id, tool }) => (
<ToolButton

View File

@ -1,12 +1,13 @@
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
import { Box, Text, Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
import { getSubcategoryLabel, ToolRegistryEntry } from "../../data/toolsTaxonomy";
import ToolButton from "./toolPicker/ToolButton";
import "./toolPicker/ToolPicker.css";
import { useToolSections } from "../../hooks/useToolSections";
import { SubcategoryGroup, useToolSections } from "../../hooks/useToolSections";
import SubcategoryHeader from "./shared/SubcategoryHeader";
import NoToolsFound from "./shared/NoToolsFound";
import { TFunction } from "i18next";
interface ToolPickerProps {
selectedToolKey: string | null;
@ -17,14 +18,15 @@ interface ToolPickerProps {
// Helper function to render tool buttons for a subcategory
const renderToolButtons = (
subcategory: any,
t: TFunction,
subcategory: SubcategoryGroup,
selectedToolKey: string | null,
onSelect: (id: string) => void,
showSubcategoryHeader: boolean = true
) => (
<Box key={subcategory.subcategory} w="100%">
<Box key={subcategory.subcategoryId} w="100%">
{showSubcategoryHeader && (
<SubcategoryHeader label={subcategory.subcategory} />
<SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
)}
<Stack gap="xs">
{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 ? (
<NoToolsFound />
) : (
searchGroups.map(group => renderToolButtons(group, selectedToolKey, onSelect))
searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect))
)}
</Stack>
) : (
@ -164,8 +166,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
<Box ref={quickAccessRef} w="100%">
<Stack p="sm" gap="xs">
{quickSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, false)
{quickSection?.subcategories.map(sc =>
renderToolButtons(t, sc, selectedToolKey, onSelect, false)
)}
</Stack>
</Box>
@ -210,8 +212,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
<Box ref={allToolsRef} w="100%">
<Stack p="sm" gap="xs">
{allSection?.subcategories.map(sc =>
renderToolButtons(sc, selectedToolKey, onSelect, true)
{allSection?.subcategories.map(sc =>
renderToolButtons(t, sc, selectedToolKey, onSelect, true)
)}
</Stack>
</Box>

View File

@ -21,7 +21,7 @@ export function SuggestedToolsSection(): React.ReactElement {
const IconComponent = tool.icon;
return (
<Card
key={tool.name}
key={tool.id}
p="sm"
withBorder
style={{ cursor: 'pointer' }}

View File

@ -14,9 +14,9 @@ interface ToolButtonProps {
const ToolButton: React.FC<ToolButtonProps> = ({ 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<ToolButtonProps> = ({ id, tool, isSelected, onSelect
);
};
export default ToolButton;
export default ToolButton;

View File

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

View File

@ -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<BaseToolProps> | 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<BaseToolProps> | 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<string, ToolRegistryEntry>;
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>;
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, string> = {
[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, 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);
const endpoints = new Set<string>();
Object.values(extensionToEndpoint).forEach(toEndpoints => {
Object.values(toEndpoints).forEach(endpoint => {
endpoints.add(endpoint);
});
});
return Array.from(endpoints);
};
export const getAllApplicationEndpoints = (
registry: ToolRegistry,
extensionToEndpoint?: Record<string, Record<string, string>>
registry: ToolRegistry,
extensionToEndpoint?: Record<string, Record<string, string>>
): 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]));
};

View File

@ -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: <span className="material-symbols-rounded">signature</span>,
@ -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: <span className="material-symbols-rounded">cleaning_services</span>,
@ -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: <span className="material-symbols-rounded">preview_off</span>,
@ -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: <span className="material-symbols-rounded">lock</span>,
@ -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: <span className="material-symbols-rounded">verified</span>,
@ -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: <span className="material-symbols-rounded">assignment</span>,
@ -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: <span className="material-symbols-rounded">rotate_right</span>,
@ -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: <span className="material-symbols-rounded">content_cut</span>,
@ -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: <span className="material-symbols-rounded">move_down</span>,
@ -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: <span className="material-symbols-rounded">crop_free</span>,
@ -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: <span className="material-symbols-rounded">123</span>,
@ -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: <span className="material-symbols-rounded">dashboard</span>,
@ -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: <span className="material-symbols-rounded">looks_one</span>,
@ -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: <span className="material-symbols-rounded">filter</span>,
@ -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: <span className="material-symbols-rounded">scan_delete</span>,
@ -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: <span className="material-symbols-rounded">thread_unread</span>,
@ -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: <span className="material-symbols-rounded">remove_selection</span>,
@ -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: <span className="material-symbols-rounded">lock_open_right</span>,
@ -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: <span className="material-symbols-rounded">match_word</span>,
@ -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: <span className="material-symbols-rounded">split_scene_right</span>,
@ -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: <span className="material-symbols-rounded">content_cut</span>,
@ -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: <span className="material-symbols-rounded">build</span>,
@ -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: <span className="material-symbols-rounded">layers</span>,
@ -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: <span className="material-symbols-rounded">format_color_fill</span>,
@ -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: <span className="material-symbols-rounded">image</span>,
@ -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: <span className="material-symbols-rounded">bookmark_add</span>,
@ -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: <span className="material-symbols-rounded">scanner</span>,
@ -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: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
@ -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: <span className="material-symbols-rounded">zoom_in_map</span>,
@ -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
},
};

View File

@ -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<any>;
navigate: () => void;
@ -17,27 +17,27 @@ export interface SuggestedTool {
const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
{
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]);
}
}

View File

@ -17,7 +17,7 @@ interface ToolManagementResult {
export const useToolManagement = (): ToolManagementResult => {
const { t } = useTranslation();
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
const [selectedToolKey, setSelectedToolKey] = useState<string /* FIX ME: Should be ToolId */ | null>(null);
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
// Build endpoints list from registry entries with fallback to legacy mapping

View File

@ -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<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
const all: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
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<string, Array<{ id: string; tool: ToolRegistryEntry }>>) =>
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<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
const seen = new Set<string>();
const searchGroups: SubcategoryGroup[] = useMemo(() => {
const subMap = {} as SubcategoryIdMap;
const seen = new Set<string /* FIX ME: Should be ToolId */>();
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 };