From bf08d8ab3a305e9db41efabd0c15b9b429c3771e Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Wed, 21 Jan 2026 23:56:49 -0800 Subject: [PATCH 1/3] refactor: nest role fields in UserProjectMembership API response Changed the UserProjectMembership API to return role data as a nested object {"id": "...", "name": "...", "description": "..."} instead of three flattened fields. This provides better API consistency with the roles list endpoint and simplifies the frontend data model. Changes: - Backend: Updated serializers to return nested role object with null support - Frontend: Updated TypeScript types to match new structure - Frontend: Simplified data conversion in useMembers hook - Frontend: Added null safety to UI components (manage access dialog, team columns) - Tests: Updated assertions to verify nested structure All tests passing. Write operations (role_id parameter) unchanged. Co-Authored-By: Claude Sonnet 4.5 --- ami/users/api/serializers.py | 30 +++++-------------- .../tests/test_membership_management_api.py | 4 ++- ui/src/data-services/hooks/team/useMembers.ts | 6 +--- ui/src/data-services/models/member.ts | 17 +++++++++-- ui/src/data-services/models/role.ts | 2 +- .../project/team/manage-access-dialog.tsx | 4 +-- ui/src/pages/project/team/team-columns.tsx | 4 +-- 7 files changed, 31 insertions(+), 36 deletions(-) diff --git a/ami/users/api/serializers.py b/ami/users/api/serializers.py index cd097d7ef..c80492786 100644 --- a/ami/users/api/serializers.py +++ b/ami/users/api/serializers.py @@ -109,8 +109,6 @@ class UserProjectMembershipSerializer(DefaultSerializer): user = MemberUserSerializer(read_only=True) role = serializers.SerializerMethodField(read_only=True) - role_display_name = serializers.SerializerMethodField(read_only=True) - role_description = serializers.SerializerMethodField(read_only=True) class Meta: model = UserProjectMembership @@ -121,8 +119,6 @@ class Meta: "user", "project", "role", - "role_display_name", - "role_description", "created_at", "updated_at", ] @@ -132,8 +128,6 @@ class Meta: "created_at", "updated_at", "role", - "role_display_name", - "role_description", ] def validate_email(self, value): @@ -161,19 +155,13 @@ def get_role(self, obj): from ami.users.roles import Role role_cls = Role.get_primary_role(obj.project, obj.user) - return role_cls.__name__ if role_cls else None - - def get_role_display_name(self, obj): - from ami.users.roles import Role - - role_cls = Role.get_primary_role(obj.project, obj.user) - return role_cls.display_name if role_cls else None - - def get_role_description(self, obj): - from ami.users.roles import Role - - role_cls = Role.get_primary_role(obj.project, obj.user) - return role_cls.description if role_cls else None + if role_cls is None: + return None + return { + "id": role_cls.__name__, + "name": role_cls.display_name, + "description": role_cls.description, + } def validate(self, attrs): project = self.context["project"] @@ -207,8 +195,6 @@ def validate(self, attrs): class UserProjectMembershipListSerializer(UserProjectMembershipSerializer): user = MemberUserSerializer(read_only=True) role = serializers.SerializerMethodField() - role_display_name = serializers.SerializerMethodField() - role_description = serializers.SerializerMethodField() class Meta: model = UserProjectMembership @@ -216,8 +202,6 @@ class Meta: "id", "user", "role", - "role_display_name", - "role_description", "created_at", "updated_at", ] diff --git a/ami/users/tests/test_membership_management_api.py b/ami/users/tests/test_membership_management_api.py index a3e9010e5..53e7b6f80 100644 --- a/ami/users/tests/test_membership_management_api.py +++ b/ami/users/tests/test_membership_management_api.py @@ -112,7 +112,9 @@ def test_update_membership_functionality(self): self.assertEqual(resp.status_code, 200) updated = resp.json() - self.assertEqual(updated["role"], ProjectManager.__name__) + self.assertEqual(updated["role"]["id"], ProjectManager.__name__) + self.assertEqual(updated["role"]["name"], ProjectManager.display_name) + self.assertEqual(updated["role"]["description"], ProjectManager.description) membership = UserProjectMembership.objects.get( project=self.project, diff --git a/ui/src/data-services/hooks/team/useMembers.ts b/ui/src/data-services/hooks/team/useMembers.ts index 0154bbe0d..3afd07c30 100644 --- a/ui/src/data-services/hooks/team/useMembers.ts +++ b/ui/src/data-services/hooks/team/useMembers.ts @@ -14,11 +14,7 @@ const convertServerRecord = (record: ServerMember): Member => ({ id: `${record.id}`, image: record.user.image, name: record.user.name, - role: { - description: record.role_description, - id: record.role, - name: record.role_display_name, - }, + role: record.role, updatedAt: record.updated_at ? new Date(record.updated_at) : undefined, userId: `${record.user.id}`, }) diff --git a/ui/src/data-services/models/member.ts b/ui/src/data-services/models/member.ts index be3d6f31a..e7dd88f30 100644 --- a/ui/src/data-services/models/member.ts +++ b/ui/src/data-services/models/member.ts @@ -1,6 +1,19 @@ import { Role } from './role' +import { UserPermission } from 'utils/user/types' -export type ServerMember = any // TODO: Update this type +export type ServerMember = { + created_at: string + id: string + role: Role | null + updated_at: string + user: { + id: string + name: string + email: string + image?: string + } + user_permissions: UserPermission[] +} export type Member = { addedAt: Date @@ -10,7 +23,7 @@ export type Member = { id: string image?: string name: string - role: Role + role: Role | null updatedAt?: Date userId: string } diff --git a/ui/src/data-services/models/role.ts b/ui/src/data-services/models/role.ts index b51e42444..cd636467b 100644 --- a/ui/src/data-services/models/role.ts +++ b/ui/src/data-services/models/role.ts @@ -1,5 +1,5 @@ export type Role = { name: string id: string - description?: string + description: string } diff --git a/ui/src/pages/project/team/manage-access-dialog.tsx b/ui/src/pages/project/team/manage-access-dialog.tsx index 76ef5f35d..54a6082d6 100644 --- a/ui/src/pages/project/team/manage-access-dialog.tsx +++ b/ui/src/pages/project/team/manage-access-dialog.tsx @@ -19,7 +19,7 @@ import { RolesPicker } from './roles-picker' export const ManageAccessDialog = ({ member }: { member: Member }) => { const { projectId } = useParams() const [isOpen, setIsOpen] = useState(false) - const [roleId, setRoleId] = useState(member.role.id) + const [roleId, setRoleId] = useState(member.role?.id ?? '') const { updateMember, isLoading, isSuccess, error } = useUpdateMember( projectId as string, member.id @@ -27,7 +27,7 @@ export const ManageAccessDialog = ({ member }: { member: Member }) => { const errorMessage = useFormError({ error }) // Reset form on open state change - useEffect(() => setRoleId(member.role.id), [isOpen, member]) + useEffect(() => setRoleId(member.role?.id ?? ''), [isOpen, member]) return ( diff --git a/ui/src/pages/project/team/team-columns.tsx b/ui/src/pages/project/team/team-columns.tsx index c9bc313c2..c20ae1d55 100644 --- a/ui/src/pages/project/team/team-columns.tsx +++ b/ui/src/pages/project/team/team-columns.tsx @@ -48,8 +48,8 @@ export const columns: (userId?: string) => TableColumn[] = ( renderCell: (item: Member) => (
- {item.role.name} - {item.role.description ? ( + {item.role?.name ?? ''} + {item.role?.description ? (