Skip to content

Commit a1acda7

Browse files
committed
move utils into helpers
1 parent 49fcca3 commit a1acda7

4 files changed

Lines changed: 131 additions & 94 deletions

File tree

ui/goose2/src/features/skills/lib/skillsHelpers.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import type { SkillInfo } from "../api/skills";
2+
import type { SkillCategory, SkillViewInfo } from "./skillCategories";
3+
4+
export type SkillsFilter = "all" | "global" | `project:${string}`;
5+
6+
export interface SkillsSection {
7+
id: string;
8+
title: string;
9+
skills: SkillViewInfo[];
10+
}
211

312
// Mirrors crates/goose/src/skills/mod.rs::validate_skill_name.
413
// Keep in sync with the Rust rule.
@@ -49,6 +58,104 @@ export function compareSkillsByName(a: SkillInfo, b: SkillInfo) {
4958
);
5059
}
5160

61+
export function filterSkills(
62+
skills: SkillViewInfo[],
63+
filters: {
64+
search: string;
65+
activeFilter: SkillsFilter;
66+
selectedCategories: SkillCategory[];
67+
},
68+
getCategoryLabel: (category: SkillCategory) => string,
69+
): SkillViewInfo[] {
70+
const searchTerm = filters.search.trim().toLowerCase();
71+
return skills.filter((skill) => {
72+
const matchesSearch =
73+
searchTerm.length === 0 ||
74+
skill.name.toLowerCase().includes(searchTerm) ||
75+
skill.description.toLowerCase().includes(searchTerm) ||
76+
skill.sourceLabel.toLowerCase().includes(searchTerm) ||
77+
getCategoryLabel(skill.inferredCategory)
78+
.toLowerCase()
79+
.includes(searchTerm);
80+
81+
const matchesFilter =
82+
filters.activeFilter === "all"
83+
? true
84+
: filters.activeFilter === "global"
85+
? skill.sourceKind === "global"
86+
: skill.projectLinks.some(
87+
(project) => `project:${project.id}` === filters.activeFilter,
88+
);
89+
90+
const matchesCategory =
91+
filters.selectedCategories.length === 0 ||
92+
filters.selectedCategories.includes(skill.inferredCategory);
93+
94+
return matchesSearch && matchesFilter && matchesCategory;
95+
});
96+
}
97+
98+
export function groupSkills(
99+
filteredSkills: SkillViewInfo[],
100+
activeFilter: SkillsFilter,
101+
projectFilters: { id: string; name: string }[],
102+
labels: { personalTitle: string; projectsFallback: string },
103+
): SkillsSection[] {
104+
if (activeFilter === "global") {
105+
return [
106+
{
107+
id: "personal",
108+
title: labels.personalTitle,
109+
skills: [...filteredSkills].sort(compareSkillsByName),
110+
},
111+
];
112+
}
113+
114+
if (activeFilter.startsWith("project:")) {
115+
const projectId = activeFilter.slice("project:".length);
116+
const projectName =
117+
projectFilters.find((project) => project.id === projectId)?.name ??
118+
labels.projectsFallback;
119+
120+
return [
121+
{
122+
id: activeFilter,
123+
title: projectName,
124+
skills: [...filteredSkills].sort(compareSkillsByName),
125+
},
126+
];
127+
}
128+
129+
const personalSkills = filteredSkills
130+
.filter((skill) => skill.sourceKind === "global")
131+
.sort(compareSkillsByName);
132+
133+
const projectSections = projectFilters
134+
.map((project) => ({
135+
id: `project:${project.id}`,
136+
title: project.name,
137+
skills: filteredSkills
138+
.filter((skill) =>
139+
skill.projectLinks.some((link) => link.id === project.id),
140+
)
141+
.sort(compareSkillsByName),
142+
}))
143+
.filter((section) => section.skills.length > 0);
144+
145+
return [
146+
...(personalSkills.length > 0
147+
? [
148+
{
149+
id: "personal",
150+
title: labels.personalTitle,
151+
skills: personalSkills,
152+
},
153+
]
154+
: []),
155+
...projectSections,
156+
];
157+
}
158+
52159
export function downloadExport(json: string, filename: string) {
53160
const blob = new Blob([json], { type: "application/json" });
54161
const url = URL.createObjectURL(blob);

ui/goose2/src/features/skills/ui/SkillsListSections.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import {
77
} from "@/shared/ui/accordion";
88
import { Button } from "@/shared/ui/button";
99
import type { SkillViewInfo } from "../lib/skillCategories";
10-
11-
export interface SkillsSection {
12-
id: string;
13-
title: string;
14-
skills: SkillViewInfo[];
15-
}
10+
import type { SkillsSection } from "../lib/skillsHelpers";
1611

1712
interface SkillsListSectionsProps {
1813
sections: SkillsSection[];

ui/goose2/src/features/skills/ui/SkillsToolbar.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { Button } from "@/shared/ui/button";
55
import { FilterRow } from "@/shared/ui/page-shell";
66
import { SkillCategoryFilter } from "./SkillCategoryFilter";
77
import type { SkillCategory } from "../lib/skillCategories";
8-
9-
export type SkillsFilter = "all" | "global" | `project:${string}`;
8+
import type { SkillsFilter } from "../lib/skillsHelpers";
109

1110
interface SkillsToolbarProps {
1211
search: string;

ui/goose2/src/features/skills/ui/SkillsView.tsx

Lines changed: 22 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import { useSkillImportExport } from "../hooks/useSkillImportExport";
1010
import { SkillDetailPage } from "./SkillDetailPage";
1111
import { SkillsDialogs } from "./SkillsDialogs";
1212
import { SkillsEmptyState } from "./SkillsEmptyState";
13-
import { SkillsListSections, type SkillsSection } from "./SkillsListSections";
14-
import { SkillsToolbar, type SkillsFilter } from "./SkillsToolbar";
13+
import { SkillsListSections } from "./SkillsListSections";
14+
import { SkillsToolbar } from "./SkillsToolbar";
1515
import { hydrateProjectNames } from "../lib/projectHydration";
1616
import {
17-
compareSkillsByName,
17+
filterSkills,
18+
groupSkills,
1819
uniqueProjectFilters,
20+
type SkillsFilter,
1921
} from "../lib/skillsHelpers";
2022
import {
2123
deleteSkill,
@@ -106,90 +108,24 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
106108
);
107109
}, [categoryFilters]);
108110

109-
const filteredSkills = useMemo(() => {
110-
const searchTerm = search.trim().toLowerCase();
111-
return skills.filter((skill) => {
112-
const matchesSearch =
113-
searchTerm.length === 0 ||
114-
skill.name.toLowerCase().includes(searchTerm) ||
115-
skill.description.toLowerCase().includes(searchTerm) ||
116-
skill.sourceLabel.toLowerCase().includes(searchTerm) ||
117-
t(`view.categories.options.${skill.inferredCategory}`)
118-
.toLowerCase()
119-
.includes(searchTerm);
120-
121-
const matchesFilter =
122-
activeFilter === "all"
123-
? true
124-
: activeFilter === "global"
125-
? skill.sourceKind === "global"
126-
: skill.projectLinks.some(
127-
(project) => `project:${project.id}` === activeFilter,
128-
);
129-
130-
const matchesCategory =
131-
selectedCategories.length === 0 ||
132-
selectedCategories.includes(skill.inferredCategory);
133-
134-
return matchesSearch && matchesFilter && matchesCategory;
135-
});
136-
}, [activeFilter, search, selectedCategories, skills, t]);
137-
138-
const groupedSkills = useMemo<SkillsSection[]>(() => {
139-
if (activeFilter === "global") {
140-
return [
141-
{
142-
id: "personal",
143-
title: t("view.filtersGlobal"),
144-
skills: [...filteredSkills].sort(compareSkillsByName),
145-
},
146-
];
147-
}
148-
149-
if (activeFilter.startsWith("project:")) {
150-
const projectId = activeFilter.slice("project:".length);
151-
const projectName =
152-
projectFilters.find((project) => project.id === projectId)?.name ??
153-
t("view.projects");
154-
155-
return [
156-
{
157-
id: activeFilter,
158-
title: projectName,
159-
skills: [...filteredSkills].sort(compareSkillsByName),
160-
},
161-
];
162-
}
163-
164-
const personalSkills = filteredSkills
165-
.filter((skill) => skill.sourceKind === "global")
166-
.sort(compareSkillsByName);
167-
168-
const projectSections = projectFilters
169-
.map((project) => ({
170-
id: `project:${project.id}`,
171-
title: project.name,
172-
skills: filteredSkills
173-
.filter((skill) =>
174-
skill.projectLinks.some((link) => link.id === project.id),
175-
)
176-
.sort(compareSkillsByName),
177-
}))
178-
.filter((section) => section.skills.length > 0);
111+
const filteredSkills = useMemo(
112+
() =>
113+
filterSkills(
114+
skills,
115+
{ search, activeFilter, selectedCategories },
116+
(category) => t(`view.categories.options.${category}`),
117+
),
118+
[skills, search, activeFilter, selectedCategories, t],
119+
);
179120

180-
return [
181-
...(personalSkills.length > 0
182-
? [
183-
{
184-
id: "personal",
185-
title: t("view.filtersGlobal"),
186-
skills: personalSkills,
187-
},
188-
]
189-
: []),
190-
...projectSections,
191-
];
192-
}, [activeFilter, filteredSkills, projectFilters, t]);
121+
const groupedSkills = useMemo(
122+
() =>
123+
groupSkills(filteredSkills, activeFilter, projectFilters, {
124+
personalTitle: t("view.filtersGlobal"),
125+
projectsFallback: t("view.projects"),
126+
}),
127+
[filteredSkills, activeFilter, projectFilters, t],
128+
);
193129

194130
useEffect(() => {
195131
const nextIds = groupedSkills.map((section) => section.id);

0 commit comments

Comments
 (0)