UpdateRolesPanel.utils.ts189 lines · main
1import { groupBy, isEqual } from 'lodash'
2
3import {
4 OrganizationRole,
5 OrganizationRolesResponse,
6} from '@/data/organization-members/organization-roles-query'
7import { OrganizationMember } from '@/data/organizations/organization-members-query'
8
9export interface ProjectRoleConfiguration {
10 ref?: string
11 name?: string
12 roleId: number
13 baseRoleId?: number
14}
15
16export const formatMemberRoleToProjectRoleConfiguration = (
17 member: OrganizationMember,
18 allRoles: OrganizationRolesResponse
19) => {
20 const { org_scoped_roles, project_scoped_roles } = allRoles
21
22 const roleConfiguration = member.role_ids
23 .map((id) => {
24 const orgRole = org_scoped_roles.find((role) => role.id === id)
25 if (orgRole !== undefined) {
26 return { ref: undefined, name: undefined, roleId: orgRole.id }
27 }
28
29 const projectRole = project_scoped_roles.find((role) => role.id === id)
30 if (projectRole !== undefined) {
31 return projectRole.projects.map((x) => ({
32 ref: x.ref,
33 name: x.name,
34 roleId: projectRole.id,
35 baseRoleId: projectRole.base_role_id,
36 }))
37 }
38
39 return undefined
40 })
41 .filter(Boolean)
42 .flat()
43 .filter((p) => {
44 // [Joshen] Validate only for project scoped roles
45 // This filters out project scoped roles for projects that the user doesn't have access to
46 // (e.g if the project was deleted)
47 if ('baseRoleId' in p!) {
48 return p.ref !== undefined
49 } else {
50 return p
51 }
52 })
53 .sort((a, b) => (a?.name ?? '').localeCompare(b?.name ?? '')) as ProjectRoleConfiguration[]
54
55 return roleConfiguration
56}
57
58export const deriveChanges = (
59 original: ProjectRoleConfiguration[] = [],
60 final: ProjectRoleConfiguration[] = []
61) => {
62 const removed: ProjectRoleConfiguration[] = []
63 const added: ProjectRoleConfiguration[] = []
64 const updated: {
65 ref?: string
66 name?: string
67 originalRole: number
68 originalBaseRole?: number
69 updatedRole: number
70 }[] = []
71
72 original.forEach((x) => {
73 const updatedRoleForProject = final.find((y) => x.ref === y.ref)
74 if (updatedRoleForProject === undefined) {
75 removed.push(x)
76 } else if (updatedRoleForProject.roleId !== x.roleId) {
77 updated.push({
78 ref: updatedRoleForProject.ref,
79 name: updatedRoleForProject.name,
80 originalRole: x.roleId,
81 originalBaseRole: x.baseRoleId,
82 updatedRole: updatedRoleForProject.roleId,
83 })
84 }
85 })
86
87 final.forEach((x) => {
88 const newRoleForProject = original.find((y) => x.ref === y.ref)
89 if (newRoleForProject === undefined) {
90 added.push(x)
91 }
92 })
93
94 return { removed, added, updated }
95}
96
97// [Joshen] Essentially spit out 3 arrays
98// What to (1) Remove (2) Assign (3) Update
99// And so that we can unit test this
100export const deriveRoleChangeActions = (
101 existingRoles: OrganizationRole[],
102 changesToRoles: {
103 removed: ProjectRoleConfiguration[]
104 added: ProjectRoleConfiguration[]
105 updated: {
106 ref?: string
107 name?: string
108 originalRole: number
109 originalBaseRole?: number
110 updatedRole: number
111 }[]
112 }
113) => {
114 const { removed, added, updated } = changesToRoles
115 const toRemove: number[] = []
116 const toAssign: { roleId: number; refs: string[] }[] = []
117 const toUpdate: { roleId: number; refs: string[] }[] = []
118
119 const groupByAddedRoles = groupBy(added, 'roleId')
120 const groupByRemovedRoles = groupBy(removed, 'roleId')
121 const groupByUpdatingFromRoles = groupBy(updated, 'originalRole')
122 const groupByUpdatingToRoles = groupBy(updated, 'updatedRole')
123 const existingProjectRolesByBaseIds = existingRoles
124 .filter((r) => (r?.projects ?? []).length > 0)
125 .map((r) => r.base_role_id)
126
127 existingRoles.forEach((role) => {
128 const projectRefsApplied = role.projects.map((x) => x.ref)
129 if (projectRefsApplied.length === 0) {
130 // [Joshen] In this case we're removing an org scope role, skip all the chekcs
131 return toRemove.push(role.id)
132 }
133
134 const toRemoveRole = projectRefsApplied.every((ref) => {
135 const isRemoved = removed.map((r) => r.ref).some((x) => x === ref)
136 const isUpdated = updated.map((r) => r.ref).some((x) => x === ref)
137 return isRemoved || isUpdated
138 })
139 const isRoleGettingAdded = added.some((r) => r.roleId === role.base_role_id)
140 const isRoleGettingUpdatedTo = updated.some((r) => r.updatedRole === role.base_role_id)
141
142 if (toRemoveRole && !isRoleGettingAdded && !isRoleGettingUpdatedTo) {
143 return toRemove.push(role.id)
144 }
145
146 const projectsToAddToRole = (groupByAddedRoles[role.base_role_id]?.map((r) => r.ref) ??
147 []) as string[]
148 const projectsToRemoveFromRole = (groupByRemovedRoles[role.id]?.map((r) => r.ref) ??
149 []) as string[]
150 const projectsUpdatingFromRole = (groupByUpdatingFromRoles[role.id]?.map((r) => r.ref) ??
151 []) as string[]
152 const projectsUpdatingToRole = (groupByUpdatingToRoles[role.base_role_id]?.map((r) => r.ref) ??
153 []) as string[]
154 const projectRefsAppliedUpdated = projectRefsApplied
155 .filter((x) => !projectsToRemoveFromRole.includes(x))
156 .filter((x) => !projectsUpdatingFromRole.includes(x))
157 .concat(projectsToAddToRole)
158 .concat(projectsUpdatingToRole)
159
160 if (!isEqual(projectRefsApplied, projectRefsAppliedUpdated)) {
161 toUpdate.push({ roleId: role.id, refs: projectRefsAppliedUpdated })
162 }
163 })
164
165 Object.keys(groupByAddedRoles).forEach((roleId) => {
166 if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
167 toAssign.push({
168 roleId: Number(roleId),
169 refs: groupByAddedRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
170 })
171 }
172 })
173
174 Object.keys(groupByUpdatingToRoles).forEach((roleId) => {
175 if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
176 toAssign.push({
177 roleId: Number(roleId),
178 refs: groupByUpdatingToRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
179 })
180 }
181 })
182
183 // [Joshen] Am sorting the results just so its more deterministic when writing tests
184 return {
185 toRemove: toRemove.sort((a, b) => a - b),
186 toAssign: toAssign.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
187 toUpdate: toUpdate.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
188 }
189}