Skip to content

Commit 1e2edda

Browse files
authored
Add support for removing users (#13329)
This PR adds support for removing `User` from an organization by the admin. The user is then removed entirely and all `Assets` are transfereed to the organization admin. - Fix #13286 - Fix enso-org/cloud-v2#2045
1 parent 0f8cfe1 commit 1e2edda

File tree

8 files changed

+54
-9
lines changed

8 files changed

+54
-9
lines changed

app/common/src/text.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ interface PlaceholderOverrides {
174174
readonly planOverriddenToX: [planName: string]
175175
readonly 'manageLabelsModal.createLabelWithTitle': [labelName: string]
176176
readonly assetsTableBackgroundRefreshIntervalOverriddenToXMs: [ms: number]
177+
readonly deleteUserConfirmation: [userUsername: string, userEmail: string]
177178
readonly willUploadUpToXFileChunksAtOnce: [parallelism: number]
178179

179180
readonly commercialUseNotice: [originalTitle: string]

app/common/src/text/english.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
"changeUserGroupsError": "Could not set user groups",
5858
"deleteUserGroupError": "Could not delete user group '$0'",
5959
"deleteUserError": "Could not delete user '$0'",
60+
"deleteUserConfirmation": "permanently delete user: '$0 ($1)'",
61+
"deleteUserAlert": "We will transfer all belonging assets to the organization admin account.",
6062
"anotherProjectIsBeingOpenedError": "Another project is currently being opened",
6163
"localBackendNotDetectedError": "Could not detect the local backend",
6264
"invalidInput": "Invalid input",

app/gui/src/dashboard/components/Alert/Alert.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const ALERT_STYLES = tv({
3535
},
3636
},
3737
slots: {
38-
iconContainer: 'mt-1',
38+
iconContainer: 'my-auto pt-1',
3939
children: 'flex flex-col items-stretch',
4040
},
4141
defaultVariants: {

app/gui/src/dashboard/layouts/Settings/MembersSettingsSection.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { Scroller } from '#/components/Scroller'
66
import { Text } from '#/components/Text'
77
import { backendMutationOptions, backendQueryOptions } from '#/hooks/backendHooks'
88
import * as billingHooks from '#/hooks/billing'
9+
import ConfirmDeleteModal from '#/modals/ConfirmDeleteModal'
910
import InviteUsersModal from '#/modals/InviteUsersModal'
11+
import { setModal } from '#/providers/ModalProvider'
1012
import type * as backendModule from '#/services/Backend'
1113
import type RemoteBackend from '#/services/RemoteBackend'
1214
import * as authProvider from '$/providers/react'
@@ -100,7 +102,12 @@ export default function MembersSettingsSection() {
100102
{getText('active')}
101103
{member.email !== user.email && isAdmin && (
102104
<Button.Group gap="small" className="mt-0.5">
103-
<RemoveMemberButton backend={backend} userId={member.userId} />
105+
<RemoveMemberButton
106+
backend={backend}
107+
userId={member.userId}
108+
userEmail={member.email}
109+
userUsername={member.name}
110+
/>
104111
</Button.Group>
105112
)}
106113
</div>
@@ -178,11 +185,13 @@ function ResendInvitationButton(props: ResendInvitationButtonProps) {
178185
interface RemoveMemberButtonProps {
179186
readonly backend: RemoteBackend
180187
readonly userId: backendModule.UserId
188+
readonly userEmail: string
189+
readonly userUsername: string
181190
}
182191

183192
/** Action button for removing a member. */
184193
function RemoveMemberButton(props: RemoveMemberButtonProps) {
185-
const { backend, userId } = props
194+
const { backend, userId, userUsername, userEmail } = props
186195
const { getText } = useText()
187196

188197
const removeMutation = useMutation(
@@ -193,7 +202,24 @@ function RemoveMemberButton(props: RemoveMemberButtonProps) {
193202
)
194203

195204
return (
196-
<Button variant="icon" size="custom" onPress={() => removeMutation.mutateAsync([userId])}>
205+
<Button
206+
variant="icon"
207+
size="custom"
208+
onPress={() => {
209+
setModal(
210+
<ConfirmDeleteModal
211+
defaultOpen={true}
212+
cannotUndo={true}
213+
actionText={getText('deleteUserConfirmation', userUsername, userEmail)}
214+
alert={getText('deleteUserAlert')}
215+
onConfirm={async () => {
216+
await removeMutation.mutateAsync([userId])
217+
}}
218+
actionButtonLabel={getText('remove')}
219+
/>,
220+
)
221+
}}
222+
>
197223
{getText('remove')}
198224
</Button>
199225
)

app/gui/src/dashboard/modals/ConfirmDeleteModal.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useText } from '$/providers/react'
77
/** Props for a {@link ConfirmDeleteModal}. */
88
export interface ConfirmDeleteModalProps extends Confirmable {
99
readonly defaultOpen?: boolean | undefined
10+
readonly alert?: string | undefined
1011
readonly cannotUndo?: boolean | undefined
1112
/** Must fit in the sentence "Are you sure you want to <action>?". */
1213
readonly actionText: string
@@ -19,6 +20,7 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
1920
const {
2021
// MUST NOT be defaulted. Omitting this value should fall back to `Dialog`'s behavior.
2122
defaultOpen,
23+
alert,
2224
cannotUndo = false,
2325
actionText,
2426
actionButtonLabel = 'Delete',
@@ -39,6 +41,12 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
3941
>
4042
<Text className="relative">{getText('confirmPrompt', actionText)}</Text>
4143

44+
{alert != null && (
45+
<Alert variant="outline" icon="warning">
46+
{alert}
47+
</Alert>
48+
)}
49+
4250
{cannotUndo && (
4351
<Alert variant="outline" icon="warning">
4452
{getText('thisOperationCannotBeUndone')}

app/gui/src/dashboard/modals/InviteUsersModal/InviteUsersForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @file A modal with inputs for user email and permission level. */
22
import { Form } from '#/components/Form'
3-
import { ResizableContentEditableInput } from '#/components/Inputs/ResizableInput'
3+
import { Input } from '#/components/Inputs/Input'
44
import * as paywallComponents from '#/components/Paywall'
55
import { backendMutationOptions } from '#/hooks/backendHooks'
66
import * as billingHooks from '#/hooks/billing'
@@ -133,7 +133,7 @@ export function InviteUsersForm(props: InviteUsersFormProps) {
133133
})
134134
}}
135135
>
136-
<ResizableContentEditableInput
136+
<Input
137137
ref={inputRef}
138138
name="emails"
139139
label={getText('inviteEmailFieldLabel')}

app/gui/src/dashboard/services/RemoteBackend.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,14 @@ export default class RemoteBackend extends Backend {
185185

186186
/**
187187
* Delete a user.
188-
* FIXME: Not implemented on backend yet.
189188
*/
190-
override async removeUser(): Promise<void> {
191-
return await this.throw(null, 'removeUserBackendError')
189+
override async removeUser(userId: backend.UserId): Promise<void> {
190+
const response = await this.delete(remoteBackendPaths.removeUserPath(userId))
191+
if (!response.ok) {
192+
return await this.throw(response, 'removeUserBackendError')
193+
} else {
194+
return
195+
}
192196
}
193197

194198
/** Invite a new user to the organization by email. */

app/gui/src/dashboard/services/remoteBackendPaths.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export function cancelSubscriptionPath(subscriptionId: backend.SubscriptionId) {
8080
return `payments/subscriptions/${subscriptionId}`
8181
}
8282

83+
/** Relative HTTP path to the "delete user" endpoint of the Cloud backend API. */
84+
export function removeUserPath(userId: backend.UserId) {
85+
return `users/${userId}`
86+
}
8387
/** Relative HTTP path to the "change user groups" endpoint of the Cloud backend API. */
8488
export function changeUserGroupPath(userId: backend.UserId) {
8589
return `users/${userId}/usergroups`

0 commit comments

Comments
 (0)