mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-09 03:17:52 +00:00
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
This commit is contained in:
parent
0cc3c46831
commit
0020989ccb
2 changed files with 108 additions and 11 deletions
78
web/e2e/namespace-skill-pagination.spec.ts
Normal file
78
web/e2e/namespace-skill-pagination.spec.ts
Normal file
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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 ? (
|
||||
<SkeletonList count={6} />
|
||||
) : skillsData && skillsData.items.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{skillsData.items.map((skill, idx) => (
|
||||
<div key={skill.id} className={`animate-fade-up delay-${Math.min(idx + 1, 6)}`}>
|
||||
<SkillCard
|
||||
skill={skill}
|
||||
onClick={() => handleSkillClick(skill.slug)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{skillsData.items.map((skill, idx) => (
|
||||
<div key={skill.id} className={`animate-fade-up delay-${Math.min(idx + 1, 6)}`}>
|
||||
<SkillCard
|
||||
skill={skill}
|
||||
onClick={() => handleSkillClick(skill.slug)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{skillsData.total > PAGE_SIZE ? (
|
||||
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={t('namespace.emptyTitle')}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue