From 0020989ccb642ab0a9f8042595f29db44ea8f94d Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 27 Apr 2026 10:54:57 +0800 Subject: [PATCH] fix(namespace): add pagination to namespace skill list The namespace detail page hardcoded a limit of 20 skills with no pagination controls, so namespaces with more than 20 skills appeared truncated. Add page state, wire it into the search query, and render the shared Pagination component when total exceeds PAGE_SIZE. Also reset page to 0 when the namespace param changes. Closes #350 --- web/e2e/namespace-skill-pagination.spec.ts | 78 ++++++++++++++++++++++ web/src/pages/namespace.tsx | 41 +++++++++--- 2 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 web/e2e/namespace-skill-pagination.spec.ts diff --git a/web/e2e/namespace-skill-pagination.spec.ts b/web/e2e/namespace-skill-pagination.spec.ts new file mode 100644 index 00000000..f3d492bb --- /dev/null +++ b/web/e2e/namespace-skill-pagination.spec.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' +import { registerSession } from './helpers/session' +import { E2eTestDataBuilder } from './helpers/test-data-builder' + +test.describe('Namespace Skill List Pagination (Real API)', () => { + test.beforeEach(async ({ page }, testInfo) => { + await setEnglishLocale(page) + await registerSession(page, testInfo) + }) + + test('shows namespace page with skills and no pagination when under 20 skills', async ({ page }, testInfo) => { + const builder = new E2eTestDataBuilder(page, testInfo) + await builder.init() + + try { + const namespace = await builder.ensureWritableNamespace() + await builder.publishSkill(namespace.slug) + + await page.goto(`/space/${namespace.slug}`) + + await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible() + await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible() + + // Verify pagination controls do not appear when there are fewer than 20 skills + await expect(page.getByRole('button', { name: 'Previous' })).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Next' })).toHaveCount(0) + } finally { + await builder.cleanup() + } + }) + + test('shows pagination controls when there are more than 20 skills', async ({ page }, testInfo) => { + const builder = new E2eTestDataBuilder(page, testInfo) + await builder.init() + + try { + const namespace = await builder.ensureWritableNamespace() + + // Publish 21 skills to trigger pagination (PAGE_SIZE = 20) + for (let i = 0; i < 21; i += 1) { + await builder.publishSkill(namespace.slug, { + name: `e2e-skill-${i}`, + description: `Test skill ${i} for pagination`, + }) + } + + await page.goto(`/space/${namespace.slug}`) + + await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible() + await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible() + + // Verify pagination controls appear + const previousButton = page.getByRole('button', { name: 'Previous' }).first() + const nextButton = page.getByRole('button', { name: 'Next' }).first() + + await expect(previousButton).toBeVisible() + await expect(nextButton).toBeVisible() + + // First page: Previous should be disabled, Next should be enabled + await expect(previousButton).toBeDisabled() + await expect(nextButton).toBeEnabled() + + // Navigate to second page + await nextButton.click() + + // Second page: both buttons should be enabled (or Previous enabled, Next disabled if only 2 pages) + await expect(previousButton).toBeEnabled() + + // Navigate back to first page + await previousButton.click() + await expect(previousButton).toBeDisabled() + await expect(nextButton).toBeEnabled() + } finally { + await builder.cleanup() + } + }) +}) diff --git a/web/src/pages/namespace.tsx b/web/src/pages/namespace.tsx index 6754e953..69ac0127 100644 --- a/web/src/pages/namespace.tsx +++ b/web/src/pages/namespace.tsx @@ -1,12 +1,16 @@ +import { useState, useEffect } from 'react' import { useNavigate, useParams } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { NamespaceHeader } from '@/features/namespace/namespace-header' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' import { EmptyState } from '@/shared/components/empty-state' +import { Pagination } from '@/shared/components/pagination' import { useSearchSkills } from '@/shared/hooks/use-skill-queries' import { useNamespaceDetail } from '@/shared/hooks/use-namespace-queries' +const PAGE_SIZE = 20 + /** * Public namespace page showing namespace metadata and the skills currently discoverable inside it. */ @@ -14,13 +18,22 @@ export function NamespacePage() { const { t } = useTranslation() const navigate = useNavigate() const { namespace } = useParams({ from: '/space/$namespace' }) + const [page, setPage] = useState(0) + + // Reset page when namespace changes + useEffect(() => { + setPage(0) + }, [namespace]) const { data: namespaceData, isLoading: isLoadingNamespace } = useNamespaceDetail(namespace) const { data: skillsData, isLoading: isLoadingSkills } = useSearchSkills({ namespace, - size: 20, + page, + size: PAGE_SIZE, }) + const totalPages = skillsData ? Math.max(Math.ceil(skillsData.total / skillsData.size), 1) : 1 + const handleSkillClick = (slug: string) => { navigate({ to: `/space/${namespace}/${encodeURIComponent(slug)}` }) } @@ -47,16 +60,22 @@ export function NamespacePage() { {isLoadingSkills ? ( ) : skillsData && skillsData.items.length > 0 ? ( -
- {skillsData.items.map((skill, idx) => ( -
- handleSkillClick(skill.slug)} - /> -
- ))} -
+ <> +
+ {skillsData.items.map((skill, idx) => ( +
+ handleSkillClick(skill.slug)} + /> +
+ ))} +
+ + {skillsData.total > PAGE_SIZE ? ( + + ) : null} + ) : (