diff --git a/src/langbot/pkg/api/http/controller/groups/workspaces.py b/src/langbot/pkg/api/http/controller/groups/workspaces.py index 6b6817b07..4640596cd 100644 --- a/src/langbot/pkg/api/http/controller/groups/workspaces.py +++ b/src/langbot/pkg/api/http/controller/groups/workspaces.py @@ -30,12 +30,14 @@ def _workspace_payload(workspace: Workspace) -> dict[str, typing.Any]: def _membership_payload( membership: WorkspaceMembership, *, + display_name: str, email: str, ) -> dict[str, typing.Any]: return { 'uuid': membership.uuid, 'workspace_uuid': membership.workspace_uuid, 'account_uuid': membership.account_uuid, + 'display_name': display_name, 'email': email, 'role': membership.role, 'status': membership.status, @@ -94,7 +96,11 @@ class WorkspacesRouterGroup(group.RouterGroup): workspaces.append( { 'workspace': _workspace_payload(access.workspace), - 'membership': _membership_payload(access.membership, email=account.user), + 'membership': _membership_payload( + access.membership, + display_name=account.user, + email=account.normalized_email, + ), 'permissions': sorted(permissions_for_role(access.membership.role)), 'placement_generation': access.execution.placement_generation, 'plan_name': plan_name, @@ -137,6 +143,7 @@ class WorkspacesRouterGroup(group.RouterGroup): 'uuid': None, 'workspace_uuid': request_context.workspace_uuid, 'account_uuid': None, + 'display_name': None, 'email': None, 'role': 'owner', 'status': 'active', @@ -154,7 +161,11 @@ class WorkspacesRouterGroup(group.RouterGroup): return self.success( data={ 'workspace': _workspace_payload(workspace), - 'membership': _membership_payload(membership, email=account.user), + 'membership': _membership_payload( + membership, + display_name=account.user, + email=account.normalized_email, + ), 'permissions': sorted(request_context.workspace.permissions), 'placement_generation': request_context.placement_generation, 'plan_name': plan_name, @@ -283,7 +294,8 @@ class WorkspacesRouterGroup(group.RouterGroup): data={ 'member': _membership_payload( member, - email=account.user if account is not None else '', + display_name=account.user if account is not None else '', + email=account.normalized_email if account is not None else '', ) } ) @@ -302,7 +314,11 @@ class WorkspacesRouterGroup(group.RouterGroup): @staticmethod def _member_view_payload(view: WorkspaceMemberView) -> dict[str, typing.Any]: - return _membership_payload(view.membership, email=view.email) + return _membership_payload( + view.membership, + display_name=view.display_name, + email=view.email, + ) @group.group_class('invitations', '/api/v1/invitations') diff --git a/src/langbot/pkg/workspace/collaboration.py b/src/langbot/pkg/workspace/collaboration.py index 3ffbb6999..5d88935bf 100644 --- a/src/langbot/pkg/workspace/collaboration.py +++ b/src/langbot/pkg/workspace/collaboration.py @@ -88,6 +88,7 @@ class ResolvedWorkspaceAccess: @dataclasses.dataclass(frozen=True, slots=True) class WorkspaceMemberView: membership: WorkspaceMembership + display_name: str email: str @@ -294,7 +295,7 @@ class WorkspaceCollaborationService: async def operation(active_session: AsyncSession) -> list[WorkspaceMemberView]: await self._load_actor(active_session, workspace_uuid, actor) statement = ( - sqlalchemy.select(WorkspaceMembership, User.user) + sqlalchemy.select(WorkspaceMembership, User.user, User.normalized_email) .join(User, User.uuid == WorkspaceMembership.account_uuid) .where( WorkspaceMembership.workspace_uuid == workspace_uuid, @@ -304,8 +305,12 @@ class WorkspaceCollaborationService: .order_by(WorkspaceMembership.created_at, WorkspaceMembership.uuid) ) return [ - WorkspaceMemberView(membership=membership, email=email) - for membership, email in (await active_session.execute(statement)).all() + WorkspaceMemberView( + membership=membership, + display_name=display_name, + email=email, + ) + for membership, display_name, email in (await active_session.execute(statement)).all() ] return await self._run(operation, session=session, read_only=True) diff --git a/tests/integration/api/test_workspaces.py b/tests/integration/api/test_workspaces.py index d3a70a56d..04e798ca4 100644 --- a/tests/integration/api/test_workspaces.py +++ b/tests/integration/api/test_workspaces.py @@ -281,6 +281,31 @@ async def test_owner_invites_second_account_and_secret_is_not_persisted(workspac assert (await forbidden_invite.get_json())['code'] == 'permission_denied' +async def test_workspace_member_list_returns_display_name_and_email(workspace_api): + _, client, engine, owner_token = workspace_api + + current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token)) + current = (await current_response.get_json())['data'] + workspace_uuid = current['workspace']['uuid'] + owner_uuid = current['membership']['account_uuid'] + + async with engine.begin() as connection: + await connection.execute( + sqlalchemy.update(User).where(User.uuid == owner_uuid).values(user='Owner Display Name') + ) + + response = await client.get( + f'/api/v1/workspaces/{workspace_uuid}/members', + headers=_auth(owner_token, workspace_uuid), + ) + + assert response.status_code == 200 + members = (await response.get_json())['data']['members'] + assert len(members) == 1 + assert members[0]['display_name'] == 'Owner Display Name' + assert members[0]['email'] == 'owner@example.com' + + async def test_oss_invitation_accept_requires_logout_before_registration(workspace_api): _, client, _, owner_token = workspace_api diff --git a/web/src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx b/web/src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx index 2469c5194..ecd3e08cb 100644 --- a/web/src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx +++ b/web/src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx @@ -313,18 +313,20 @@ export default function WorkspaceSettingsPanel({ - + - {member.email} + {member.display_name} {isSelf && ( {t('workspace.you')} )} - - {t(`workspace.roles.${member.role}`)} + + {member.email} + + {t(`workspace.roles.${member.role}`)} - + {canUpdateMembers && member.role !== 'owner' && (