Feature/v2/fuzzy tool search (#4482)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01
2025-09-25 16:32:30 +01:00
committed by GitHub
parent 1219cebd07
commit 21b1428ab5
16 changed files with 584 additions and 77 deletions
+37 -5
View File
@@ -27,12 +27,19 @@ export interface ToolSection {
subcategories: SubcategoryGroup[];
};
export function useToolSections(filteredTools: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry][]) {
export function useToolSections(
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
searchQuery?: string
) {
const { t } = useTranslation();
const groupedTools = useMemo(() => {
if (!filteredTools || !Array.isArray(filteredTools)) {
return {} as GroupedTools;
}
const grouped = {} as GroupedTools;
filteredTools.forEach(([id, tool]) => {
filteredTools.forEach(({ item: [id, tool] }) => {
const categoryId = tool.categoryId;
const subcategoryId = tool.subcategoryId;
if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap;
@@ -92,9 +99,13 @@ export function useToolSections(filteredTools: [string /* FIX ME: Should be Tool
}, [groupedTools]);
const searchGroups: SubcategoryGroup[] = useMemo(() => {
if (!filteredTools || !Array.isArray(filteredTools)) {
return [];
}
const subMap = {} as SubcategoryIdMap;
const seen = new Set<string /* FIX ME: Should be ToolId */>();
filteredTools.forEach(([id, tool]) => {
filteredTools.forEach(({ item: [id, tool] }) => {
const toolId = id as string /* FIX ME: Should be ToolId */;
if (seen.has(toolId)) return;
seen.add(toolId);
@@ -102,10 +113,31 @@ export function useToolSections(filteredTools: [string /* FIX ME: Should be Tool
if (!subMap[sub]) subMap[sub] = [];
subMap[sub].push({ id: toolId, tool });
});
return Object.entries(subMap)
const entries = Object.entries(subMap);
// If a search query is present, always order subcategories by first occurrence in
// the ranked filteredTools list so the top-ranked tools' subcategory appears first.
if (searchQuery && searchQuery.trim()) {
const order: SubcategoryId[] = [];
filteredTools.forEach(({ item: [_, tool] }) => {
const sc = tool.subcategoryId;
if (!order.includes(sc)) order.push(sc);
});
return entries
.sort(([a], [b]) => {
const ai = order.indexOf(a as SubcategoryId);
const bi = order.indexOf(b as SubcategoryId);
if (ai !== bi) return ai - bi;
return (a as SubcategoryId).localeCompare(b as SubcategoryId);
})
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
}
// No search: alphabetical subcategory ordering
return entries
.sort(([a], [b]) => a.localeCompare(b))
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
}, [filteredTools]);
}, [filteredTools, searchQuery]);
return { sections, searchGroups };
}