perf(webui): make marketplace filters instant

This commit is contained in:
Xubin Ren 2026-07-28 18:36:38 +08:00
parent 12ebc7b8e2
commit d276fe1386
2 changed files with 35 additions and 9 deletions

View File

@ -53,11 +53,25 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
() => new Set(installedSkills.map((skill) => skill.name)), () => new Set(installedSkills.map((skill) => skill.name)),
[installedSkills], [installedSkills],
); );
const visibleTrending = useMemo(
() =>
provider === "all"
? trending
: trending.filter((skill) => skill.provider === provider),
[provider, trending],
);
const visibleResults = useMemo(
() =>
provider === "all"
? results
: results.filter((skill) => skill.provider === provider),
[provider, results],
);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
setTrendingLoading(true); setTrendingLoading(true);
fetchTrendingMarketplaceSkills(token, provider) fetchTrendingMarketplaceSkills(token)
.then((payload) => { .then((payload) => {
if (cancelled) return; if (cancelled) return;
setTrending(payload.skills); setTrending(payload.skills);
@ -71,7 +85,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [provider, token]); }, [token]);
useEffect(() => { useEffect(() => {
const skills = query.trim().length < 2 ? trending : results; const skills = query.trim().length < 2 ? trending : results;
@ -106,7 +120,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
setLoading(true); setLoading(true);
setError(""); setError("");
searchMarketplaceSkills(token, normalized, provider) searchMarketplaceSkills(token, normalized)
.then((payload) => { .then((payload) => {
if (cancelled) return; if (cancelled) return;
setResults(payload.skills); setResults(payload.skills);
@ -118,7 +132,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
reason instanceof Error reason instanceof Error
? reason.message ? reason.message
: t("settings.skills.marketplaceSearchFailed", { : t("settings.skills.marketplaceSearchFailed", {
defaultValue: "Could not search skills.sh.", defaultValue: "Could not search skill marketplaces.",
}), }),
); );
}) })
@ -131,7 +145,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
cancelled = true; cancelled = true;
window.clearTimeout(timer); window.clearTimeout(timer);
}; };
}, [provider, query, t, token]); }, [query, t, token]);
const install = async (skill: MarketplaceSkillSummary) => { const install = async (skill: MarketplaceSkillSummary) => {
setSelected(null); setSelected(null);
@ -238,9 +252,9 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
</div> </div>
{trendingLoading ? ( {trendingLoading ? (
<TrendingSkeleton /> <TrendingSkeleton />
) : trending.length ? ( ) : visibleTrending.length ? (
<MarketplaceSkillGroups <MarketplaceSkillGroups
skills={trending} skills={visibleTrending}
installedNames={installedNames} installedNames={installedNames}
installing={installing} installing={installing}
trends={trends} trends={trends}
@ -255,7 +269,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
</div> </div>
)} )}
</section> </section>
) : !loading && results.length === 0 && !error ? ( ) : !loading && visibleResults.length === 0 && !error ? (
<div className="rounded-[22px] bg-settings-surface px-5 py-12 text-center text-sm text-muted-foreground"> <div className="rounded-[22px] bg-settings-surface px-5 py-12 text-center text-sm text-muted-foreground">
{t("settings.skills.marketplaceEmpty", { {t("settings.skills.marketplaceEmpty", {
query: query.trim(), query: query.trim(),
@ -265,7 +279,7 @@ export function SkillsMarketplace({ installedSkills }: { installedSkills: SkillS
) : ( ) : (
<div className="overflow-hidden rounded-[22px] bg-settings-surface"> <div className="overflow-hidden rounded-[22px] bg-settings-surface">
<MarketplaceSkillGroups <MarketplaceSkillGroups
skills={results} skills={visibleResults}
installedNames={installedNames} installedNames={installedNames}
installing={installing} installing={installing}
trends={trends} trends={trends}

View File

@ -717,6 +717,18 @@ describe("App layout", () => {
expect(screen.getAllByText("SkillHub")).toHaveLength(2); expect(screen.getAllByText("SkillHub")).toHaveLength(2);
expect(screen.getAllByText("skills.sh")).toHaveLength(2); expect(screen.getAllByText("skills.sh")).toHaveLength(2);
expect(screen.getByText(/14,481 installs \/ 24h/)).toBeInTheDocument(); expect(screen.getByText(/14,481 installs \/ 24h/)).toBeInTheDocument();
fireEvent.click(screen.getByRole("tab", { name: "SkillHub" }));
expect(screen.getByText("ima-skills")).toBeInTheDocument();
expect(screen.queryByText("find-skills")).not.toBeInTheDocument();
expect(
vi.mocked(fetch).mock.calls.some(
([input]) =>
String(input) === "/api/webui/skills/trending?provider=skillhub",
),
).toBe(false);
fireEvent.click(screen.getByRole("tab", { name: "All" }));
expect(screen.getByText("find-skills")).toBeInTheDocument();
expect(screen.getByText("ima-skills")).toBeInTheDocument();
expect( expect(
await screen.findByRole("img", { name: "8-week install trend" }), await screen.findByRole("img", { name: "8-week install trend" }),
).toBeInTheDocument(); ).toBeInTheDocument();