mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-05 08:05:56 +00:00
fix(namespace): scope review entry queries
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
78cbe05ebe
commit
f8df616480
21 changed files with 144 additions and 12 deletions
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.controller.BaseApiController;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.BatchMemberRequest;
|
||||
|
|
@ -81,6 +82,7 @@ public class NamespaceController extends BaseApiController {
|
|||
public ApiResponse<PageResponse<MyNamespaceResponse>> listMyNamespacesPage(
|
||||
Pageable pageable,
|
||||
@RequestParam(required = false) NamespaceStatus status,
|
||||
@RequestParam(required = false) NamespaceType type,
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String slug,
|
||||
@RequestParam(required = false) Set<NamespaceRole> roles,
|
||||
|
|
@ -93,6 +95,7 @@ public class NamespaceController extends BaseApiController {
|
|||
userNsRoles,
|
||||
normalizePlatformRoles(platformRoles),
|
||||
status,
|
||||
type,
|
||||
q,
|
||||
slug,
|
||||
roles));
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ public class NamespacePortalQueryAppService {
|
|||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles,
|
||||
NamespaceStatus status,
|
||||
NamespaceType type,
|
||||
String query,
|
||||
String slug,
|
||||
Set<NamespaceRole> roles) {
|
||||
|
|
@ -180,6 +181,7 @@ public class NamespacePortalQueryAppService {
|
|||
if (isSuperAdmin(platformRoles) && requestedRoles.isEmpty()) {
|
||||
Page<Namespace> visibleNamespaces = namespaceRepository.search(
|
||||
status,
|
||||
type,
|
||||
normalizedQuery,
|
||||
normalizedSlug,
|
||||
boundedPageable
|
||||
|
|
@ -200,6 +202,7 @@ public class NamespacePortalQueryAppService {
|
|||
Page<Namespace> visibleNamespaces = namespaceRepository.searchByIdIn(
|
||||
scopedNamespaceIds,
|
||||
status,
|
||||
type,
|
||||
normalizedQuery,
|
||||
normalizedSlug,
|
||||
boundedPageable
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class NamespacePortalControllerTest {
|
|||
Namespace active = namespace(1L, "active", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
Namespace archived = namespace(3L, "archived", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
given(namespaceRepository.search(eq(null), eq(null), eq(null), any()))
|
||||
given(namespaceRepository.search(eq(null), eq(null), eq(null), eq(null), any()))
|
||||
.willReturn(new org.springframework.data.domain.PageImpl<>(
|
||||
List.of(active, archived),
|
||||
org.springframework.data.domain.PageRequest.of(0, 2),
|
||||
|
|
@ -159,6 +159,7 @@ class NamespacePortalControllerTest {
|
|||
given(namespaceRepository.searchByIdIn(
|
||||
eq(List.of(1L)),
|
||||
eq(NamespaceStatus.ACTIVE),
|
||||
eq(NamespaceType.TEAM),
|
||||
eq("team"),
|
||||
eq("team-ai"),
|
||||
any()
|
||||
|
|
@ -172,6 +173,7 @@ class NamespacePortalControllerTest {
|
|||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.param("status", "ACTIVE")
|
||||
.param("type", "TEAM")
|
||||
.param("q", "team")
|
||||
.param("slug", "team-ai")
|
||||
.param("roles", "OWNER", "ADMIN")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
|
@ -34,6 +35,9 @@ class NamespaceJpaRepositoryTest {
|
|||
Namespace archived = new Namespace("archived-percent", "50% Archived", "owner-1");
|
||||
archived.setStatus(NamespaceStatus.ARCHIVED);
|
||||
persist(archived);
|
||||
Namespace global = new Namespace("global", "50% Global", "owner-1");
|
||||
global.setType(NamespaceType.GLOBAL);
|
||||
persist(global);
|
||||
entityManager.flush();
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +45,7 @@ class NamespaceJpaRepositoryTest {
|
|||
void search_treatsEscapedWildcardsLiterallyAndAppliesStatus() {
|
||||
var percentPage = repository.search(
|
||||
NamespaceStatus.ACTIVE,
|
||||
NamespaceType.TEAM,
|
||||
"!%",
|
||||
null,
|
||||
PageRequest.of(0, 10)
|
||||
|
|
@ -48,6 +53,7 @@ class NamespaceJpaRepositoryTest {
|
|||
var underscorePage = repository.searchByIdIn(
|
||||
List.of(percentNamespace.getId(), underscoreNamespace.getId()),
|
||||
NamespaceStatus.ACTIVE,
|
||||
NamespaceType.TEAM,
|
||||
"!_",
|
||||
null,
|
||||
PageRequest.of(0, 10)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
when(namespaceRepository.searchByIdIn(
|
||||
eq(List.of(1L)),
|
||||
eq(NamespaceStatus.ACTIVE),
|
||||
eq(NamespaceType.TEAM),
|
||||
eq("team"),
|
||||
eq("team-ai"),
|
||||
any(Pageable.class)
|
||||
|
|
@ -165,6 +166,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Map.of(1L, NamespaceRole.OWNER, 2L, NamespaceRole.MEMBER),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ACTIVE,
|
||||
NamespaceType.TEAM,
|
||||
" team ",
|
||||
" team-ai ",
|
||||
Set.of(NamespaceRole.OWNER, NamespaceRole.ADMIN)
|
||||
|
|
@ -175,11 +177,12 @@ class NamespacePortalQueryAppServiceTest {
|
|||
verify(namespaceRepository).searchByIdIn(
|
||||
eq(List.of(1L)),
|
||||
eq(NamespaceStatus.ACTIVE),
|
||||
eq(NamespaceType.TEAM),
|
||||
eq("team"),
|
||||
eq("team-ai"),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any());
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -189,6 +192,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Pageable expectedPageable = PageRequest.of(1, 10);
|
||||
when(namespaceRepository.search(
|
||||
eq(NamespaceStatus.ARCHIVED),
|
||||
eq(null),
|
||||
eq("ops"),
|
||||
eq("ops-team"),
|
||||
any(Pageable.class)
|
||||
|
|
@ -199,6 +203,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Map.of(),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ARCHIVED,
|
||||
null,
|
||||
" ops ",
|
||||
" ops-team ",
|
||||
Set.of()
|
||||
|
|
@ -208,17 +213,19 @@ class NamespacePortalQueryAppServiceTest {
|
|||
assertThat(response.total()).isEqualTo(11);
|
||||
verify(namespaceRepository).search(
|
||||
eq(NamespaceStatus.ARCHIVED),
|
||||
eq(null),
|
||||
eq("ops"),
|
||||
eq("ops-team"),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).searchByIdIn(anyList(), any(), any(), any(), any());
|
||||
verify(namespaceRepository, never()).searchByIdIn(anyList(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_escapesLikeWildcardsForLiteralSubstringSearch() {
|
||||
Pageable expectedPageable = PageRequest.of(0, 20);
|
||||
when(namespaceRepository.search(
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq("50!%!_!!off"),
|
||||
eq(null),
|
||||
|
|
@ -230,12 +237,14 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Map.of(),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
null,
|
||||
null,
|
||||
" 50%_!off ",
|
||||
null,
|
||||
Set.of()
|
||||
);
|
||||
|
||||
verify(namespaceRepository).search(
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq("50!%!_!!off"),
|
||||
eq(null),
|
||||
|
|
@ -252,6 +261,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
any(Pageable.class)
|
||||
)).thenReturn(new PageImpl<>(List.of(administered, member), PageRequest.of(0, 20), 2));
|
||||
|
||||
|
|
@ -260,6 +270,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Map.of(2L, NamespaceRole.ADMIN, 1L, NamespaceRole.MEMBER),
|
||||
Set.of(),
|
||||
null,
|
||||
null,
|
||||
" ",
|
||||
"\t",
|
||||
Set.of()
|
||||
|
|
@ -273,9 +284,10 @@ class NamespacePortalQueryAppServiceTest {
|
|||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any());
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -285,6 +297,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
Map.of(1L, NamespaceRole.MEMBER),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ACTIVE,
|
||||
NamespaceType.TEAM,
|
||||
" team ",
|
||||
null,
|
||||
Set.of(NamespaceRole.OWNER, NamespaceRole.ADMIN)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,17 @@ public interface NamespaceRepository {
|
|||
Optional<Namespace> findBySlug(String slug);
|
||||
Page<Namespace> findAll(Pageable pageable);
|
||||
Page<Namespace> findByStatus(NamespaceStatus status, Pageable pageable);
|
||||
Page<Namespace> search(NamespaceStatus status, String query, String slug, Pageable pageable);
|
||||
Page<Namespace> search(
|
||||
NamespaceStatus status,
|
||||
NamespaceType type,
|
||||
String query,
|
||||
String slug,
|
||||
Pageable pageable
|
||||
);
|
||||
Page<Namespace> searchByIdIn(
|
||||
List<Long> ids,
|
||||
NamespaceStatus status,
|
||||
NamespaceType type,
|
||||
String query,
|
||||
String slug,
|
||||
Pageable pageable
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.iflytek.skillhub.infra.jpa;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
|
@ -28,6 +29,7 @@ public interface NamespaceJpaRepository
|
|||
SELECT n
|
||||
FROM Namespace n
|
||||
WHERE (:status IS NULL OR n.status = :status)
|
||||
AND (:type IS NULL OR n.type = :type)
|
||||
AND (
|
||||
:query IS NULL
|
||||
OR lower(n.slug) LIKE lower(concat('%', :query, '%')) ESCAPE '!'
|
||||
|
|
@ -36,6 +38,7 @@ public interface NamespaceJpaRepository
|
|||
AND (:slug IS NULL OR n.slug = :slug)
|
||||
""")
|
||||
Page<Namespace> search(@Param("status") NamespaceStatus status,
|
||||
@Param("type") NamespaceType type,
|
||||
@Param("query") String query,
|
||||
@Param("slug") String slug,
|
||||
Pageable pageable);
|
||||
|
|
@ -46,6 +49,7 @@ public interface NamespaceJpaRepository
|
|||
FROM Namespace n
|
||||
WHERE n.id IN :ids
|
||||
AND (:status IS NULL OR n.status = :status)
|
||||
AND (:type IS NULL OR n.type = :type)
|
||||
AND (
|
||||
:query IS NULL
|
||||
OR lower(n.slug) LIKE lower(concat('%', :query, '%')) ESCAPE '!'
|
||||
|
|
@ -55,6 +59,7 @@ public interface NamespaceJpaRepository
|
|||
""")
|
||||
Page<Namespace> searchByIdIn(@Param("ids") List<Long> ids,
|
||||
@Param("status") NamespaceStatus status,
|
||||
@Param("type") NamespaceType type,
|
||||
@Param("query") String query,
|
||||
@Param("slug") String slug,
|
||||
Pageable pageable);
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ describe('namespaceApi.listMinePage', () => {
|
|||
page: 1,
|
||||
size: 20,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
q: 'team ai',
|
||||
slug: 'team-ai',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
|
|
@ -264,7 +265,7 @@ describe('namespaceApi.listMinePage', () => {
|
|||
|
||||
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',
|
||||
'https://api.example.com/api/web/me/namespaces/page?page=1&size=20&status=ACTIVE&type=TEAM&q=team+ai&slug=team-ai&roles=OWNER&roles=ADMIN',
|
||||
expect.objectContaining({ headers: expect.any(Headers) }),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export interface MyNamespacePageParams {
|
|||
page?: number
|
||||
size?: number
|
||||
status?: 'ACTIVE' | 'FROZEN' | 'ARCHIVED'
|
||||
type?: 'GLOBAL' | 'TEAM'
|
||||
q?: string
|
||||
slug?: string
|
||||
roles?: Array<'OWNER' | 'ADMIN' | 'MEMBER'>
|
||||
|
|
@ -659,6 +660,9 @@ export const namespaceApi = {
|
|||
if (params.status) {
|
||||
query.set('status', params.status)
|
||||
}
|
||||
if (params.type) {
|
||||
query.set('type', params.type)
|
||||
}
|
||||
if (params.q?.trim()) {
|
||||
query.set('q', params.q.trim())
|
||||
}
|
||||
|
|
|
|||
2
web/src/api/generated/schema.d.ts
vendored
2
web/src/api/generated/schema.d.ts
vendored
|
|
@ -10054,6 +10054,7 @@ export interface operations {
|
|||
query: {
|
||||
pageable: components["schemas"]["Pageable"];
|
||||
status?: "ACTIVE" | "FROZEN" | "ARCHIVED";
|
||||
type?: "GLOBAL" | "TEAM";
|
||||
q?: string;
|
||||
slug?: string;
|
||||
roles?: ("OWNER" | "ADMIN" | "MEMBER")[];
|
||||
|
|
@ -10080,6 +10081,7 @@ export interface operations {
|
|||
query: {
|
||||
pageable: components["schemas"]["Pageable"];
|
||||
status?: "ACTIVE" | "FROZEN" | "ARCHIVED";
|
||||
type?: "GLOBAL" | "TEAM";
|
||||
q?: string;
|
||||
slug?: string;
|
||||
roles?: ("OWNER" | "ADMIN" | "MEMBER")[];
|
||||
|
|
|
|||
|
|
@ -34,9 +34,14 @@ describe('useNamespaceReviewEntry', () => {
|
|||
|
||||
it('uses one bounded ACTIVE query when an active review namespace exists', () => {
|
||||
const active = namespace('zeta-active', 'ACTIVE')
|
||||
useMyNamespacesPageMock.mockImplementation((params: { status?: string }) => ({
|
||||
const global = { ...namespace('global', 'ACTIVE'), type: 'GLOBAL' as const }
|
||||
useMyNamespacesPageMock.mockImplementation((params: { status?: string; type?: string }) => ({
|
||||
data: {
|
||||
items: params.status === 'ACTIVE' ? [active] : [namespace('alpha-archived', 'ARCHIVED')],
|
||||
items: params.type !== 'TEAM'
|
||||
? [global]
|
||||
: params.status === 'ACTIVE'
|
||||
? [active]
|
||||
: [namespace('alpha-archived', 'ARCHIVED')],
|
||||
total: 1,
|
||||
page: 0,
|
||||
size: 1,
|
||||
|
|
@ -51,11 +56,13 @@ describe('useNamespaceReviewEntry', () => {
|
|||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, true)
|
||||
expect(useMyNamespacesPageMock).toHaveBeenNthCalledWith(2, {
|
||||
page: 0,
|
||||
size: 1,
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, false)
|
||||
expect(result.namespaceReviewEntry?.slug).toBe('zeta-active')
|
||||
|
|
@ -79,6 +86,7 @@ describe('useNamespaceReviewEntry', () => {
|
|||
expect(useMyNamespacesPageMock).toHaveBeenNthCalledWith(2, {
|
||||
page: 0,
|
||||
size: 1,
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, true)
|
||||
expect(result.namespaceReviewEntry?.slug).toBe('alpha-archived')
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function useNamespaceReviewEntry(hasGlobalReviewAccess: boolean) {
|
|||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
roles: [...REVIEW_ROLES],
|
||||
}, !hasGlobalReviewAccess)
|
||||
const activeEntry = getPreferredNamespaceReviewEntry(activeQuery.data?.items)
|
||||
|
|
@ -23,6 +24,7 @@ export function useNamespaceReviewEntry(hasGlobalReviewAccess: boolean) {
|
|||
const fallbackQuery = useMyNamespacesPage({
|
||||
page: 0,
|
||||
size: 1,
|
||||
type: 'TEAM',
|
||||
roles: [...REVIEW_ROLES],
|
||||
}, fallbackEnabled)
|
||||
const fallbackEntry = fallbackEnabled
|
||||
|
|
@ -34,5 +36,8 @@ export function useNamespaceReviewEntry(hasGlobalReviewAccess: boolean) {
|
|||
isLoadingNamespaces: !hasGlobalReviewAccess
|
||||
&& (activeQuery.isLoading || (fallbackEnabled && fallbackQuery.isLoading)),
|
||||
hasNamespaceQueryError: Boolean(activeQuery.error || (fallbackEnabled && fallbackQuery.error)),
|
||||
retryNamespaceQueries: () => fallbackEnabled
|
||||
? fallbackQuery.refetch()
|
||||
: activeQuery.refetch(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -492,6 +492,8 @@
|
|||
"pageSubtitle": "Manage access credentials for CLI and API"
|
||||
},
|
||||
"reviews": {
|
||||
"namespaceLoadError": "Failed to load namespace review access.",
|
||||
"retryNamespaceLoad": "Retry",
|
||||
"title": "Review Center",
|
||||
"subtitle": "Manage platform review tasks",
|
||||
"typeSkill": "Skill Reviews",
|
||||
|
|
@ -1365,6 +1367,8 @@
|
|||
"namespace": "Namespace",
|
||||
"selectNamespace": "Select namespace",
|
||||
"namespaceUnavailable": "The selected namespace is not active or is no longer available.",
|
||||
"namespaceValidationError": "Failed to validate the selected namespace.",
|
||||
"retryNamespaceValidation": "Retry validation",
|
||||
"visibility": "Visibility",
|
||||
"visibilityOptions": {
|
||||
"public": "Public",
|
||||
|
|
|
|||
|
|
@ -492,6 +492,8 @@
|
|||
"pageSubtitle": "管理 CLI 和 API 使用的访问凭证"
|
||||
},
|
||||
"reviews": {
|
||||
"namespaceLoadError": "无法加载命名空间审核权限。",
|
||||
"retryNamespaceLoad": "重试",
|
||||
"title": "审核中心",
|
||||
"subtitle": "管理平台审核事务",
|
||||
"typeSkill": "技能审核",
|
||||
|
|
@ -1366,6 +1368,8 @@
|
|||
"namespace": "命名空间",
|
||||
"selectNamespace": "选择命名空间",
|
||||
"namespaceUnavailable": "所选命名空间未启用或已不可用。",
|
||||
"namespaceValidationError": "无法校验所选命名空间。",
|
||||
"retryNamespaceValidation": "重新校验",
|
||||
"visibility": "可见性",
|
||||
"visibilityOptions": {
|
||||
"public": "公开",
|
||||
|
|
|
|||
|
|
@ -135,6 +135,21 @@ describe('PublishPage', () => {
|
|||
expect(html).toContain('publish.namespaceUnavailable')
|
||||
})
|
||||
|
||||
it('distinguishes a namespace validation request failure from an unavailable namespace', () => {
|
||||
useMyNamespacesPageMock.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
error: new Error('network down'),
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(createElement(PublishPage))
|
||||
|
||||
expect(html).toContain('publish.namespaceValidationError')
|
||||
expect(html).toContain('publish.retryNamespaceValidation')
|
||||
expect(html).not.toContain('publish.namespaceUnavailable')
|
||||
})
|
||||
|
||||
it('exports a named component function', () => {
|
||||
expect(typeof PublishPage).toBe('function')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@ export function PublishPage() {
|
|||
const [warningDialogOpen, setWarningDialogOpen] = useState(false)
|
||||
const [precheckWarnings, setPrecheckWarnings] = useState<string[]>([])
|
||||
|
||||
const { data: selectedNamespacePage, isLoading: isLoadingSelectedNamespace } = useMyNamespacesPage({
|
||||
const {
|
||||
data: selectedNamespacePage,
|
||||
isLoading: isLoadingSelectedNamespace,
|
||||
error: selectedNamespaceError,
|
||||
refetch: refetchSelectedNamespace,
|
||||
} = useMyNamespacesPage({
|
||||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
|
|
@ -165,8 +170,17 @@ export function PublishPage() {
|
|||
status="ACTIVE"
|
||||
disabled={publishMutation.isPending}
|
||||
/>
|
||||
{namespaceSlug && !isLoadingSelectedNamespace && !selectedNamespace ? (
|
||||
<p className="text-sm text-destructive">{t('publish.namespaceUnavailable')}</p>
|
||||
{namespaceSlug && !isLoadingSelectedNamespace ? (
|
||||
selectedNamespaceError ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-destructive">{t('publish.namespaceValidationError')}</p>
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => refetchSelectedNamespace()}>
|
||||
{t('publish.retryNamespaceValidation')}
|
||||
</Button>
|
||||
</div>
|
||||
) : !selectedNamespace ? (
|
||||
<p className="text-sm text-destructive">{t('publish.namespaceUnavailable')}</p>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,25 @@ describe('ReviewsPage', () => {
|
|||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, false)
|
||||
})
|
||||
|
||||
it('renders a recoverable error when namespace review access cannot be loaded', () => {
|
||||
userMock.platformRoles = ['USER']
|
||||
hasRoleMock.mockReturnValue(false)
|
||||
useMyNamespacesPageMock.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
error: new Error('network down'),
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(createElement(ReviewsPage))
|
||||
|
||||
expect(html).toContain('reviews.namespaceLoadError')
|
||||
expect(html).toContain('reviews.retryNamespaceLoad')
|
||||
expect(html).not.toContain('Loading...')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useNavigate, useSearch } from '@tanstack/react-router'
|
|||
import { FileCheck2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
import {
|
||||
|
|
@ -54,6 +55,7 @@ export function ReviewsPage() {
|
|||
namespaceReviewEntry,
|
||||
isLoadingNamespaces,
|
||||
hasNamespaceQueryError,
|
||||
retryNamespaceQueries,
|
||||
} = useNamespaceReviewEntry(hasGlobalReviewAccess)
|
||||
const showTypeTabs = isSkillAdmin && isUserAdmin
|
||||
|
||||
|
|
@ -252,7 +254,16 @@ export function ReviewsPage() {
|
|||
<div className="space-y-8 animate-fade-up">
|
||||
<DashboardPageHeader title={t('reviews.title')} subtitle={t('reviews.subtitle')} />
|
||||
<Card className="p-8 text-center text-muted-foreground">
|
||||
Loading...
|
||||
{hasNamespaceQueryError ? (
|
||||
<div className="space-y-3">
|
||||
<p>{t('reviews.namespaceLoadError')}</p>
|
||||
<Button type="button" variant="outline" onClick={() => retryNamespaceQueries()}>
|
||||
{t('reviews.retryNamespaceLoad')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
'Loading...'
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -149,11 +149,13 @@ describe('UserMenu security settings visibility', () => {
|
|||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, true)
|
||||
expect(useMyNamespacesPageMock).toHaveBeenCalledWith({
|
||||
page: 0,
|
||||
size: 1,
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, false)
|
||||
expect(html).toContain('user.menu.reviews')
|
||||
|
|
@ -173,6 +175,7 @@ describe('UserMenu security settings visibility', () => {
|
|||
page: 0,
|
||||
size: 1,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
}, false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ describe('use-namespace-queries exports', () => {
|
|||
page: 3,
|
||||
size: 15,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
q: 'team',
|
||||
slug: 'team-ai',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
|
|
@ -64,6 +65,7 @@ describe('use-namespace-queries exports', () => {
|
|||
page: 3,
|
||||
size: 15,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
q: 'team',
|
||||
slug: 'team-ai',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
|
|
@ -79,6 +81,7 @@ describe('use-namespace-queries exports', () => {
|
|||
page: 3,
|
||||
size: 15,
|
||||
status: 'ACTIVE',
|
||||
type: 'TEAM',
|
||||
q: 'team',
|
||||
slug: 'team-ai',
|
||||
roles: ['OWNER', 'ADMIN'],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function normalizeMyNamespacePageParams(params: MyNamespacePageParams = {}): MyN
|
|||
page: params.page ?? 0,
|
||||
size: params.size ?? MY_NAMESPACES_PAGE_SIZE,
|
||||
...(params.status ? { status: params.status } : {}),
|
||||
...(params.type ? { type: params.type } : {}),
|
||||
...(q ? { q } : {}),
|
||||
...(slug ? { slug } : {}),
|
||||
...(params.roles?.length ? { roles: [...params.roles] } : {}),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue