fix(namespace): preserve review menu visibility

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-07-16 09:40:43 +08:00
parent 35e9ca588a
commit 669d341d51
6 changed files with 86 additions and 9 deletions

View file

@ -76,7 +76,7 @@ vi.mock('@/features/auth/use-auth', () => ({
const useMyNamespacesMock = vi.fn()
vi.mock('@/shared/hooks/use-namespace-queries', () => ({
useMyNamespaces: () => useMyNamespacesMock(),
useMyNamespaces: (enabled?: boolean) => useMyNamespacesMock(enabled),
}))
vi.mock('@/shared/components/dashboard-page-header', () => ({
@ -194,4 +194,13 @@ describe('ReviewsPage', () => {
expect(useReviewListMock).toHaveBeenCalled()
expect(useReviewListMock.mock.calls.every((call) => call[5] === false)).toBe(true)
})
it('skips namespace loading for super admins with global review access', () => {
hasRoleMock.mockImplementation((role: string) => role === 'SKILL_ADMIN' || role === 'USER_ADMIN' || role === 'SUPER_ADMIN')
userMock.platformRoles = ['SUPER_ADMIN']
renderToStaticMarkup(createElement(ReviewsPage))
expect(useMyNamespacesMock).toHaveBeenCalledWith(false)
})
})

View file

@ -40,7 +40,6 @@ export function ReviewsPage() {
const navigate = useNavigate()
const search = useSearch({ from: '/dashboard/reviews' })
const { hasRole, user } = useAuth()
const { data: myNamespaces, isLoading: isLoadingNamespaces } = useMyNamespaces()
const [pages, setPages] = useState<Record<ReviewStatus, number>>({
PENDING: 0,
APPROVED: 0,
@ -52,6 +51,7 @@ export function ReviewsPage() {
const isSkillAdmin = hasRole('SKILL_ADMIN') || hasRole('SUPER_ADMIN')
const isUserAdmin = hasRole('USER_ADMIN') || hasRole('SUPER_ADMIN')
const hasGlobalReviewAccess = canAccessGlobalReviewCenter(user?.platformRoles)
const { data: myNamespaces, isLoading: isLoadingNamespaces } = useMyNamespaces(!hasGlobalReviewAccess)
const namespaceReviewEntry = getPreferredNamespaceReviewEntry(myNamespaces)
const showTypeTabs = isSkillAdmin && isUserAdmin

View file

@ -1,10 +1,11 @@
import type { ReactNode } from 'react'
import type { ManagedNamespace } from '@/api/types'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as mod from './user-menu'
import { UserMenu } from './user-menu'
const useMyNamespacesMock = vi.hoisted(() => vi.fn(() => ({ data: [] })))
const useMyNamespacesMock = vi.hoisted(() => vi.fn(() => ({ data: [] as ManagedNamespace[] })))
vi.mock('react', async () => {
const actual = await vi.importActual<typeof import('react')>('react')
@ -112,7 +113,41 @@ describe('UserMenu security settings visibility', () => {
expect(html).not.toContain('user.menu.security')
})
it('does not fetch namespace memberships while rendering the global menu', () => {
it('shows reviews for namespace admins without platform review roles', () => {
useMyNamespacesMock.mockReturnValue({
data: [
{
id: 10,
slug: 'team-admin',
displayName: 'Team Admin',
type: 'TEAM',
status: 'ACTIVE',
immutable: false,
canFreeze: false,
canUnfreeze: false,
canArchive: false,
canRestore: false,
canDelete: false,
currentUserRole: 'ADMIN',
createdAt: '',
},
],
})
const html = renderToStaticMarkup(
<UserMenu
user={{
displayName: 'Namespace Admin',
platformRoles: ['USER'],
}}
/>,
)
expect(useMyNamespacesMock).toHaveBeenCalledWith(true)
expect(html).toContain('user.menu.reviews')
})
it('disables namespace membership loading while rendering the global menu for platform reviewers', () => {
renderToStaticMarkup(
<UserMenu
user={{
@ -122,6 +157,6 @@ describe('UserMenu security settings visibility', () => {
/>,
)
expect(useMyNamespacesMock).not.toHaveBeenCalled()
expect(useMyNamespacesMock).toHaveBeenCalledWith(false)
})
})

View file

@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'
import { Link } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { authApi } from '@/api/client'
import { buildGlobalReviewsPath, canAccessGlobalReviewCenter } from '@/features/review/review-paths'
import { useMyNamespaces } from '@/shared/hooks/use-namespace-queries'
import { buildGlobalReviewsPath, canAccessGlobalReviewCenter, canAccessReviewCenter } from '@/features/review/review-paths'
import { clearSessionScopedQueries } from '@/features/notification/notification-session'
import { canViewGovernanceCenter } from '@/shared/lib/governance-access'
import { cn } from '@/shared/lib/utils'
@ -35,7 +36,9 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) {
const isUserAdmin = hasRole('USER_ADMIN') || hasRole('SUPER_ADMIN')
const isAuditor = hasRole('AUDITOR') || hasRole('SUPER_ADMIN')
const isSuperAdmin = hasRole('SUPER_ADMIN')
const reviewCenterVisible = canAccessGlobalReviewCenter(user.platformRoles)
const hasGlobalReviewAccess = canAccessGlobalReviewCenter(user.platformRoles)
const { data: myNamespaces } = useMyNamespaces(!hasGlobalReviewAccess)
const reviewCenterVisible = canAccessReviewCenter(user.platformRoles, myNamespaces)
const canChangePassword = user.canChangePassword === true
const open = isHovered || isClickOpen

View file

@ -1,4 +1,18 @@
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const useQueryMock = vi.hoisted(() => vi.fn())
vi.mock('@tanstack/react-query', () => ({
useQuery: useQueryMock,
useMutation: vi.fn(),
useQueryClient: vi.fn(),
}))
vi.mock('@/api/client', () => ({
namespaceApi: {
listMine: vi.fn(),
},
}))
/**
* use-namespace-queries.ts exports React hooks that wrap @tanstack/react-query
@ -10,6 +24,10 @@ import { describe, expect, it } from 'vitest'
* Here we verify that all expected hooks are exported.
*/
describe('use-namespace-queries exports', () => {
beforeEach(() => {
useQueryMock.mockClear()
})
it('exports all expected hook functions', async () => {
const mod = await import('./use-namespace-queries')
expect(typeof mod.useMyNamespaces).toBe('function')
@ -25,4 +43,15 @@ describe('use-namespace-queries exports', () => {
expect(typeof mod.useArchiveNamespace).toBe('function')
expect(typeof mod.useRestoreNamespace).toBe('function')
})
it('passes the enabled flag to the my namespaces query', async () => {
const mod = await import('./use-namespace-queries')
mod.useMyNamespaces(false)
expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining({
queryKey: ['namespaces', 'my'],
enabled: false,
}))
})
})

View file

@ -51,10 +51,11 @@ function invalidateNamespaceQueries(queryClient: ReturnType<typeof useQueryClien
queryClient.invalidateQueries({ queryKey: ['reviews'] })
}
export function useMyNamespaces() {
export function useMyNamespaces(enabled = true) {
return useQuery({
queryKey: ['namespaces', 'my'],
queryFn: getMyNamespaces,
enabled,
})
}