From 739e21e0fd297b0f0a2b78208ce6b4dd8c06871b Mon Sep 17 00:00:00 2001 From: Seasoning <49091147+Rsweater@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:02:20 +0800 Subject: [PATCH] Feat/namespace member display username (#236) * feat: display username and email in namespace member management MemberResponse DTO now includes displayName and email fields. NamespacePortalQueryAppService batch-loads UserAccount data via findByIdIn to avoid N+1 queries. Frontend member table shows username (with userId as fallback subtitle) and email columns instead of raw user IDs. * test(namespace): add displayName/email assertions and new test coverage - Controller: add displayName and email assertions to addMember and updateMemberRole tests - QueryAppService: add listMembers tests for batch user enrichment and null degradation - CommandAppService: add addMember/updateMemberRole tests for displayName/email population and graceful degradation Addresses PR #236 review feedback comments: - Issue 2: Controller tests now assert displayName and email from mocked UserAccount - Issue 3: Service layer tests now cover batch query and null user degradation --- .../iflytek/skillhub/dto/MemberResponse.java | 18 ++++ .../NamespacePortalCommandAppService.java | 12 ++- .../NamespacePortalQueryAppService.java | 23 ++++- .../NamespacePortalControllerTest.java | 19 +++- .../NamespacePortalCommandAppServiceTest.java | 99 ++++++++++++++++++- .../NamespacePortalQueryAppServiceTest.java | 57 ++++++++++- web/src/api/generated/schema.d.ts | 2 + web/src/api/types.ts | 2 + web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/zh.json | 2 + web/src/pages/dashboard/namespace-members.tsx | 11 ++- 11 files changed, 236 insertions(+), 11 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MemberResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MemberResponse.java index dfd39d35..0bb52cbc 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MemberResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MemberResponse.java @@ -2,6 +2,7 @@ package com.iflytek.skillhub.dto; import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.domain.user.UserAccount; import java.time.Instant; @@ -9,6 +10,8 @@ public record MemberResponse( Long id, Long namespaceId, String userId, + String displayName, + String email, NamespaceRole role, Instant createdAt, Instant updatedAt @@ -18,6 +21,21 @@ public record MemberResponse( member.getId(), member.getNamespaceId(), member.getUserId(), + null, + null, + member.getRole(), + member.getCreatedAt(), + member.getUpdatedAt() + ); + } + + public static MemberResponse from(NamespaceMember member, UserAccount user) { + return new MemberResponse( + member.getId(), + member.getNamespaceId(), + member.getUserId(), + user != null ? user.getDisplayName() : null, + user != null ? user.getEmail() : null, member.getRole(), member.getCreatedAt(), member.getUpdatedAt() diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalCommandAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalCommandAppService.java index 92c1340b..3e81884b 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalCommandAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/NamespacePortalCommandAppService.java @@ -7,6 +7,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceMemberService; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceService; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.dto.MemberResponse; import com.iflytek.skillhub.dto.MessageResponse; import com.iflytek.skillhub.dto.NamespaceLifecycleRequest; @@ -28,15 +30,18 @@ public class NamespacePortalCommandAppService { private final NamespaceRepository namespaceRepository; private final NamespaceGovernanceService namespaceGovernanceService; private final NamespaceMemberService namespaceMemberService; + private final UserAccountRepository userAccountRepository; public NamespacePortalCommandAppService(NamespaceService namespaceService, NamespaceRepository namespaceRepository, NamespaceGovernanceService namespaceGovernanceService, - NamespaceMemberService namespaceMemberService) { + NamespaceMemberService namespaceMemberService, + UserAccountRepository userAccountRepository) { this.namespaceService = namespaceService; this.namespaceRepository = namespaceRepository; this.namespaceGovernanceService = namespaceGovernanceService; this.namespaceMemberService = namespaceMemberService; + this.userAccountRepository = userAccountRepository; } @Transactional @@ -135,7 +140,8 @@ public class NamespacePortalCommandAppService { role, operatorUserId ); - return MemberResponse.from(member); + UserAccount user = userAccountRepository.findById(memberUserId).orElse(null); + return MemberResponse.from(member, user); } @Transactional @@ -157,7 +163,7 @@ public class NamespacePortalCommandAppService { request.role(), operatorUserId ); - return MemberResponse.from(member); + return MemberResponse.from(member, userAccountRepository.findById(userId).orElse(null)); } private boolean canCreateNamespace(PlatformPrincipal principal) { 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 17dd9b99..6a21714e 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 @@ -8,6 +8,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceService; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.dto.MemberResponse; import com.iflytek.skillhub.dto.MyNamespaceResponse; import com.iflytek.skillhub.dto.NamespaceResponse; @@ -15,6 +17,8 @@ import com.iflytek.skillhub.dto.PageResponse; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -31,15 +35,18 @@ public class NamespacePortalQueryAppService { private final NamespaceService namespaceService; private final NamespaceMemberService namespaceMemberService; private final NamespaceAccessPolicy namespaceAccessPolicy; + private final UserAccountRepository userAccountRepository; public NamespacePortalQueryAppService(NamespaceRepository namespaceRepository, NamespaceService namespaceService, NamespaceMemberService namespaceMemberService, - NamespaceAccessPolicy namespaceAccessPolicy) { + NamespaceAccessPolicy namespaceAccessPolicy, + UserAccountRepository userAccountRepository) { this.namespaceRepository = namespaceRepository; this.namespaceService = namespaceService; this.namespaceMemberService = namespaceMemberService; this.namespaceAccessPolicy = namespaceAccessPolicy; + this.userAccountRepository = userAccountRepository; } @Transactional(readOnly = true) @@ -78,6 +85,18 @@ public class NamespacePortalQueryAppService { Namespace namespace = namespaceService.getNamespaceBySlug(slug); namespaceService.assertMember(namespace.getId(), userId); Page members = namespaceMemberService.listMembers(namespace.getId(), pageable); - return PageResponse.from(members.map(MemberResponse::from)); + + List memberUserIds = members.getContent().stream() + .map(NamespaceMember::getUserId) + .toList(); + + Map userMap = memberUserIds.isEmpty() + ? Map.of() + : userAccountRepository.findByIdIn(memberUserIds).stream() + .collect(Collectors.toMap(UserAccount::getId, Function.identity())); + + return PageResponse.from(members.map(member -> + MemberResponse.from(member, userMap.get(member.getUserId())) + )); } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java index 76663e99..76775b1f 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java @@ -12,6 +12,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceService; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; import com.iflytek.skillhub.domain.namespace.NamespaceType; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; import com.iflytek.skillhub.dto.NamespaceCandidateUserResponse; import com.iflytek.skillhub.service.NamespaceMemberCandidateService; import org.junit.jupiter.api.Test; @@ -71,6 +73,9 @@ class NamespacePortalControllerTest { @MockBean private DeviceAuthService deviceAuthService; + @MockBean + private UserAccountRepository userAccountRepository; + @Test void listMyNamespaces_returnsFrozenAndArchivedNamespacesWithCurrentRole() throws Exception { Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ARCHIVED, NamespaceType.TEAM); @@ -188,9 +193,12 @@ class NamespacePortalControllerTest { void addMember_returnsCreatedMember() throws Exception { Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM); NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN); + UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null); given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace); given(namespaceMemberService.addMember(1L, "user-2", NamespaceRole.ADMIN, "owner-1")) .willReturn(member); + given(userAccountRepository.findById("user-2")) + .willReturn(java.util.Optional.of(user)); mockMvc.perform(post("/api/v1/namespaces/team-a/members") .with(csrf()) @@ -203,7 +211,9 @@ class NamespacePortalControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.userId").value("user-2")) - .andExpect(jsonPath("$.data.role").value("ADMIN")); + .andExpect(jsonPath("$.data.role").value("ADMIN")) + .andExpect(jsonPath("$.data.displayName").value("Alice")) + .andExpect(jsonPath("$.data.email").value("alice@example.com")); } @Test @@ -224,9 +234,12 @@ class NamespacePortalControllerTest { void updateMemberRole_returnsUpdatedMember() throws Exception { Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM); NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.OWNER); + UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null); given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace); given(namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.OWNER, "owner-1")) .willReturn(member); + given(userAccountRepository.findById("user-2")) + .willReturn(java.util.Optional.of(user)); mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put("/api/v1/namespaces/team-a/members/user-2/role") .with(csrf()) @@ -239,7 +252,9 @@ class NamespacePortalControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.userId").value("user-2")) - .andExpect(jsonPath("$.data.role").value("OWNER")); + .andExpect(jsonPath("$.data.role").value("OWNER")) + .andExpect(jsonPath("$.data.displayName").value("Alice")) + .andExpect(jsonPath("$.data.email").value("alice@example.com")); } @Test diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalCommandAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalCommandAppServiceTest.java index 80a76c15..719ad40d 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalCommandAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/NamespacePortalCommandAppServiceTest.java @@ -9,17 +9,24 @@ import static org.mockito.Mockito.when; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceGovernanceService; +import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceMemberService; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; +import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceService; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; import com.iflytek.skillhub.domain.namespace.NamespaceType; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; +import com.iflytek.skillhub.dto.MemberResponse; import com.iflytek.skillhub.dto.NamespaceLifecycleRequest; import com.iflytek.skillhub.dto.NamespaceRequest; +import com.iflytek.skillhub.dto.UpdateMemberRoleRequest; import com.iflytek.skillhub.exception.ForbiddenException; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; +import java.util.Optional; import java.util.Set; class NamespacePortalCommandAppServiceTest { @@ -28,11 +35,13 @@ class NamespacePortalCommandAppServiceTest { private final NamespaceRepository namespaceRepository = mock(NamespaceRepository.class); private final NamespaceGovernanceService namespaceGovernanceService = mock(NamespaceGovernanceService.class); private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class); + private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class); private final NamespacePortalCommandAppService service = new NamespacePortalCommandAppService( namespaceService, namespaceRepository, namespaceGovernanceService, - namespaceMemberService + namespaceMemberService, + userAccountRepository ); @Test @@ -71,4 +80,92 @@ class NamespacePortalCommandAppServiceTest { namespace.setType(NamespaceType.TEAM); return namespace; } + + @Test + void addMember_populatesDisplayNameAndEmail() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN); + ReflectionTestUtils.setField(member, "id", 10L); + UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.addMember(1L, "user-2", NamespaceRole.ADMIN, "owner-1")) + .thenReturn(member); + when(userAccountRepository.findById("user-2")) + .thenReturn(Optional.of(user)); + + MemberResponse result = service.addMember("team-a", "user-2", NamespaceRole.ADMIN, "owner-1"); + + assertThat(result.userId()).isEqualTo("user-2"); + assertThat(result.displayName()).isEqualTo("Alice"); + assertThat(result.email()).isEqualTo("alice@example.com"); + assertThat(result.role()).isEqualTo(NamespaceRole.ADMIN); + } + + @Test + void addMember_withoutUserAccount_degradesGracefully() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "ghost", NamespaceRole.MEMBER); + ReflectionTestUtils.setField(member, "id", 20L); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.addMember(1L, "ghost", NamespaceRole.MEMBER, "owner-1")) + .thenReturn(member); + when(userAccountRepository.findById("ghost")) + .thenReturn(Optional.empty()); + + MemberResponse result = service.addMember("team-a", "ghost", NamespaceRole.MEMBER, "owner-1"); + + assertThat(result.userId()).isEqualTo("ghost"); + assertThat(result.displayName()).isNull(); + assertThat(result.email()).isNull(); + } + + @Test + void updateMemberRole_populatesDisplayNameAndEmail() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.OWNER); + ReflectionTestUtils.setField(member, "id", 10L); + UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.OWNER, "owner-1")) + .thenReturn(member); + when(userAccountRepository.findById("user-2")) + .thenReturn(Optional.of(user)); + + MemberResponse result = service.updateMemberRole( + "team-a", "user-2", + new UpdateMemberRoleRequest(NamespaceRole.OWNER), + "owner-1" + ); + + assertThat(result.userId()).isEqualTo("user-2"); + assertThat(result.displayName()).isEqualTo("Alice"); + assertThat(result.email()).isEqualTo("alice@example.com"); + assertThat(result.role()).isEqualTo(NamespaceRole.OWNER); + } + + @Test + void updateMemberRole_withoutUserAccount_degradesGracefully() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "ghost", NamespaceRole.MEMBER); + ReflectionTestUtils.setField(member, "id", 20L); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.updateMemberRole(1L, "ghost", NamespaceRole.ADMIN, "owner-1")) + .thenReturn(member); + when(userAccountRepository.findById("ghost")) + .thenReturn(Optional.empty()); + + MemberResponse result = service.updateMemberRole( + "team-a", "ghost", + new UpdateMemberRoleRequest(NamespaceRole.ADMIN), + "owner-1" + ); + + assertThat(result.userId()).isEqualTo("ghost"); + assertThat(result.displayName()).isNull(); + assertThat(result.email()).isNull(); + } } 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 61e6a458..7108fc40 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 @@ -1,19 +1,28 @@ package com.iflytek.skillhub.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceAccessPolicy; +import com.iflytek.skillhub.domain.namespace.NamespaceMember; import com.iflytek.skillhub.domain.namespace.NamespaceMemberService; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceService; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; import com.iflytek.skillhub.domain.namespace.NamespaceType; +import com.iflytek.skillhub.domain.user.UserAccount; +import com.iflytek.skillhub.domain.user.UserAccountRepository; +import com.iflytek.skillhub.dto.MemberResponse; +import com.iflytek.skillhub.dto.PageResponse; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; @@ -25,11 +34,13 @@ class NamespacePortalQueryAppServiceTest { private final NamespaceService namespaceService = mock(NamespaceService.class); private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class); private final NamespaceAccessPolicy namespaceAccessPolicy = mock(NamespaceAccessPolicy.class); + private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class); private final NamespacePortalQueryAppService service = new NamespacePortalQueryAppService( namespaceRepository, namespaceService, namespaceMemberService, - namespaceAccessPolicy + namespaceAccessPolicy, + userAccountRepository ); @Test @@ -67,4 +78,48 @@ class NamespacePortalQueryAppServiceTest { namespace.setType(NamespaceType.TEAM); return namespace; } + + @Test + void listMembers_withUserAccount_returnsDisplayNameAndEmail() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN); + ReflectionTestUtils.setField(member, "id", 10L); + UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.listMembers(eq(1L), any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of(member), PageRequest.of(0, 20), 1)); + when(userAccountRepository.findByIdIn(List.of("user-2"))) + .thenReturn(List.of(user)); + + PageResponse result = service.listMembers("team-a", PageRequest.of(0, 20), "owner-1"); + + assertThat(result.items()).hasSize(1); + MemberResponse mr = result.items().get(0); + assertThat(mr.userId()).isEqualTo("user-2"); + assertThat(mr.displayName()).isEqualTo("Alice"); + assertThat(mr.email()).isEqualTo("alice@example.com"); + assertThat(mr.role()).isEqualTo(NamespaceRole.ADMIN); + } + + @Test + void listMembers_withoutUserAccount_returnsNullFields() { + Namespace ns = namespace(1L, "team-a"); + NamespaceMember member = new NamespaceMember(1L, "ghost-user", NamespaceRole.MEMBER); + ReflectionTestUtils.setField(member, "id", 20L); + + when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns); + when(namespaceMemberService.listMembers(eq(1L), any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of(member), PageRequest.of(0, 20), 1)); + when(userAccountRepository.findByIdIn(List.of("ghost-user"))) + .thenReturn(List.of()); + + PageResponse result = service.listMembers("team-a", PageRequest.of(0, 20), "owner-1"); + + assertThat(result.items()).hasSize(1); + MemberResponse mr = result.items().get(0); + assertThat(mr.userId()).isEqualTo("ghost-user"); + assertThat(mr.displayName()).isNull(); + assertThat(mr.email()).isNull(); + } } diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts index 2e2df9ab..5da2df9d 100644 --- a/web/src/api/generated/schema.d.ts +++ b/web/src/api/generated/schema.d.ts @@ -3037,6 +3037,8 @@ export interface components { /** Format: int64 */ namespaceId?: number; userId?: string; + displayName?: string; + email?: string; /** @enum {string} */ role?: "OWNER" | "ADMIN" | "MEMBER"; /** Format: date-time */ diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 07f861a4..61c9fa46 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -116,6 +116,8 @@ export interface ManagedNamespace extends Namespace { export interface NamespaceMember { id: number userId: string + displayName?: string + email?: string role: NamespaceRole createdAt: string } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b1e35669..5d1c5c93 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -983,6 +983,8 @@ "savingRole": "Saving...", "changeRole": "Change role", "colUserId": "User ID", + "colUsername": "Username", + "colEmail": "Email", "colRole": "Role", "colJoinedAt": "Joined At", "colActions": "Actions", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 41972c55..16144d08 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -983,6 +983,8 @@ "savingRole": "保存中...", "changeRole": "修改角色", "colUserId": "用户 ID", + "colUsername": "用户名", + "colEmail": "邮箱", "colRole": "角色", "colJoinedAt": "加入时间", "colActions": "操作", diff --git a/web/src/pages/dashboard/namespace-members.tsx b/web/src/pages/dashboard/namespace-members.tsx index 76504b38..88adb53d 100644 --- a/web/src/pages/dashboard/namespace-members.tsx +++ b/web/src/pages/dashboard/namespace-members.tsx @@ -178,7 +178,8 @@ export function NamespaceMembersPage() { - + + @@ -193,7 +194,13 @@ export function NamespaceMembersPage() { return ( - + +
{t('members.colUserId')}{t('members.colUsername')}{t('members.colEmail')} {t('members.colRole')} {t('members.colJoinedAt')} {t('members.colActions')}
{member.userId} +
+ {member.displayName || member.userId} + {member.userId} +
+
{member.email || '-'} {canManageMembers && !isOwner ? (