UpdateRolesConfirmationModal.tsx226 lines · main
1import { useQueryClient } from '@tanstack/react-query'
2import { useParams } from 'common'
3import { useState } from 'react'
4import { toast } from 'sonner'
5import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
6
7import {
8 deriveChanges,
9 deriveRoleChangeActions,
10 formatMemberRoleToProjectRoleConfiguration,
11 ProjectRoleConfiguration,
12} from './UpdateRolesPanel.utils'
13import { organizationKeys } from '@/data/organization-members/keys'
14import { useOrganizationMemberAssignRoleMutation } from '@/data/organization-members/organization-member-role-assign-mutation'
15import { useOrganizationMemberUnassignRoleMutation } from '@/data/organization-members/organization-member-role-unassign-mutation'
16import { useOrganizationMemberUpdateRoleMutation } from '@/data/organization-members/organization-member-role-update-mutation'
17import {
18 OrganizationRole,
19 useOrganizationRolesV2Query,
20} from '@/data/organization-members/organization-roles-query'
21import { organizationKeys as organizationKeysV1 } from '@/data/organizations/keys'
22import { OrganizationMember } from '@/data/organizations/organization-members-query'
23import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
24
25interface UpdateRolesConfirmationModal {
26 visible: boolean
27 member: OrganizationMember
28 projectsRoleConfiguration: ProjectRoleConfiguration[]
29 onClose: (success?: boolean) => void
30}
31
32export const UpdateRolesConfirmationModal = ({
33 visible,
34 member,
35 projectsRoleConfiguration,
36 onClose,
37}: UpdateRolesConfirmationModal) => {
38 const { slug } = useParams()
39 const queryClient = useQueryClient()
40 const { data: organization } = useSelectedOrganizationQuery()
41
42 const { data: allRoles } = useOrganizationRolesV2Query({ slug: organization?.slug })
43
44 // [Joshen] Separate saving state instead of using RQ due to several successive steps
45 const [saving, setSaving] = useState(false)
46 const { mutateAsync: assignRole } = useOrganizationMemberAssignRoleMutation()
47 const { mutateAsync: removeRole } = useOrganizationMemberUnassignRoleMutation({
48 onError: () => {},
49 })
50 const { mutateAsync: updateRole } = useOrganizationMemberUpdateRoleMutation()
51
52 const availableRoles = allRoles?.org_scoped_roles ?? []
53 const { org_scoped_roles, project_scoped_roles } = allRoles ?? {
54 org_scoped_roles: [],
55 project_scoped_roles: [],
56 }
57 const originalConfiguration =
58 allRoles !== undefined ? formatMemberRoleToProjectRoleConfiguration(member, allRoles) : []
59 const changesToRoles = deriveChanges(originalConfiguration, projectsRoleConfiguration)
60
61 const onConfirmUpdateMemberRoles = async () => {
62 if (slug === undefined) return console.error('Slug is required')
63
64 setSaving(true)
65 const gotrueId = member.gotrue_id
66 const existingRoles = member.role_ids
67 .map((id) => {
68 return [...org_scoped_roles, ...project_scoped_roles].find((r) => r.id === id)
69 })
70 .filter(Boolean) as OrganizationRole[]
71
72 const isChangeWithinOrgScope =
73 projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0].ref === undefined
74
75 // Early return if we're just updating org level roles
76 // Everything else below is just project level role changes then
77 if (isChangeWithinOrgScope) {
78 try {
79 await assignRole({
80 slug,
81 gotrueId,
82 roleId: projectsRoleConfiguration[0].roleId,
83 })
84 toast.success(`Successfully updated role for ${member.username}`)
85 onClose(true)
86 } catch (error: any) {
87 toast.error(`Failed to update role: ${error.message}`)
88 } finally {
89 setSaving(false)
90 return
91 }
92 }
93
94 const { toRemove, toAssign, toUpdate } = deriveRoleChangeActions(existingRoles, changesToRoles)
95
96 try {
97 for (const { roleId, refs } of toAssign) {
98 await assignRole({
99 slug,
100 gotrueId,
101 roleId,
102 projects: refs,
103 skipInvalidation: true,
104 })
105 }
106 for (const roleId of toRemove) {
107 await removeRole({ slug, gotrueId, roleId, skipInvalidation: true })
108 }
109 for (const { roleId, refs } of toUpdate) {
110 await updateRole({
111 slug,
112 gotrueId,
113 roleId,
114 roleName: project_scoped_roles.find((r) => r.id === roleId)?.name as string,
115 projects: refs,
116 skipInvalidation: true,
117 })
118 }
119
120 await Promise.all([
121 queryClient.invalidateQueries({ queryKey: organizationKeys.rolesV2(slug) }),
122 queryClient.invalidateQueries({ queryKey: organizationKeysV1.members(slug) }),
123 ])
124 toast.success(`Successfully updated role for ${member.username}`)
125 onClose(true)
126 } catch (error: any) {
127 toast.error(`Failed to update role: ${error.message}`)
128 } finally {
129 setSaving(false)
130 return
131 }
132 }
133
134 return (
135 <ConfirmationModal
136 size="medium"
137 visible={visible}
138 loading={saving}
139 title="Confirm to change roles of member"
140 confirmLabel="Update roles"
141 confirmLabelLoading="Updating"
142 onCancel={() => onClose()}
143 onConfirm={onConfirmUpdateMemberRoles}
144 >
145 <div className="flex flex-col gap-y-3">
146 <p className="text-sm text-foreground-light">
147 You are making the following changes to the role of{' '}
148 <span className="text-foreground">{member.username}</span> in the organization{' '}
149 <span className="text-foreground">{organization?.name}</span>:
150 </p>
151 <div className="flex flex-col gap-y-2">
152 {changesToRoles.removed.length !== 0 && (
153 <div>
154 <p className="text-sm">
155 Removing {changesToRoles.removed.length} role
156 {changesToRoles.removed.length > 1 ? 's' : ''} for user:
157 </p>
158 <ul className="list-disc pl-6">
159 {changesToRoles.removed.map((x, i) => {
160 const role =
161 org_scoped_roles.find((y) => y.id === x.roleId) ??
162 project_scoped_roles.find((y) => y.id === x.roleId)
163 const roleName = (role?.name ?? 'Unknown').split('_')[0]
164
165 return (
166 <li key={`update-${i}`} className="text-sm text-foreground-light">
167 <span className="text-foreground">{roleName}</span> on{' '}
168 <span className="text-foreground">{x?.name ?? 'organization'}</span>
169 </li>
170 )
171 })}
172 </ul>
173 </div>
174 )}
175 {changesToRoles.added.length !== 0 && (
176 <div>
177 <p className="text-sm">
178 Adding {changesToRoles.added.length} role
179 {changesToRoles.added.length > 1 ? 's' : ''} for user:
180 </p>
181 <ul className="list-disc pl-6">
182 {changesToRoles.added.map((x, i) => {
183 const role = availableRoles.find((y) => y.id === x.roleId)
184 return (
185 <li key={`update-${i}`} className="text-sm text-foreground-light">
186 <span className="text-foreground">{role?.name}</span> on{' '}
187 <span className="text-foreground">{x?.name ?? 'organization'}</span>
188 </li>
189 )
190 })}
191 </ul>
192 </div>
193 )}
194 {changesToRoles.updated.length !== 0 && (
195 <div>
196 <p className="text-sm">
197 Updating {changesToRoles.updated.length} role
198 {changesToRoles.updated.length > 1 ? 's' : ''} for user:
199 </p>
200 <ul className="list-disc pl-6">
201 {changesToRoles.updated.map((x, i) => {
202 const originalRole =
203 org_scoped_roles.find((y) => y.id === x.originalRole) ??
204 project_scoped_roles.find((y) => y.id === x.originalRole)
205 const updatedRole = org_scoped_roles.find((y) => y.id === x.updatedRole)
206 const originalRoleName = (originalRole?.name ?? 'Unknown').split('_')[0]
207
208 return (
209 <li key={`update-${i}`} className="text-sm text-foreground-light">
210 From <span className="text-foreground">{originalRoleName}</span> to{' '}
211 <span className="text-foreground">{updatedRole?.name ?? 'Unknown'}</span> on{' '}
212 <span className="text-foreground">{x?.name ?? 'organization'}</span>
213 </li>
214 )
215 })}
216 </ul>
217 </div>
218 )}
219 </div>
220 <p className="text-sm text-foreground">
221 By changing the role of this member their permissions will change.
222 </p>
223 </div>
224 </ConfirmationModal>
225 )
226}