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' && (