From 5a93b2ee8b7a7f78ac054a0191eb11e8887fb145 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:05:20 -0700 Subject: [PATCH 1/8] improvement(admin): move user row actions into an overflow menu with confirm modals --- .../settings/components/admin/admin.tsx | 295 ++++++++++-------- 1 file changed, 173 insertions(+), 122 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index c7cd6f626af..e8c358fcd70 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -1,7 +1,18 @@ 'use client' import { useEffect, useMemo, useRef, useState } from 'react' -import { Badge, Button, Chip, ChipInput, ChipSelect, cn, Label, Search, Switch } from '@sim/emcn' +import { + Badge, + Button, + Chip, + ChipConfirmModal, + ChipInput, + ChipModalField, + ChipSelect, + Label, + Search, + Switch, +} from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { useQueryStates } from 'nuqs' import type { MothershipEnvironment } from '@/lib/api/contracts' @@ -12,6 +23,7 @@ import { adminUrlKeys, } from '@/app/workspace/[workspaceId]/settings/components/admin/search-params' import { useRecentImpersonations } from '@/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations' +import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' @@ -37,10 +49,13 @@ const USER_TABLE_HEADER = ( Email Role Status - Actions + Actions ) +/** The row action awaiting confirmation in {@link ChipConfirmModal}. */ +type PendingUserAction = { type: 'ban'; user: AdminUser } | { type: 'role'; user: AdminUser } + const MOTHERSHIP_ENV_OPTIONS: { value: MothershipEnvironment; label: string }[] = [ { value: 'default', label: 'Default' }, { value: 'dev', label: 'Dev' }, @@ -72,7 +87,7 @@ export function Admin() { ) const [searchInput, setSearchInput] = useState(searchQuery) - const [banUserId, setBanUserId] = useState(null) + const [pendingAction, setPendingAction] = useState(null) const [banReason, setBanReason] = useState('') const [impersonatingUserId, setImpersonatingUserId] = useState(null) const [impersonationGuardError, setImpersonationGuardError] = useState(null) @@ -155,6 +170,38 @@ export function Admin() { ) } + const isDemotion = pendingAction?.user.role === 'admin' + + const closePendingAction = () => { + setPendingAction(null) + setBanReason('') + } + + const handleConfirmBan = () => { + if (pendingAction?.type !== 'ban') return + const trimmedReason = banReason.trim() + banUser.reset() + banUser.mutate( + { + userId: pendingAction.user.id, + ...(trimmedReason ? { banReason: trimmedReason } : {}), + }, + { onSuccess: closePendingAction } + ) + } + + const handleConfirmRoleChange = () => { + if (pendingAction?.type !== 'role') return + setUserRole.reset() + setUserRole.mutate( + { + userId: pendingAction.user.id, + role: pendingAction.user.role === 'admin' ? 'user' : 'admin', + }, + { onSuccess: closePendingAction } + ) + } + const pendingUserIds = useMemo(() => { const ids = new Set() if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId) @@ -183,7 +230,7 @@ export function Admin() { impersonatingUserId, ]) - /** Confirms the send in place, since nothing about the user row changes. */ + /** Confirms the send on the menu item itself, since nothing about the user row changes. */ const resetPasswordLabel = (userId: string) => { if (sendPasswordReset.variables?.userId !== userId) return 'Reset password' if (sendPasswordReset.isPending) return 'Sending...' @@ -192,126 +239,70 @@ export function Admin() { } const renderUserRow = (u: AdminUser) => ( -
-
- {u.name || '—'} - {u.email} - - {u.role || 'user'} - - - {u.banned ? Banned : Active} - - - {u.id !== session?.user?.id && ( - <> - - - - {u.banned ? ( - - ) : ( - - )} - - )} - -
- {banUserId === u.id && !u.banned && ( -
- setBanReason(e.target.value)} - placeholder='Reason (optional)' - className='flex-1' - /> - + { + setProvisionWarning(null) + sendPasswordReset.reset() + sendPasswordReset.mutate({ userId: u.id, email: u.email }) + }, + disabled: pendingUserIds.has(u.id), }, { - onSuccess: () => { - setBanUserId(null) - setBanReason('') - }, - } - ) - }} - disabled={pendingUserIds.has(u.id)} - > - Confirm Ban - -
- )} + label: u.role === 'admin' ? 'Demote' : 'Promote', + onSelect: () => setPendingAction({ type: 'role', user: u }), + disabled: pendingUserIds.has(u.id), + }, + u.banned + ? { + label: 'Unban', + onSelect: () => { + unbanUser.reset() + unbanUser.mutate({ userId: u.id }) + }, + disabled: pendingUserIds.has(u.id), + } + : { + label: 'Ban', + onSelect: () => { + setBanReason('') + setPendingAction({ type: 'ban', user: u }) + }, + destructive: true, + disabled: pendingUserIds.has(u.id), + }, + ]} + /> + + )} +
) @@ -503,6 +494,66 @@ export function Admin() { )} + { + if (!open) closePendingAction() + }} + srTitle='Ban user' + title='Ban user' + text={[ + 'Banning ', + { text: pendingAction?.user.email ?? 'this user', bold: true }, + ' ', + { + text: 'signs them out everywhere and blocks them from signing back in.', + error: true, + }, + ' You can unban them later.', + ]} + confirm={{ + label: 'Ban', + onClick: handleConfirmBan, + pending: banUser.isPending, + pendingLabel: 'Banning...', + }} + > + + + + { + if (!open) closePendingAction() + }} + srTitle={isDemotion ? 'Demote user' : 'Promote user'} + title={isDemotion ? 'Demote user' : 'Promote user'} + text={[ + isDemotion ? 'Demoting ' : 'Promoting ', + { text: pendingAction?.user.email ?? 'this user', bold: true }, + ' ', + isDemotion + ? { text: 'revokes their platform admin access.', error: true } + : { + text: 'grants full platform admin access, including impersonating any user.', + error: true, + }, + ]} + confirm={{ + label: isDemotion ? 'Demote' : 'Promote', + onClick: handleConfirmRoleChange, + pending: setUserRole.isPending, + pendingLabel: isDemotion ? 'Demoting...' : 'Promoting...', + }} + /> + Date: Fri, 7 Aug 2026 12:10:39 -0700 Subject: [PATCH 2/8] fix(admin): surface password reset status outside the actions menu --- .../settings/components/admin/admin.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index e8c358fcd70..1ec0d26ea45 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -230,14 +230,6 @@ export function Admin() { impersonatingUserId, ]) - /** Confirms the send on the menu item itself, since nothing about the user row changes. */ - const resetPasswordLabel = (userId: string) => { - if (sendPasswordReset.variables?.userId !== userId) return 'Reset password' - if (sendPasswordReset.isPending) return 'Sending...' - if (sendPasswordReset.isSuccess) return 'Reset sent' - return 'Reset password' - } - const renderUserRow = (u: AdminUser) => (
{u.name || '—'} @@ -267,7 +259,7 @@ export function Admin() { label={`Actions for ${u.email}`} actions={[ { - label: resetPasswordLabel(u.id), + label: 'Reset password', onSelect: () => { setProvisionWarning(null) sendPasswordReset.reset() @@ -438,6 +430,15 @@ export function Admin() {

{provisionWarning}

)} + {sendPasswordReset.variables && + (sendPasswordReset.isPending || sendPasswordReset.isSuccess) && ( +

+ {sendPasswordReset.isPending + ? `Sending a password reset email to ${sendPasswordReset.variables.email}...` + : `Password reset email sent to ${sendPasswordReset.variables.email}.`} +

+ )} + {searchQuery.length > 0 && usersData ? ( <>
From a2874b2d96693ed278187ae135dd75ab5c2a0db8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:17:14 -0700 Subject: [PATCH 3/8] fix(admin): surface ban and role change errors inside the confirm modal --- .../[workspaceId]/settings/components/admin/admin.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 1ec0d26ea45..c3c0386e683 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -7,6 +7,7 @@ import { Chip, ChipConfirmModal, ChipInput, + ChipModalError, ChipModalField, ChipSelect, Label, @@ -269,7 +270,10 @@ export function Admin() { }, { label: u.role === 'admin' ? 'Demote' : 'Promote', - onSelect: () => setPendingAction({ type: 'role', user: u }), + onSelect: () => { + setUserRole.reset() + setPendingAction({ type: 'role', user: u }) + }, disabled: pendingUserIds.has(u.id), }, u.banned @@ -527,6 +531,7 @@ export function Admin() { placeholder='Optional' disabled={banUser.isPending} /> + {banUser.error?.message} + > + {setUserRole.error?.message} + Date: Fri, 7 Aug 2026 12:22:55 -0700 Subject: [PATCH 4/8] fix(admin): reset the ban mutation when opening the confirm modal --- .../workspace/[workspaceId]/settings/components/admin/admin.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index c3c0386e683..41ca38c3d54 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -288,6 +288,7 @@ export function Admin() { : { label: 'Ban', onSelect: () => { + banUser.reset() setBanReason('') setPendingAction({ type: 'ban', user: u }) }, From c6ead9db9e627805645e23960743e8fca869a697 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:32:40 -0700 Subject: [PATCH 5/8] improvement(admin): simplify the user row actions after review passes --- .../settings/components/admin/admin.tsx | 122 +++++++----------- 1 file changed, 44 insertions(+), 78 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 41ca38c3d54..45ab2a0b588 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Badge, Button, @@ -13,6 +13,7 @@ import { Label, Search, Switch, + toast, } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { useQueryStates } from 'nuqs' @@ -54,8 +55,15 @@ const USER_TABLE_HEADER = (
) -/** The row action awaiting confirmation in {@link ChipConfirmModal}. */ -type PendingUserAction = { type: 'ban'; user: AdminUser } | { type: 'role'; user: AdminUser } +/** + * The row action awaiting confirmation in {@link ChipConfirmModal}. Holds only + * the id so the modal reads the live row — a background refetch while it is + * open must not leave the confirm acting on a stale role. + */ +interface PendingUserAction { + type: 'ban' | 'role' + userId: string +} const MOTHERSHIP_ENV_OPTIONS: { value: MothershipEnvironment; label: string }[] = [ { value: 'default', label: 'Default' }, @@ -171,7 +179,12 @@ export function Admin() { ) } - const isDemotion = pendingAction?.user.role === 'admin' + const pendingUser = pendingAction + ? (usersData?.users.find((u) => u.id === pendingAction.userId) ?? + recentUsers?.find((u) => u.id === pendingAction.userId) ?? + null) + : null + const isDemotion = pendingUser?.role === 'admin' const closePendingAction = () => { setPendingAction(null) @@ -181,10 +194,9 @@ export function Admin() { const handleConfirmBan = () => { if (pendingAction?.type !== 'ban') return const trimmedReason = banReason.trim() - banUser.reset() banUser.mutate( { - userId: pendingAction.user.id, + userId: pendingAction.userId, ...(trimmedReason ? { banReason: trimmedReason } : {}), }, { onSuccess: closePendingAction } @@ -192,44 +204,20 @@ export function Admin() { } const handleConfirmRoleChange = () => { - if (pendingAction?.type !== 'role') return - setUserRole.reset() + if (pendingAction?.type !== 'role' || !pendingUser) return setUserRole.mutate( - { - userId: pendingAction.user.id, - role: pendingAction.user.role === 'admin' ? 'user' : 'admin', - }, + { userId: pendingUser.id, role: isDemotion ? 'user' : 'admin' }, { onSuccess: closePendingAction } ) } - const pendingUserIds = useMemo(() => { - const ids = new Set() - if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId) - ids.add((setUserRole.variables as { userId: string }).userId) - if (banUser.isPending && (banUser.variables as { userId?: string })?.userId) - ids.add((banUser.variables as { userId: string }).userId) - if (unbanUser.isPending && (unbanUser.variables as { userId?: string })?.userId) - ids.add((unbanUser.variables as { userId: string }).userId) - if (impersonateUser.isPending && (impersonateUser.variables as { userId?: string })?.userId) - ids.add((impersonateUser.variables as { userId: string }).userId) - if (sendPasswordReset.isPending && sendPasswordReset.variables?.userId) - ids.add(sendPasswordReset.variables.userId) - if (impersonatingUserId) ids.add(impersonatingUserId) - return ids - }, [ - setUserRole.isPending, - setUserRole.variables, - banUser.isPending, - banUser.variables, - unbanUser.isPending, - unbanUser.variables, - impersonateUser.isPending, - impersonateUser.variables, - sendPasswordReset.isPending, - sendPasswordReset.variables, - impersonatingUserId, - ]) + /** Rows with an action in flight, whose remaining actions stay disabled. */ + const pendingUserIds = new Set() + for (const mutation of [setUserRole, banUser, unbanUser, impersonateUser, sendPasswordReset]) { + if (mutation.isPending && mutation.variables?.userId) + pendingUserIds.add(mutation.variables.userId) + } + if (impersonatingUserId) pendingUserIds.add(impersonatingUserId) const renderUserRow = (u: AdminUser) => (
@@ -244,27 +232,25 @@ export function Admin() { {u.id !== session?.user?.id && ( <> - + {impersonatingUserId === u.id ? 'Switching...' : 'Impersonate'} + { setProvisionWarning(null) - sendPasswordReset.reset() - sendPasswordReset.mutate({ userId: u.id, email: u.email }) + sendPasswordReset.mutate( + { userId: u.id, email: u.email }, + { + onSuccess: () => toast.success(`Password reset email sent to ${u.email}`), + } + ) }, disabled: pendingUserIds.has(u.id), }, @@ -272,17 +258,14 @@ export function Admin() { label: u.role === 'admin' ? 'Demote' : 'Promote', onSelect: () => { setUserRole.reset() - setPendingAction({ type: 'role', user: u }) + setPendingAction({ type: 'role', userId: u.id }) }, disabled: pendingUserIds.has(u.id), }, u.banned ? { label: 'Unban', - onSelect: () => { - unbanUser.reset() - unbanUser.mutate({ userId: u.id }) - }, + onSelect: () => unbanUser.mutate({ userId: u.id }), disabled: pendingUserIds.has(u.id), } : { @@ -290,7 +273,7 @@ export function Admin() { onSelect: () => { banUser.reset() setBanReason('') - setPendingAction({ type: 'ban', user: u }) + setPendingAction({ type: 'ban', userId: u.id }) }, destructive: true, disabled: pendingUserIds.has(u.id), @@ -412,21 +395,13 @@ export function Admin() {

)} - {(setUserRole.error || - banUser.error || - unbanUser.error || + {(unbanUser.error || impersonateUser.error || sendPasswordReset.error || impersonationGuardError) && (

{impersonationGuardError || - ( - setUserRole.error || - banUser.error || - unbanUser.error || - impersonateUser.error || - sendPasswordReset.error - )?.message || + (unbanUser.error || impersonateUser.error || sendPasswordReset.error)?.message || 'Action failed. Please try again.'}

)} @@ -435,15 +410,6 @@ export function Admin() {

{provisionWarning}

)} - {sendPasswordReset.variables && - (sendPasswordReset.isPending || sendPasswordReset.isSuccess) && ( -

- {sendPasswordReset.isPending - ? `Sending a password reset email to ${sendPasswordReset.variables.email}...` - : `Password reset email sent to ${sendPasswordReset.variables.email}.`} -

- )} - {searchQuery.length > 0 && usersData ? ( <>
@@ -509,7 +475,7 @@ export function Admin() { title='Ban user' text={[ 'Banning ', - { text: pendingAction?.user.email ?? 'this user', bold: true }, + { text: pendingUser?.email ?? 'this user', bold: true }, ' ', { text: 'signs them out everywhere and blocks them from signing back in.', @@ -544,7 +510,7 @@ export function Admin() { title={isDemotion ? 'Demote user' : 'Promote user'} text={[ isDemotion ? 'Demoting ' : 'Promoting ', - { text: pendingAction?.user.email ?? 'this user', bold: true }, + { text: pendingUser?.email ?? 'this user', bold: true }, ' ', isDemotion ? { text: 'revokes their platform admin access.', error: true } From f93de3434dbbce4a304d252e6430352a55cbb496 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:38:32 -0700 Subject: [PATCH 6/8] fix(admin): show password reset progress while the request is in flight --- .../[workspaceId]/settings/components/admin/admin.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 45ab2a0b588..7f81d594e0d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -410,6 +410,12 @@ export function Admin() {

{provisionWarning}

)} + {sendPasswordReset.isPending && sendPasswordReset.variables && ( +

+ Sending a password reset email to {sendPasswordReset.variables.email}... +

+ )} + {searchQuery.length > 0 && usersData ? ( <>
From d36229d6fe9cefafe17278efb8183a242b361e1a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:44:51 -0700 Subject: [PATCH 7/8] fix(admin): confirm the role change the admin chose, not the live row's inverse --- .../settings/components/admin/admin.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 7f81d594e0d..fc6c99e45ab 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -56,14 +56,14 @@ const USER_TABLE_HEADER = ( ) /** - * The row action awaiting confirmation in {@link ChipConfirmModal}. Holds only - * the id so the modal reads the live row — a background refetch while it is - * open must not leave the confirm acting on a stale role. + * The row action awaiting confirmation in {@link ChipConfirmModal}. Carries the + * id plus, for a role change, the role the admin chose to apply — never the + * whole user row. A refetch while the modal is open therefore refreshes the + * name it shows without ever redirecting the action the admin committed to. */ -interface PendingUserAction { - type: 'ban' | 'role' - userId: string -} +type PendingUserAction = + | { type: 'ban'; userId: string } + | { type: 'role'; userId: string; nextRole: 'admin' | 'user' } const MOTHERSHIP_ENV_OPTIONS: { value: MothershipEnvironment; label: string }[] = [ { value: 'default', label: 'Default' }, @@ -184,7 +184,7 @@ export function Admin() { recentUsers?.find((u) => u.id === pendingAction.userId) ?? null) : null - const isDemotion = pendingUser?.role === 'admin' + const isDemotion = pendingAction?.type === 'role' && pendingAction.nextRole === 'user' const closePendingAction = () => { setPendingAction(null) @@ -204,9 +204,9 @@ export function Admin() { } const handleConfirmRoleChange = () => { - if (pendingAction?.type !== 'role' || !pendingUser) return + if (pendingAction?.type !== 'role') return setUserRole.mutate( - { userId: pendingUser.id, role: isDemotion ? 'user' : 'admin' }, + { userId: pendingAction.userId, role: pendingAction.nextRole }, { onSuccess: closePendingAction } ) } @@ -258,7 +258,11 @@ export function Admin() { label: u.role === 'admin' ? 'Demote' : 'Promote', onSelect: () => { setUserRole.reset() - setPendingAction({ type: 'role', userId: u.id }) + setPendingAction({ + type: 'role', + userId: u.id, + nextRole: u.role === 'admin' ? 'user' : 'admin', + }) }, disabled: pendingUserIds.has(u.id), }, From 731f1d7134c8435b82a177aeb592a7735be06b97 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 13:05:51 -0700 Subject: [PATCH 8/8] improvement(admin): align the role confirm and reset feedback with house patterns --- .../settings/components/admin/admin.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index fc6c99e45ab..3e5d8f4e1d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -56,10 +56,9 @@ const USER_TABLE_HEADER = ( ) /** - * The row action awaiting confirmation in {@link ChipConfirmModal}. Carries the - * id plus, for a role change, the role the admin chose to apply — never the - * whole user row. A refetch while the modal is open therefore refreshes the - * name it shows without ever redirecting the action the admin committed to. + * The row action awaiting confirmation. Holds ids, never the user row, so a + * refetch while the modal is open refreshes the name it shows without + * redirecting the action the admin committed to. */ type PendingUserAction = | { type: 'ban'; userId: string } @@ -211,7 +210,6 @@ export function Admin() { ) } - /** Rows with an action in flight, whose remaining actions stay disabled. */ const pendingUserIds = new Set() for (const mutation of [setUserRole, banUser, unbanUser, impersonateUser, sendPasswordReset]) { if (mutation.isPending && mutation.variables?.userId) @@ -233,6 +231,7 @@ export function Admin() { {u.id !== session?.user?.id && ( <> handleImpersonate(u.id, u.email)} disabled={pendingUserIds.has(u.id)} > @@ -249,6 +248,13 @@ export function Admin() { { userId: u.id, email: u.email }, { onSuccess: () => toast.success(`Password reset email sent to ${u.email}`), + onError: (error) => + toast.error( + getErrorMessage( + error, + `Could not send a password reset email to ${u.email}` + ) + ), } ) }, @@ -399,13 +405,10 @@ export function Admin() {

)} - {(unbanUser.error || - impersonateUser.error || - sendPasswordReset.error || - impersonationGuardError) && ( + {(unbanUser.error || impersonateUser.error || impersonationGuardError) && (

{impersonationGuardError || - (unbanUser.error || impersonateUser.error || sendPasswordReset.error)?.message || + (unbanUser.error || impersonateUser.error)?.message || 'Action failed. Please try again.'}

)} @@ -524,14 +527,12 @@ export function Admin() { ' ', isDemotion ? { text: 'revokes their platform admin access.', error: true } - : { - text: 'grants full platform admin access, including impersonating any user.', - error: true, - }, + : 'grants full platform admin access, including impersonating any user.', ]} confirm={{ label: isDemotion ? 'Demote' : 'Promote', onClick: handleConfirmRoleChange, + variant: isDemotion ? 'destructive' : 'primary', pending: setUserRole.isPending, pendingLabel: isDemotion ? 'Demoting...' : 'Promoting...', }}