diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalQueryAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalQueryAppService.java index 5a0767dc..c7841d60 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalQueryAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalQueryAppService.java @@ -174,7 +174,7 @@ public class NamespacePortalQueryAppService { Map namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of(); Set requestedRoles = roles != null ? roles : Set.of(); Pageable boundedPageable = normalizeMyNamespacesPageable(pageable); - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = normalizeSearchFilter(query); String normalizedSlug = normalizeFilter(slug); if (isSuperAdmin(platformRoles) && requestedRoles.isEmpty()) { @@ -304,6 +304,17 @@ public class NamespacePortalQueryAppService { return value.trim(); } + private String normalizeSearchFilter(String value) { + String normalized = normalizeFilter(value); + if (normalized == null) { + return null; + } + return normalized + .replace("!", "!!") + .replace("%", "!%") + .replace("_", "!_"); + } + private boolean isSuperAdmin(Set platformRoles) { return platformRoles != null && platformRoles.contains(SUPER_ADMIN_ROLE); } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepositoryTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepositoryTest.java new file mode 100644 index 00000000..050bdbcc --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepositoryTest.java @@ -0,0 +1,67 @@ +package com.iflytek.skillhub.infra.jpa; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.iflytek.skillhub.domain.namespace.Namespace; +import com.iflytek.skillhub.domain.namespace.NamespaceStatus; +import jakarta.persistence.EntityManager; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; + +@DataJpaTest +@ActiveProfiles("test") +class NamespaceJpaRepositoryTest { + + @Autowired + private NamespaceJpaRepository repository; + + @Autowired + private EntityManager entityManager; + + private Namespace percentNamespace; + private Namespace underscoreNamespace; + + @BeforeEach + void setUp() { + percentNamespace = persist(new Namespace("percent-team", "50% Tools", "owner-1")); + underscoreNamespace = persist(new Namespace("underscore-team", "50_Tools", "owner-1")); + persist(new Namespace("plain-team", "Plain Tools", "owner-1")); + Namespace archived = new Namespace("archived-percent", "50% Archived", "owner-1"); + archived.setStatus(NamespaceStatus.ARCHIVED); + persist(archived); + entityManager.flush(); + } + + @Test + void search_treatsEscapedWildcardsLiterallyAndAppliesStatus() { + var percentPage = repository.search( + NamespaceStatus.ACTIVE, + "!%", + null, + PageRequest.of(0, 10) + ); + var underscorePage = repository.searchByIdIn( + List.of(percentNamespace.getId(), underscoreNamespace.getId()), + NamespaceStatus.ACTIVE, + "!_", + null, + PageRequest.of(0, 10) + ); + + assertThat(percentPage.getContent()).extracting(Namespace::getSlug) + .containsExactly("percent-team"); + assertThat(percentPage.getTotalElements()).isEqualTo(1); + assertThat(underscorePage.getContent()).extracting(Namespace::getSlug) + .containsExactly("underscore-team"); + } + + private Namespace persist(Namespace namespace) { + entityManager.persist(namespace); + return namespace; + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalQueryAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalQueryAppServiceTest.java index 96f4976b..2253a77f 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalQueryAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalQueryAppServiceTest.java @@ -215,6 +215,34 @@ class NamespacePortalQueryAppServiceTest { verify(namespaceRepository, never()).searchByIdIn(anyList(), any(), any(), any(), any()); } + @Test + void listMyNamespaces_escapesLikeWildcardsForLiteralSubstringSearch() { + Pageable expectedPageable = PageRequest.of(0, 20); + when(namespaceRepository.search( + eq(null), + eq("50!%!_!!off"), + eq(null), + any(Pageable.class) + )).thenReturn(new PageImpl<>(List.of(), expectedPageable, 0)); + + service.listMyNamespaces( + expectedPageable, + Map.of(), + Set.of("SUPER_ADMIN"), + null, + " 50%_!off ", + null, + Set.of() + ); + + verify(namespaceRepository).search( + eq(null), + eq("50!%!_!!off"), + eq(null), + any(Pageable.class) + ); + } + @Test void listMyNamespaces_nonSuperAdminWithoutRequestedRolesSearchesAllMembershipIds() { Namespace member = namespace(1L, "member-team"); diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java index ce918104..99e243ba 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java @@ -30,8 +30,8 @@ public interface NamespaceJpaRepository WHERE (:status IS NULL OR n.status = :status) AND ( :query IS NULL - OR lower(n.slug) LIKE lower(concat('%', :query, '%')) - OR lower(n.displayName) LIKE lower(concat('%', :query, '%')) + OR lower(n.slug) LIKE lower(concat('%', :query, '%')) ESCAPE '!' + OR lower(n.displayName) LIKE lower(concat('%', :query, '%')) ESCAPE '!' ) AND (:slug IS NULL OR n.slug = :slug) """) @@ -48,8 +48,8 @@ public interface NamespaceJpaRepository AND (:status IS NULL OR n.status = :status) AND ( :query IS NULL - OR lower(n.slug) LIKE lower(concat('%', :query, '%')) - OR lower(n.displayName) LIKE lower(concat('%', :query, '%')) + OR lower(n.slug) LIKE lower(concat('%', :query, '%')) ESCAPE '!' + OR lower(n.displayName) LIKE lower(concat('%', :query, '%')) ESCAPE '!' ) AND (:slug IS NULL OR n.slug = :slug) """) diff --git a/web/e2e/my-namespaces-super-admin-actions.spec.ts b/web/e2e/my-namespaces-super-admin-actions.spec.ts index 904be64a..ff0679d1 100644 --- a/web/e2e/my-namespaces-super-admin-actions.spec.ts +++ b/web/e2e/my-namespaces-super-admin-actions.spec.ts @@ -89,6 +89,10 @@ test.describe('My Namespaces super admin actions (Real API)', () => { name: `${namespace.displayName} (@${namespace.slug})`, })).toHaveCount(0) + await page.goto(`/dashboard/publish?namespace=${encodeURIComponent(namespace.slug)}&visibility=PUBLIC`) + await expect(page.getByRole('button', { name: `@${namespace.slug}`, exact: true })).toBeVisible() + await expect(page.getByText('The selected namespace is not active or is no longer available.')).toBeVisible() + await page.goto('/dashboard/namespaces') const namespaceCard = page.getByTestId(`namespace-card-${namespace.slug}`) diff --git a/web/src/features/review/use-namespace-review-entry.test.ts b/web/src/features/review/use-namespace-review-entry.test.ts new file mode 100644 index 00000000..97aaf380 --- /dev/null +++ b/web/src/features/review/use-namespace-review-entry.test.ts @@ -0,0 +1,95 @@ +import type { ManagedNamespace } from '@/api/types' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const useMyNamespacesPageMock = vi.hoisted(() => vi.fn()) + +vi.mock('@/shared/hooks/use-namespace-queries', () => ({ + useMyNamespacesPage: (...args: unknown[]) => useMyNamespacesPageMock(...args), +})) + +import { useNamespaceReviewEntry } from './use-namespace-review-entry' + +function namespace(slug: string, status: ManagedNamespace['status']): ManagedNamespace { + return { + id: slug.length, + slug, + displayName: slug, + type: 'TEAM', + status, + immutable: false, + canFreeze: false, + canUnfreeze: false, + canArchive: false, + canRestore: false, + canDelete: false, + currentUserRole: 'ADMIN', + createdAt: '', + } +} + +describe('useNamespaceReviewEntry', () => { + beforeEach(() => { + useMyNamespacesPageMock.mockReset() + }) + + it('uses one bounded ACTIVE query when an active review namespace exists', () => { + const active = namespace('zeta-active', 'ACTIVE') + useMyNamespacesPageMock.mockImplementation((params: { status?: string }) => ({ + data: { + items: params.status === 'ACTIVE' ? [active] : [namespace('alpha-archived', 'ARCHIVED')], + total: 1, + page: 0, + size: 1, + }, + isLoading: false, + error: null, + })) + + const result = useNamespaceReviewEntry(false) + + expect(useMyNamespacesPageMock).toHaveBeenNthCalledWith(1, { + page: 0, + size: 1, + status: 'ACTIVE', + roles: ['OWNER', 'ADMIN'], + }, true) + expect(useMyNamespacesPageMock).toHaveBeenNthCalledWith(2, { + page: 0, + size: 1, + roles: ['OWNER', 'ADMIN'], + }, false) + expect(result.namespaceReviewEntry?.slug).toBe('zeta-active') + }) + + it('falls back to one bounded any-status query when no ACTIVE namespace exists', () => { + const archived = namespace('alpha-archived', 'ARCHIVED') + useMyNamespacesPageMock.mockImplementation((params: { status?: string }) => ({ + data: { + items: params.status === 'ACTIVE' ? [] : [archived], + total: params.status === 'ACTIVE' ? 0 : 1, + page: 0, + size: 1, + }, + isLoading: false, + error: null, + })) + + const result = useNamespaceReviewEntry(false) + + expect(useMyNamespacesPageMock).toHaveBeenNthCalledWith(2, { + page: 0, + size: 1, + roles: ['OWNER', 'ADMIN'], + }, true) + expect(result.namespaceReviewEntry?.slug).toBe('alpha-archived') + }) + + it('disables both namespace queries for global reviewers', () => { + useMyNamespacesPageMock.mockReturnValue({ data: undefined, isLoading: false, error: null }) + + const result = useNamespaceReviewEntry(true) + + expect(useMyNamespacesPageMock.mock.calls.every((call) => call[1] === false)).toBe(true) + expect(result.namespaceReviewEntry).toBeNull() + }) +}) diff --git a/web/src/features/review/use-namespace-review-entry.ts b/web/src/features/review/use-namespace-review-entry.ts new file mode 100644 index 00000000..a525cfcc --- /dev/null +++ b/web/src/features/review/use-namespace-review-entry.ts @@ -0,0 +1,38 @@ +import { useMyNamespacesPage } from '@/shared/hooks/use-namespace-queries' +import { getPreferredNamespaceReviewEntry } from './review-paths' + +const REVIEW_ROLES = ['OWNER', 'ADMIN'] as const + +/** + * Resolves a review namespace with at most two one-row requests: an ACTIVE + * namespace first, then any manageable namespace as a read-only fallback. + */ +export function useNamespaceReviewEntry(hasGlobalReviewAccess: boolean) { + const activeQuery = useMyNamespacesPage({ + page: 0, + size: 1, + status: 'ACTIVE', + roles: [...REVIEW_ROLES], + }, !hasGlobalReviewAccess) + const activeEntry = getPreferredNamespaceReviewEntry(activeQuery.data?.items) + const fallbackEnabled = !hasGlobalReviewAccess + && !activeQuery.isLoading + && !activeQuery.error + && activeQuery.data !== undefined + && activeEntry === null + const fallbackQuery = useMyNamespacesPage({ + page: 0, + size: 1, + roles: [...REVIEW_ROLES], + }, fallbackEnabled) + const fallbackEntry = fallbackEnabled + ? getPreferredNamespaceReviewEntry(fallbackQuery.data?.items) + : null + + return { + namespaceReviewEntry: activeEntry ?? fallbackEntry, + isLoadingNamespaces: !hasGlobalReviewAccess + && (activeQuery.isLoading || (fallbackEnabled && fallbackQuery.isLoading)), + hasNamespaceQueryError: Boolean(activeQuery.error || (fallbackEnabled && fallbackQuery.error)), + } +} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index c5123d06..49d23b47 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1364,6 +1364,7 @@ }, "namespace": "Namespace", "selectNamespace": "Select namespace", + "namespaceUnavailable": "The selected namespace is not active or is no longer available.", "visibility": "Visibility", "visibilityOptions": { "public": "Public", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 31b36c2b..e95dfc26 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1365,6 +1365,7 @@ }, "namespace": "命名空间", "selectNamespace": "选择命名空间", + "namespaceUnavailable": "所选命名空间未启用或已不可用。", "visibility": "可见性", "visibilityOptions": { "public": "公开", diff --git a/web/src/pages/dashboard/publish.test.ts b/web/src/pages/dashboard/publish.test.ts index b39f7ac4..550d8cf8 100644 --- a/web/src/pages/dashboard/publish.test.ts +++ b/web/src/pages/dashboard/publish.test.ts @@ -77,12 +77,23 @@ describe('PublishPage', () => { beforeEach(() => { selectRecords.length = 0 useMyNamespacesPageMock.mockReset() - useMyNamespacesPageMock.mockReturnValue({ - data: { items: [], total: 0, page: 0, size: 20 }, + useMyNamespacesPageMock.mockImplementation((params: { slug?: string }) => ({ + data: { + items: params.slug ? [{ + id: 1, + slug: params.slug, + displayName: 'Team AI', + status: 'ACTIVE', + type: 'TEAM', + }] : [], + total: params.slug ? 1 : 0, + page: 0, + size: params.slug ? 1 : 20, + }, isLoading: false, error: null, refetch: vi.fn(), - }) + })) useSearchMock.mockReturnValue({ namespace: ' team-ai ', visibility: 'private', @@ -92,7 +103,12 @@ describe('PublishPage', () => { it('prefills namespace and visibility from route search params', () => { renderToStaticMarkup(createElement(PublishPage)) - expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1, slug: 'team-ai' }, true) + expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ + page: 0, + size: 1, + status: 'ACTIVE', + slug: 'team-ai', + }, true) expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 20, status: 'ACTIVE' }, false) expect(selectRecords[0]?.value).toBe('PRIVATE') }) @@ -102,10 +118,23 @@ describe('PublishPage', () => { renderToStaticMarkup(createElement(PublishPage)) - expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1 }, false) + expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1, status: 'ACTIVE' }, false) expect(selectRecords[0]?.value).toBe('PUBLIC') }) + it('marks an archived or unavailable prefilled namespace as invalid', () => { + useMyNamespacesPageMock.mockReturnValue({ + data: { items: [], total: 0, page: 0, size: 1 }, + isLoading: false, + error: null, + refetch: vi.fn(), + }) + + const html = renderToStaticMarkup(createElement(PublishPage)) + + expect(html).toContain('publish.namespaceUnavailable') + }) + it('exports a named component function', () => { expect(typeof PublishPage).toBe('function') }) diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index cafc316f..4f0d8e63 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -39,9 +39,10 @@ export function PublishPage() { const [warningDialogOpen, setWarningDialogOpen] = useState(false) const [precheckWarnings, setPrecheckWarnings] = useState([]) - const { data: selectedNamespacePage } = useMyNamespacesPage({ + const { data: selectedNamespacePage, isLoading: isLoadingSelectedNamespace } = useMyNamespacesPage({ page: 0, size: 1, + status: 'ACTIVE', ...(namespaceSlug ? { slug: namespaceSlug } : {}), }, !!namespaceSlug) const publishMutation = usePublishSkill() @@ -68,7 +69,7 @@ export function PublishPage() { } const publishSkill = async (confirmWarnings = false) => { - if (!selectedFile || !namespaceSlug) { + if (!selectedFile || !namespaceSlug || !selectedNamespace) { toast.error(t('publish.selectRequired')) return } @@ -164,6 +165,9 @@ export function PublishPage() { status="ACTIVE" disabled={publishMutation.isPending} /> + {namespaceSlug && !isLoadingSelectedNamespace && !selectedNamespace ? ( +

{t('publish.namespaceUnavailable')}

+ ) : null}
@@ -214,7 +218,7 @@ export function PublishPage() { className="w-full text-primary-foreground disabled:text-primary-foreground" size="lg" onClick={handlePublish} - disabled={!selectedFile || !namespaceSlug || publishMutation.isPending} + disabled={!selectedFile || !namespaceSlug || !selectedNamespace || publishMutation.isPending} > {publishMutation.isPending ? t('publish.publishing') : t('publish.confirm')} diff --git a/web/src/pages/dashboard/reviews.test.ts b/web/src/pages/dashboard/reviews.test.ts index dd99bca6..aba65e8e 100644 --- a/web/src/pages/dashboard/reviews.test.ts +++ b/web/src/pages/dashboard/reviews.test.ts @@ -204,6 +204,7 @@ describe('ReviewsPage', () => { expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1, + status: 'ACTIVE', roles: ['OWNER', 'ADMIN'], }, false) }) diff --git a/web/src/pages/dashboard/reviews.tsx b/web/src/pages/dashboard/reviews.tsx index e0fe3892..8f6de4d7 100644 --- a/web/src/pages/dashboard/reviews.tsx +++ b/web/src/pages/dashboard/reviews.tsx @@ -2,15 +2,14 @@ import { useEffect, useState } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { FileCheck2 } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { useMyNamespacesPage } from '@/shared/hooks/use-namespace-queries' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' import { buildNamespaceReviewsPath, canAccessGlobalReviewCenter, - getPreferredNamespaceReviewEntry, } from '@/features/review/review-paths' +import { useNamespaceReviewEntry } from '@/features/review/use-namespace-review-entry' import { Table, TableBody, @@ -51,12 +50,11 @@ 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: myNamespacesPage, isLoading: isLoadingNamespaces } = useMyNamespacesPage({ - page: 0, - size: 1, - roles: ['OWNER', 'ADMIN'], - }, !hasGlobalReviewAccess) - const namespaceReviewEntry = getPreferredNamespaceReviewEntry(myNamespacesPage?.items) + const { + namespaceReviewEntry, + isLoadingNamespaces, + hasNamespaceQueryError, + } = useNamespaceReviewEntry(hasGlobalReviewAccess) const showTypeTabs = isSkillAdmin && isUserAdmin // Determine default top-level tab @@ -66,7 +64,7 @@ export function ReviewsPage() { const skillReviewEnabled = hasGlobalReviewAccess && isSkillAdmin && activeType === 'skill' useEffect(() => { - if (hasGlobalReviewAccess || isLoadingNamespaces) { + if (hasGlobalReviewAccess || isLoadingNamespaces || hasNamespaceQueryError) { return } @@ -76,7 +74,7 @@ export function ReviewsPage() { } void navigate({ to: '/dashboard', replace: true }) - }, [hasGlobalReviewAccess, isLoadingNamespaces, namespaceReviewEntry, navigate]) + }, [hasGlobalReviewAccess, hasNamespaceQueryError, isLoadingNamespaces, namespaceReviewEntry, navigate]) const pendingQuery = useReviewList('PENDING', undefined, pages.PENDING, PAGE_SIZE, sortDirection, skillReviewEnabled && activeStatus === 'PENDING') const approvedQuery = useReviewList('APPROVED', undefined, pages.APPROVED, PAGE_SIZE, sortDirection, skillReviewEnabled && activeStatus === 'APPROVED') diff --git a/web/src/shared/components/namespace-picker.test.tsx b/web/src/shared/components/namespace-picker.test.tsx index 064a15a3..ea89e20a 100644 --- a/web/src/shared/components/namespace-picker.test.tsx +++ b/web/src/shared/components/namespace-picker.test.tsx @@ -110,4 +110,16 @@ describe('NamespacePicker', () => { expect(onValueChange).toHaveBeenCalledWith('') expect(screen.queryByRole('dialog')).toBeNull() }) + + it('uses the optional empty label for an empty trigger value', () => { + render( + , + ) + + expect(screen.getByRole('button', { name: 'All namespaces' })).toBeTruthy() + }) }) diff --git a/web/src/shared/components/namespace-picker.tsx b/web/src/shared/components/namespace-picker.tsx index 99531310..e8a7007c 100644 --- a/web/src/shared/components/namespace-picker.tsx +++ b/web/src/shared/components/namespace-picker.tsx @@ -59,7 +59,7 @@ export function NamespacePicker({ diff --git a/web/src/shared/components/user-menu.test.tsx b/web/src/shared/components/user-menu.test.tsx index 1474f0bf..5deefa89 100644 --- a/web/src/shared/components/user-menu.test.tsx +++ b/web/src/shared/components/user-menu.test.tsx @@ -148,8 +148,14 @@ describe('UserMenu security settings visibility', () => { expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1, + status: 'ACTIVE', roles: ['OWNER', 'ADMIN'], }, true) + expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ + page: 0, + size: 1, + roles: ['OWNER', 'ADMIN'], + }, false) expect(html).toContain('user.menu.reviews') }) @@ -166,6 +172,7 @@ describe('UserMenu security settings visibility', () => { expect(useMyNamespacesPageMock).toHaveBeenCalledWith({ page: 0, size: 1, + status: 'ACTIVE', roles: ['OWNER', 'ADMIN'], }, false) }) diff --git a/web/src/shared/components/user-menu.tsx b/web/src/shared/components/user-menu.tsx index a4b84ceb..76847dc8 100644 --- a/web/src/shared/components/user-menu.tsx +++ b/web/src/shared/components/user-menu.tsx @@ -3,8 +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 { useMyNamespacesPage } from '@/shared/hooks/use-namespace-queries' -import { buildGlobalReviewsPath, canAccessGlobalReviewCenter, canAccessReviewCenter } from '@/features/review/review-paths' +import { buildGlobalReviewsPath, canAccessGlobalReviewCenter } from '@/features/review/review-paths' +import { useNamespaceReviewEntry } from '@/features/review/use-namespace-review-entry' import { clearSessionScopedQueries } from '@/features/notification/notification-session' import { canViewGovernanceCenter } from '@/shared/lib/governance-access' import { cn } from '@/shared/lib/utils' @@ -37,12 +37,8 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) { const isAuditor = hasRole('AUDITOR') || hasRole('SUPER_ADMIN') const isSuperAdmin = hasRole('SUPER_ADMIN') const hasGlobalReviewAccess = canAccessGlobalReviewCenter(user.platformRoles) - const { data: myNamespacesPage } = useMyNamespacesPage({ - page: 0, - size: 1, - roles: ['OWNER', 'ADMIN'], - }, !hasGlobalReviewAccess) - const reviewCenterVisible = canAccessReviewCenter(user.platformRoles, myNamespacesPage?.items) + const { namespaceReviewEntry } = useNamespaceReviewEntry(hasGlobalReviewAccess) + const reviewCenterVisible = hasGlobalReviewAccess || namespaceReviewEntry !== null const canChangePassword = user.canChangePassword === true const open = isHovered || isClickOpen