Skip to content

Commit bf08d8a

Browse files
mihowclaude
andcommitted
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 <noreply@anthropic.com>
1 parent ae5ef7b commit bf08d8a

7 files changed

Lines changed: 31 additions & 36 deletions

File tree

ami/users/api/serializers.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ class UserProjectMembershipSerializer(DefaultSerializer):
109109

110110
user = MemberUserSerializer(read_only=True)
111111
role = serializers.SerializerMethodField(read_only=True)
112-
role_display_name = serializers.SerializerMethodField(read_only=True)
113-
role_description = serializers.SerializerMethodField(read_only=True)
114112

115113
class Meta:
116114
model = UserProjectMembership
@@ -121,8 +119,6 @@ class Meta:
121119
"user",
122120
"project",
123121
"role",
124-
"role_display_name",
125-
"role_description",
126122
"created_at",
127123
"updated_at",
128124
]
@@ -132,8 +128,6 @@ class Meta:
132128
"created_at",
133129
"updated_at",
134130
"role",
135-
"role_display_name",
136-
"role_description",
137131
]
138132

139133
def validate_email(self, value):
@@ -161,19 +155,13 @@ def get_role(self, obj):
161155
from ami.users.roles import Role
162156

163157
role_cls = Role.get_primary_role(obj.project, obj.user)
164-
return role_cls.__name__ if role_cls else None
165-
166-
def get_role_display_name(self, obj):
167-
from ami.users.roles import Role
168-
169-
role_cls = Role.get_primary_role(obj.project, obj.user)
170-
return role_cls.display_name if role_cls else None
171-
172-
def get_role_description(self, obj):
173-
from ami.users.roles import Role
174-
175-
role_cls = Role.get_primary_role(obj.project, obj.user)
176-
return role_cls.description if role_cls else None
158+
if role_cls is None:
159+
return None
160+
return {
161+
"id": role_cls.__name__,
162+
"name": role_cls.display_name,
163+
"description": role_cls.description,
164+
}
177165

178166
def validate(self, attrs):
179167
project = self.context["project"]
@@ -207,17 +195,13 @@ def validate(self, attrs):
207195
class UserProjectMembershipListSerializer(UserProjectMembershipSerializer):
208196
user = MemberUserSerializer(read_only=True)
209197
role = serializers.SerializerMethodField()
210-
role_display_name = serializers.SerializerMethodField()
211-
role_description = serializers.SerializerMethodField()
212198

213199
class Meta:
214200
model = UserProjectMembership
215201
fields = [
216202
"id",
217203
"user",
218204
"role",
219-
"role_display_name",
220-
"role_description",
221205
"created_at",
222206
"updated_at",
223207
]

ami/users/tests/test_membership_management_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def test_update_membership_functionality(self):
112112
self.assertEqual(resp.status_code, 200)
113113

114114
updated = resp.json()
115-
self.assertEqual(updated["role"], ProjectManager.__name__)
115+
self.assertEqual(updated["role"]["id"], ProjectManager.__name__)
116+
self.assertEqual(updated["role"]["name"], ProjectManager.display_name)
117+
self.assertEqual(updated["role"]["description"], ProjectManager.description)
116118

117119
membership = UserProjectMembership.objects.get(
118120
project=self.project,

ui/src/data-services/hooks/team/useMembers.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ const convertServerRecord = (record: ServerMember): Member => ({
1414
id: `${record.id}`,
1515
image: record.user.image,
1616
name: record.user.name,
17-
role: {
18-
description: record.role_description,
19-
id: record.role,
20-
name: record.role_display_name,
21-
},
17+
role: record.role,
2218
updatedAt: record.updated_at ? new Date(record.updated_at) : undefined,
2319
userId: `${record.user.id}`,
2420
})
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { Role } from './role'
2+
import { UserPermission } from 'utils/user/types'
23

3-
export type ServerMember = any // TODO: Update this type
4+
export type ServerMember = {
5+
created_at: string
6+
id: string
7+
role: Role | null
8+
updated_at: string
9+
user: {
10+
id: string
11+
name: string
12+
email: string
13+
image?: string
14+
}
15+
user_permissions: UserPermission[]
16+
}
417

518
export type Member = {
619
addedAt: Date
@@ -10,7 +23,7 @@ export type Member = {
1023
id: string
1124
image?: string
1225
name: string
13-
role: Role
26+
role: Role | null
1427
updatedAt?: Date
1528
userId: string
1629
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export type Role = {
22
name: string
33
id: string
4-
description?: string
4+
description: string
55
}

ui/src/pages/project/team/manage-access-dialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ import { RolesPicker } from './roles-picker'
1919
export const ManageAccessDialog = ({ member }: { member: Member }) => {
2020
const { projectId } = useParams()
2121
const [isOpen, setIsOpen] = useState(false)
22-
const [roleId, setRoleId] = useState<string>(member.role.id)
22+
const [roleId, setRoleId] = useState<string>(member.role?.id ?? '')
2323
const { updateMember, isLoading, isSuccess, error } = useUpdateMember(
2424
projectId as string,
2525
member.id
2626
)
2727
const errorMessage = useFormError({ error })
2828

2929
// Reset form on open state change
30-
useEffect(() => setRoleId(member.role.id), [isOpen, member])
30+
useEffect(() => setRoleId(member.role?.id ?? ''), [isOpen, member])
3131

3232
return (
3333
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>

ui/src/pages/project/team/team-columns.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export const columns: (userId?: string) => TableColumn<Member>[] = (
4848
renderCell: (item: Member) => (
4949
<BasicTableCell>
5050
<div className="flex items-center gap-2">
51-
<span>{item.role.name}</span>
52-
{item.role.description ? (
51+
<span>{item.role?.name ?? ''}</span>
52+
{item.role?.description ? (
5353
<BasicTooltip asChild content={item.role.description}>
5454
<Button
5555
aria-label={translate(STRING.ABOUT_ROLE)}

0 commit comments

Comments
 (0)