Skip to content

Commit a1e9b22

Browse files
committed
improvement(admin): simplify the user row actions after review passes
1 parent c8ce9d7 commit a1e9b22

1 file changed

Lines changed: 44 additions & 78 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/settings/components/admin

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

Lines changed: 44 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useMemo, useRef, useState } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
44
import {
55
Badge,
66
Button,
@@ -13,6 +13,7 @@ import {
1313
Label,
1414
Search,
1515
Switch,
16+
toast,
1617
} from '@sim/emcn'
1718
import { getErrorMessage } from '@sim/utils/errors'
1819
import { useQueryStates } from 'nuqs'
@@ -54,8 +55,15 @@ const USER_TABLE_HEADER = (
5455
</div>
5556
)
5657

57-
/** The row action awaiting confirmation in {@link ChipConfirmModal}. */
58-
type PendingUserAction = { type: 'ban'; user: AdminUser } | { type: 'role'; user: AdminUser }
58+
/**
59+
* The row action awaiting confirmation in {@link ChipConfirmModal}. Holds only
60+
* the id so the modal reads the live row — a background refetch while it is
61+
* open must not leave the confirm acting on a stale role.
62+
*/
63+
interface PendingUserAction {
64+
type: 'ban' | 'role'
65+
userId: string
66+
}
5967

6068
const MOTHERSHIP_ENV_OPTIONS: { value: MothershipEnvironment; label: string }[] = [
6169
{ value: 'default', label: 'Default' },
@@ -171,7 +179,12 @@ export function Admin() {
171179
)
172180
}
173181

174-
const isDemotion = pendingAction?.user.role === 'admin'
182+
const pendingUser = pendingAction
183+
? (usersData?.users.find((u) => u.id === pendingAction.userId) ??
184+
recentUsers?.find((u) => u.id === pendingAction.userId) ??
185+
null)
186+
: null
187+
const isDemotion = pendingUser?.role === 'admin'
175188

176189
const closePendingAction = () => {
177190
setPendingAction(null)
@@ -181,55 +194,30 @@ export function Admin() {
181194
const handleConfirmBan = () => {
182195
if (pendingAction?.type !== 'ban') return
183196
const trimmedReason = banReason.trim()
184-
banUser.reset()
185197
banUser.mutate(
186198
{
187-
userId: pendingAction.user.id,
199+
userId: pendingAction.userId,
188200
...(trimmedReason ? { banReason: trimmedReason } : {}),
189201
},
190202
{ onSuccess: closePendingAction }
191203
)
192204
}
193205

194206
const handleConfirmRoleChange = () => {
195-
if (pendingAction?.type !== 'role') return
196-
setUserRole.reset()
207+
if (pendingAction?.type !== 'role' || !pendingUser) return
197208
setUserRole.mutate(
198-
{
199-
userId: pendingAction.user.id,
200-
role: pendingAction.user.role === 'admin' ? 'user' : 'admin',
201-
},
209+
{ userId: pendingUser.id, role: isDemotion ? 'user' : 'admin' },
202210
{ onSuccess: closePendingAction }
203211
)
204212
}
205213

206-
const pendingUserIds = useMemo(() => {
207-
const ids = new Set<string>()
208-
if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId)
209-
ids.add((setUserRole.variables as { userId: string }).userId)
210-
if (banUser.isPending && (banUser.variables as { userId?: string })?.userId)
211-
ids.add((banUser.variables as { userId: string }).userId)
212-
if (unbanUser.isPending && (unbanUser.variables as { userId?: string })?.userId)
213-
ids.add((unbanUser.variables as { userId: string }).userId)
214-
if (impersonateUser.isPending && (impersonateUser.variables as { userId?: string })?.userId)
215-
ids.add((impersonateUser.variables as { userId: string }).userId)
216-
if (sendPasswordReset.isPending && sendPasswordReset.variables?.userId)
217-
ids.add(sendPasswordReset.variables.userId)
218-
if (impersonatingUserId) ids.add(impersonatingUserId)
219-
return ids
220-
}, [
221-
setUserRole.isPending,
222-
setUserRole.variables,
223-
banUser.isPending,
224-
banUser.variables,
225-
unbanUser.isPending,
226-
unbanUser.variables,
227-
impersonateUser.isPending,
228-
impersonateUser.variables,
229-
sendPasswordReset.isPending,
230-
sendPasswordReset.variables,
231-
impersonatingUserId,
232-
])
214+
/** Rows with an action in flight, whose remaining actions stay disabled. */
215+
const pendingUserIds = new Set<string>()
216+
for (const mutation of [setUserRole, banUser, unbanUser, impersonateUser, sendPasswordReset]) {
217+
if (mutation.isPending && mutation.variables?.userId)
218+
pendingUserIds.add(mutation.variables.userId)
219+
}
220+
if (impersonatingUserId) pendingUserIds.add(impersonatingUserId)
233221

234222
const renderUserRow = (u: AdminUser) => (
235223
<div key={u.id} className='flex items-center gap-3 px-3 py-2 text-small'>
@@ -244,53 +232,48 @@ export function Admin() {
244232
<span className='flex w-[150px] items-center justify-end gap-1'>
245233
{u.id !== session?.user?.id && (
246234
<>
247-
<Button
248-
variant='active'
249-
className='h-[28px] px-2 text-caption'
235+
<Chip
250236
onClick={() => handleImpersonate(u.id, u.email)}
251237
disabled={pendingUserIds.has(u.id)}
252238
>
253-
{impersonatingUserId === u.id ||
254-
(impersonateUser.isPending &&
255-
(impersonateUser.variables as { userId?: string } | undefined)?.userId === u.id)
256-
? 'Switching...'
257-
: 'Impersonate'}
258-
</Button>
239+
{impersonatingUserId === u.id ? 'Switching...' : 'Impersonate'}
240+
</Chip>
259241
<RowActionsMenu
260-
label={`Actions for ${u.email}`}
242+
label={`${u.email} actions`}
261243
actions={[
262244
{
263245
label: 'Reset password',
264246
onSelect: () => {
265247
setProvisionWarning(null)
266-
sendPasswordReset.reset()
267-
sendPasswordReset.mutate({ userId: u.id, email: u.email })
248+
sendPasswordReset.mutate(
249+
{ userId: u.id, email: u.email },
250+
{
251+
onSuccess: () => toast.success(`Password reset email sent to ${u.email}`),
252+
}
253+
)
268254
},
269255
disabled: pendingUserIds.has(u.id),
270256
},
271257
{
272258
label: u.role === 'admin' ? 'Demote' : 'Promote',
273259
onSelect: () => {
274260
setUserRole.reset()
275-
setPendingAction({ type: 'role', user: u })
261+
setPendingAction({ type: 'role', userId: u.id })
276262
},
277263
disabled: pendingUserIds.has(u.id),
278264
},
279265
u.banned
280266
? {
281267
label: 'Unban',
282-
onSelect: () => {
283-
unbanUser.reset()
284-
unbanUser.mutate({ userId: u.id })
285-
},
268+
onSelect: () => unbanUser.mutate({ userId: u.id }),
286269
disabled: pendingUserIds.has(u.id),
287270
}
288271
: {
289272
label: 'Ban',
290273
onSelect: () => {
291274
banUser.reset()
292275
setBanReason('')
293-
setPendingAction({ type: 'ban', user: u })
276+
setPendingAction({ type: 'ban', userId: u.id })
294277
},
295278
destructive: true,
296279
disabled: pendingUserIds.has(u.id),
@@ -412,21 +395,13 @@ export function Admin() {
412395
</p>
413396
)}
414397

415-
{(setUserRole.error ||
416-
banUser.error ||
417-
unbanUser.error ||
398+
{(unbanUser.error ||
418399
impersonateUser.error ||
419400
sendPasswordReset.error ||
420401
impersonationGuardError) && (
421402
<p className='text-[var(--text-error)] text-small'>
422403
{impersonationGuardError ||
423-
(
424-
setUserRole.error ||
425-
banUser.error ||
426-
unbanUser.error ||
427-
impersonateUser.error ||
428-
sendPasswordReset.error
429-
)?.message ||
404+
(unbanUser.error || impersonateUser.error || sendPasswordReset.error)?.message ||
430405
'Action failed. Please try again.'}
431406
</p>
432407
)}
@@ -435,15 +410,6 @@ export function Admin() {
435410
<p className='text-[var(--text-error)] text-small'>{provisionWarning}</p>
436411
)}
437412

438-
{sendPasswordReset.variables &&
439-
(sendPasswordReset.isPending || sendPasswordReset.isSuccess) && (
440-
<p className='text-[var(--text-secondary)] text-small'>
441-
{sendPasswordReset.isPending
442-
? `Sending a password reset email to ${sendPasswordReset.variables.email}...`
443-
: `Password reset email sent to ${sendPasswordReset.variables.email}.`}
444-
</p>
445-
)}
446-
447413
{searchQuery.length > 0 && usersData ? (
448414
<>
449415
<div className='flex flex-col gap-0.5'>
@@ -509,7 +475,7 @@ export function Admin() {
509475
title='Ban user'
510476
text={[
511477
'Banning ',
512-
{ text: pendingAction?.user.email ?? 'this user', bold: true },
478+
{ text: pendingUser?.email ?? 'this user', bold: true },
513479
' ',
514480
{
515481
text: 'signs them out everywhere and blocks them from signing back in.',
@@ -544,7 +510,7 @@ export function Admin() {
544510
title={isDemotion ? 'Demote user' : 'Promote user'}
545511
text={[
546512
isDemotion ? 'Demoting ' : 'Promoting ',
547-
{ text: pendingAction?.user.email ?? 'this user', bold: true },
513+
{ text: pendingUser?.email ?? 'this user', bold: true },
548514
' ',
549515
isDemotion
550516
? { text: 'revokes their platform admin access.', error: true }

0 commit comments

Comments
 (0)