fix(web): align publisher grouped All tab with catalog total (#2839)

* fix(web): align publisher grouped All tab with catalog total

The grouped All chip was summing only paginated items while the Skills/Plugins tab showed the publisher total, causing mismatches like Plugins 59 vs All 12 on large profiles.

* docs(proof): add post-fix publisher All tab screenshot
This commit is contained in:
Vyctor H. Brzezowski
2026-06-24 14:08:37 -07:00
committed by GitHub
parent 82197de7e1
commit cfb21978dd
4 changed files with 130 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+119 -1
View File
@@ -5,6 +5,7 @@ import type { ComponentType, ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
buildPublisherCatalogCategoryOptions,
buildPublisherGroupTabOptions,
formatRelativeUpdatedAt,
getCatalogItemShortTypeLabel,
groupPublisherCatalogItemsByTopic,
@@ -263,7 +264,7 @@ describe("user profile route", () => {
render(<Component />);
const groupTabs = screen.getByRole("radiogroup", { name: "Catalog groups" });
expect(within(groupTabs).getByRole("radio", { name: /all 3/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /all 136/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /gpu development 1/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /travel 1/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /uncategorized 1/i })).toBeTruthy();
@@ -282,6 +283,83 @@ describe("user profile route", () => {
expect(screen.queryByText("GPU Helper")).toBeNull();
expect(screen.queryByText("Orphan Helper")).toBeNull();
});
it("shows publisher total in grouped All tab while catalog is paginated", async () => {
const openclawPublisher = {
...publisher,
handle: "openclaw",
displayName: "OpenClaw",
stats: {
...publisher.stats,
skills: 6,
packages: 59,
},
};
loaderDataMock.mockReturnValue({ publisher: openclawPublisher });
queryMock.mockImplementation((_query, args: Record<string, unknown> | "skip") => {
if (args === "skip") return undefined;
if ("publisherHandle" in args) return { publisher: openclawPublisher, members: [] };
if ("kind" in args) return null;
return openclawPublisher;
});
paginatedQueryMock.mockReturnValue({
loadMore: vi.fn(),
results: [
{
_id: "packages:codex",
kind: "plugin",
displayName: "Codex",
summary: null,
topics: ["Codex"],
icon: null,
href: "/openclaw/plugins/codex",
installs: 1,
stars: 0,
isOfficial: true,
updatedAt: 1,
},
{
_id: "packages:diagnostics",
kind: "plugin",
displayName: "Diagnostics",
summary: null,
topics: ["Diagnostics"],
icon: null,
href: "/openclaw/plugins/diagnostics",
installs: 1,
stars: 0,
isOfficial: true,
updatedAt: 1,
},
...Array.from({ length: 10 }, (_, index) => ({
_id: `packages:plugin-${index}`,
kind: "plugin" as const,
displayName: `Plugin ${index}`,
summary: null,
topics: ["Feishu"],
icon: null,
href: `/openclaw/plugins/plugin-${index}`,
installs: 1,
stars: 0,
isOfficial: true,
updatedAt: 1,
})),
],
status: "CanLoadMore",
});
const route = await loadRoute();
const Component = route.__config.component as ComponentType;
render(<Component />);
fireEvent.click(screen.getByRole("button", { name: /plugins 59/i }));
const groupTabs = screen.getByRole("radiogroup", { name: "Catalog groups" });
expect(within(groupTabs).getByRole("radio", { name: /all 59/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /codex 1/i })).toBeTruthy();
expect(within(groupTabs).getByRole("radio", { name: /feishu 10/i })).toBeTruthy();
});
});
describe("publisher profile helpers", () => {
@@ -296,6 +374,46 @@ describe("publisher profile helpers", () => {
expect(resolveDefaultCatalogTab({ stats: { skills: 0, packages: 0 } } as never)).toBe("skills");
});
it("uses publisher total for grouped All tab when provided", () => {
const groups = groupPublisherCatalogItemsByTopic([
{
_id: "packages:a",
kind: "plugin",
displayName: "A",
summary: null,
topics: ["Codex"],
icon: null,
href: "/openclaw/plugins/a",
installs: 0,
stars: 0,
isOfficial: false,
updatedAt: 1,
},
{
_id: "packages:b",
kind: "plugin",
displayName: "B",
summary: null,
topics: ["Diagnostics"],
icon: null,
href: "/openclaw/plugins/b",
installs: 0,
stars: 0,
isOfficial: false,
updatedAt: 1,
},
]);
expect(
buildPublisherGroupTabOptions(groups).find((option) => option.value === "all")?.count,
).toBe("2");
expect(
buildPublisherGroupTabOptions(groups, { totalCount: 59 }).find(
(option) => option.value === "all",
)?.count,
).toBe("59");
});
it("parses publisher-scoped plugin routes from catalog hrefs", () => {
expect(
parsePluginCatalogRoute({
+11 -4
View File
@@ -865,6 +865,7 @@ export function PublisherProfilePage({
groups={catalogGroups}
selectedGroup={selectedCatalogGroup}
onSelectedGroupChange={setSelectedCatalogGroup}
totalCount={catalogSearch.trim() ? undefined : catalogCount}
footer={
showCatalogLoadMore ? (
<div className="publisher-profile-load-more">
@@ -1026,10 +1027,14 @@ export function groupPublisherCatalogItemsByTopic(
});
}
export function buildPublisherGroupTabOptions(groups: PublisherCatalogGroup[]) {
const totalCount = groups.reduce((sum, group) => sum + group.items.length, 0);
export function buildPublisherGroupTabOptions(
groups: PublisherCatalogGroup[],
options?: { totalCount?: number },
) {
const loadedCount = groups.reduce((sum, group) => sum + group.items.length, 0);
const allCount = options?.totalCount ?? loadedCount;
return [
{ value: "all", label: "All", count: formatCatalogTabCount(totalCount) },
{ value: "all", label: "All", count: formatCatalogTabCount(allCount) },
...groups.map((group) => ({
value: group.key,
label: group.title,
@@ -1086,15 +1091,17 @@ export function PublisherGroupedCatalog({
selectedGroup,
onSelectedGroupChange,
footer,
totalCount,
}: {
groups: PublisherCatalogGroup[];
selectedGroup: string;
onSelectedGroupChange: (value: string) => void;
footer?: ReactNode;
totalCount?: number;
}) {
const activeGroup =
selectedGroup === "all" ? null : (groups.find((group) => group.key === selectedGroup) ?? null);
const groupTabOptions = buildPublisherGroupTabOptions(groups);
const groupTabOptions = buildPublisherGroupTabOptions(groups, { totalCount });
return (
<div className="publisher-profile-grouped-catalog">