mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-10 03:27:54 +00:00
Merge pull request #354 from iflytek/fix/namespace-skill-pagination
fix(namespace): add pagination to namespace skill list
This commit is contained in:
commit
c71e4c197a
2 changed files with 134 additions and 11 deletions
104
web/e2e/namespace-skill-pagination.spec.ts
Normal file
104
web/e2e/namespace-skill-pagination.spec.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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()
|
||||
|
||||
// Build a fake skill list to simulate >20 skills without hitting rate limits.
|
||||
const fakeSkill = (i: number) => ({
|
||||
id: 90000 + i,
|
||||
namespace: namespace.slug,
|
||||
slug: `fake-skill-${i}`,
|
||||
displayName: `Fake Skill ${i}`,
|
||||
summary: `Pagination test skill ${i}`,
|
||||
downloadCount: 0,
|
||||
starCount: 0,
|
||||
ratingAvg: 0,
|
||||
ratingCount: 0,
|
||||
versions: [{ id: 90000 + i, version: '1.0.0', status: 'PUBLISHED' }],
|
||||
})
|
||||
|
||||
// Intercept the search API with a regex (glob '?' is ambiguous for literal '?').
|
||||
// Return fully mocked responses to avoid needing real published skills.
|
||||
await page.route(/\/api\/web\/skills\?/, async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const reqPage = Number(url.searchParams.get('page') ?? '0')
|
||||
const items = reqPage === 0
|
||||
? Array.from({ length: 20 }, (_, i) => fakeSkill(i))
|
||||
: [fakeSkill(20)]
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: { items, total: 21, page: reqPage, size: 20 },
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
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: Previous should be enabled
|
||||
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