fix(frontend): bound namespace page queries

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-07-27 16:23:12 +08:00
parent 9de8a51d89
commit 18c9370318
5 changed files with 104 additions and 12 deletions

View file

@ -238,6 +238,36 @@ describe('namespaceApi.listMinePage', () => {
size: 25,
})
})
it('encodes namespace filters without issuing an unbounded request', async () => {
window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com' }
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
code: 0,
msg: 'ok',
data: { items: [], total: 0, page: 1, size: 20 },
timestamp: '2026-05-07T00:00:00Z',
requestId: 'req-filtered',
}),
})
vi.stubGlobal('fetch', fetchMock)
await namespaceApi.listMinePage({
page: 1,
size: 20,
status: 'ACTIVE',
q: 'team ai',
slug: 'team-ai',
roles: ['OWNER', 'ADMIN'],
})
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(
'https://api.example.com/api/web/me/namespaces/page?page=1&size=20&status=ACTIVE&q=team+ai&slug=team-ai&roles=OWNER&roles=ADMIN',
expect.objectContaining({ headers: expect.any(Headers) }),
)
})
})
describe('getDirectAuthRuntimeConfig', () => {

View file

@ -48,6 +48,15 @@ import type {
import { ApiError } from '@/shared/lib/api-error'
import i18n from '@/i18n/config'
export interface MyNamespacePageParams {
page?: number
size?: number
status?: 'ACTIVE' | 'FROZEN' | 'ARCHIVED'
q?: string
slug?: string
roles?: Array<'OWNER' | 'ADMIN' | 'MEMBER'>
}
/**
* Front-end API foundation for generated OpenAPI calls and hand-written convenience wrappers.
*
@ -643,10 +652,21 @@ export const namespaceApi = {
return fetchJson<ManagedNamespace[]>(`${WEB_API_PREFIX}/me/namespaces`)
},
async listMinePage(params: { page?: number; size?: number } = {}): Promise<PagedResponse<ManagedNamespace>> {
async listMinePage(params: MyNamespacePageParams = {}): Promise<PagedResponse<ManagedNamespace>> {
const page = params.page ?? 0
const size = params.size ?? 20
return fetchJson<PagedResponse<ManagedNamespace>>(`${WEB_API_PREFIX}/me/namespaces/page?page=${page}&size=${size}`)
const query = new URLSearchParams({ page: String(page), size: String(size) })
if (params.status) {
query.set('status', params.status)
}
if (params.q?.trim()) {
query.set('q', params.q.trim())
}
if (params.slug?.trim()) {
query.set('slug', normalizeNamespaceSlug(params.slug))
}
params.roles?.forEach((role) => query.append('roles', role))
return fetchJson<PagedResponse<ManagedNamespace>>(`${WEB_API_PREFIX}/me/namespaces/page?${query.toString()}`)
},
async getDetail(slug: string): Promise<Namespace> {

View file

@ -160,7 +160,7 @@ export function MyNamespacesPage() {
const canCreateNamespace = hasRole('SKILL_ADMIN') || hasRole('SUPER_ADMIN')
const [page, setPage] = useState(0)
const [pendingAction, setPendingAction] = useState<PendingNamespaceAction | null>(null)
const { data: namespacePage, isLoading } = useMyNamespacesPage(page, PAGE_SIZE)
const { data: namespacePage, isLoading } = useMyNamespacesPage({ page, size: PAGE_SIZE })
const freezeMutation = useFreezeNamespace()
const unfreezeMutation = useUnfreezeNamespace()
const archiveMutation = useArchiveNamespace()

View file

@ -59,14 +59,42 @@ describe('use-namespace-queries exports', () => {
}))
})
it('passes page and size to the paged my namespaces query', async () => {
it('passes bounded filters to a single paged my namespaces query', async () => {
const mod = await import('./use-namespace-queries')
mod.useMyNamespacesPage(3, 15)
mod.useMyNamespacesPage({
page: 3,
size: 15,
status: 'ACTIVE',
q: 'team',
slug: 'team-ai',
roles: ['OWNER', 'ADMIN'],
})
expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining({
queryKey: ['namespaces', 'my', { page: 3, size: 15 }],
queryKey: ['namespaces', 'my', {
page: 3,
size: 15,
status: 'ACTIVE',
q: 'team',
slug: 'team-ai',
roles: ['OWNER', 'ADMIN'],
}],
}))
const queryOptions = useQueryMock.mock.calls[useQueryMock.mock.calls.length - 1]?.[0]
listMinePageMock.mockResolvedValue({ items: [], total: 101, page: 3, size: 15 })
await queryOptions.queryFn()
expect(listMinePageMock).toHaveBeenCalledTimes(1)
expect(listMinePageMock).toHaveBeenCalledWith({
page: 3,
size: 15,
status: 'ACTIVE',
q: 'team',
slug: 'team-ai',
roles: ['OWNER', 'ADMIN'],
})
})
it('fetches every page for compatibility consumers instead of truncating after the first page', async () => {

View file

@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { Namespace, NamespaceMember, ManagedNamespace, CreateNamespaceRequest, NamespaceCandidateUser, NamespaceRole, BatchMemberResponse, PagedResponse } from '@/api/types'
import { namespaceApi } from '@/api/client'
import { namespaceApi, type MyNamespacePageParams } from '@/api/client'
import { replaceNamespaceMemberRole } from '@/shared/lib/namespace-member-cache'
import { shouldEnableNamespaceMemberCandidates } from './skill-query-helpers'
@ -25,8 +25,21 @@ async function getMyNamespaces(): Promise<ManagedNamespace[]> {
return namespaces
}
async function getMyNamespacesPage(page = 0, size = MY_NAMESPACES_PAGE_SIZE): Promise<PagedResponse<ManagedNamespace>> {
return namespaceApi.listMinePage({ page, size })
function normalizeMyNamespacePageParams(params: MyNamespacePageParams = {}): MyNamespacePageParams {
const q = params.q?.trim()
const slug = params.slug?.trim()
return {
page: params.page ?? 0,
size: params.size ?? MY_NAMESPACES_PAGE_SIZE,
...(params.status ? { status: params.status } : {}),
...(q ? { q } : {}),
...(slug ? { slug } : {}),
...(params.roles?.length ? { roles: [...params.roles] } : {}),
}
}
async function getMyNamespacesPage(params: MyNamespacePageParams): Promise<PagedResponse<ManagedNamespace>> {
return namespaceApi.listMinePage(params)
}
async function createNamespace(request: CreateNamespaceRequest): Promise<Namespace> {
@ -80,10 +93,11 @@ export function useMyNamespaces(enabled = true) {
})
}
export function useMyNamespacesPage(page = 0, size = MY_NAMESPACES_PAGE_SIZE, enabled = true) {
export function useMyNamespacesPage(params: MyNamespacePageParams = {}, enabled = true) {
const normalizedParams = normalizeMyNamespacePageParams(params)
return useQuery({
queryKey: ['namespaces', 'my', { page, size }],
queryFn: () => getMyNamespacesPage(page, size),
queryKey: ['namespaces', 'my', normalizedParams],
queryFn: () => getMyNamespacesPage(normalizedParams),
enabled,
})
}