mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
fix(namespace): preserve bounded selection semantics
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
2bd358049b
commit
78cbe05ebe
17 changed files with 324 additions and 32 deletions
|
|
@ -174,7 +174,7 @@ public class NamespacePortalQueryAppService {
|
|||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
Set<NamespaceRole> 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<String> platformRoles) {
|
||||
return platformRoles != null && platformRoles.contains(SUPER_ADMIN_ROLE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
95
web/src/features/review/use-namespace-review-entry.test.ts
Normal file
95
web/src/features/review/use-namespace-review-entry.test.ts
Normal file
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
38
web/src/features/review/use-namespace-review-entry.ts
Normal file
38
web/src/features/review/use-namespace-review-entry.ts
Normal file
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1365,6 +1365,7 @@
|
|||
},
|
||||
"namespace": "命名空间",
|
||||
"selectNamespace": "选择命名空间",
|
||||
"namespaceUnavailable": "所选命名空间未启用或已不可用。",
|
||||
"visibility": "可见性",
|
||||
"visibilityOptions": {
|
||||
"public": "公开",
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,9 +39,10 @@ export function PublishPage() {
|
|||
const [warningDialogOpen, setWarningDialogOpen] = useState(false)
|
||||
const [precheckWarnings, setPrecheckWarnings] = useState<string[]>([])
|
||||
|
||||
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 ? (
|
||||
<p className="text-sm text-destructive">{t('publish.namespaceUnavailable')}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
|
@ -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')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ describe('ReviewsPage', () => {
|
|||
expect(useMyNamespacesPageMock).toHaveBeenCalledWith({
|
||||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<NamespacePicker
|
||||
value=""
|
||||
onValueChange={vi.fn()}
|
||||
emptyValueLabel="All namespaces"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'All namespaces' })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function NamespacePicker({
|
|||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline" disabled={disabled} className="w-full justify-start">
|
||||
{value ? `@${value}` : t('namespacePicker.placeholder')}
|
||||
{value ? `@${value}` : emptyValueLabel ?? t('namespacePicker.placeholder')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent aria-label={t('namespacePicker.title')}>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue