11'use client'
22
3- import { useEffect , useMemo , useRef , useState } from 'react'
3+ import { useEffect , useRef , useState } from 'react'
44import {
55 Badge ,
66 Button ,
@@ -13,6 +13,7 @@ import {
1313 Label ,
1414 Search ,
1515 Switch ,
16+ toast ,
1617} from '@sim/emcn'
1718import { getErrorMessage } from '@sim/utils/errors'
1819import { 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
6068const 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