Skip to content

Commit e4955c6

Browse files
committed
fix(user-modal): enhance actions menu with improved dropdown interactions and state management
1 parent c89b5ff commit e4955c6

File tree

2 files changed

+52
-24
lines changed

2 files changed

+52
-24
lines changed

dashboard/src/components/dialogs/user-modal.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
310310
const [isUserAllIPsModalOpen, setUserAllIPsModalOpen] = useState(false)
311311
const [isUsageModalOpen, setUsageModalOpen] = useState(false)
312312
const [isSubscriptionClientsModalOpen, setSubscriptionClientsModalOpen] = useState(false)
313+
const [isActionsMenuOpen, setActionsMenuOpen] = useState(false)
313314

314315
// Watch next plan values directly for reactivity
315316
const nextPlanUserTemplateId = form.watch('next_plan.user_template_id')
@@ -342,6 +343,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
342343
setOnHoldCalendarOpen(false)
343344
setNextPlanEnabled(false)
344345
setNextPlanManuallyDisabled(false)
346+
setActionsMenuOpen(false)
345347
} else {
346348
setNextPlanManuallyDisabled(false)
347349
if (editingUser) {
@@ -2414,40 +2416,60 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
24142416
{renderUserMetaPanel('mt-4 lg:hidden')}
24152417
</div>
24162418
{/* Cancel/Create buttons - always visible */}
2417-
<div className="mt-4 flex flex-row items-center justify-between gap-3 overflow-x-auto pb-1">
2419+
<div className="mt-4 flex flex-row items-center justify-end gap-3 overflow-x-auto">
24182420
{editingUser && (
2419-
<DropdownMenu>
2421+
<DropdownMenu modal={false} open={isActionsMenuOpen} onOpenChange={setActionsMenuOpen}>
24202422
<DropdownMenuTrigger asChild>
24212423
<Button
24222424
type="button"
24232425
variant="outline"
2426+
size="icon"
24242427
aria-label={t('actions', { defaultValue: 'Actions' })}
2425-
className="group h-9 border-border/70 bg-background/80 px-3 shadow-sm backdrop-blur transition-all hover:border-primary/40 hover:bg-primary/5 hover:shadow-md focus-visible:ring-2 focus-visible:ring-primary/30 data-[state=open]:border-primary/50 data-[state=open]:bg-primary/10"
2428+
className="group h-10 w-10 border-border/70 bg-background/80 shadow-sm backdrop-blur transition-all hover:border-primary/40 hover:bg-primary/5 hover:shadow-md focus-visible:ring-2 focus-visible:ring-primary/30 data-[state=open]:border-primary/50 data-[state=open]:bg-primary/10"
24262429
>
2427-
<span className="text-xs font-medium">{t('actions', { defaultValue: 'Actions' })}</span>
2428-
<EllipsisVertical className="ml-1 h-4 w-4 text-muted-foreground transition-all duration-200 group-hover:text-foreground group-data-[state=open]:rotate-90 group-data-[state=open]:text-foreground" />
2430+
<EllipsisVertical className="h-4 w-4 text-muted-foreground transition-all duration-200 group-hover:text-foreground group-data-[state=open]:rotate-90 group-data-[state=open]:text-foreground" />
24292431
</Button>
24302432
</DropdownMenuTrigger>
2431-
<DropdownMenuContent align="end">
2433+
<DropdownMenuContent
2434+
align="start"
2435+
onEscapeKeyDown={() => setActionsMenuOpen(false)}
2436+
onPointerDownOutside={() => setActionsMenuOpen(false)}
2437+
onInteractOutside={() => setActionsMenuOpen(false)}
2438+
>
24322439
{isSudo && (
2433-
<DropdownMenuItem onClick={() => setUserAllIPsModalOpen(true)}>
2440+
<DropdownMenuItem onSelect={() => {
2441+
setActionsMenuOpen(false)
2442+
setUserAllIPsModalOpen(true)
2443+
}}>
24342444
<Network className="mr-2 h-4 w-4" />
24352445
<span>{t('userAllIPs.ipAddresses', { defaultValue: 'IP addresses' })}</span>
24362446
</DropdownMenuItem>
24372447
)}
2438-
<DropdownMenuItem onClick={() => setUsageModalOpen(true)}>
2448+
<DropdownMenuItem onSelect={() => {
2449+
setActionsMenuOpen(false)
2450+
setUsageModalOpen(true)
2451+
}}>
24392452
<PieChart className="mr-2 h-4 w-4" />
24402453
<span>{t('userDialog.usage', { defaultValue: 'Usage' })}</span>
24412454
</DropdownMenuItem>
2442-
<DropdownMenuItem onClick={() => setSubscriptionClientsModalOpen(true)}>
2455+
<DropdownMenuItem onSelect={() => {
2456+
setActionsMenuOpen(false)
2457+
setSubscriptionClientsModalOpen(true)
2458+
}}>
24432459
<Users className="mr-2 h-4 w-4" />
24442460
<span>{t('subscriptionClients.clients', { defaultValue: 'Clients' })}</span>
24452461
</DropdownMenuItem>
2446-
<DropdownMenuItem onClick={() => setRevokeSubDialogOpen(true)}>
2462+
<DropdownMenuItem onSelect={() => {
2463+
setActionsMenuOpen(false)
2464+
setRevokeSubDialogOpen(true)
2465+
}}>
24472466
<Link2Off className="mr-2 h-4 w-4" />
24482467
<span>{t('userDialog.revokeSubscription', { defaultValue: 'Revoke subscription' })}</span>
24492468
</DropdownMenuItem>
2450-
<DropdownMenuItem onClick={() => setResetUsageDialogOpen(true)}>
2469+
<DropdownMenuItem onSelect={() => {
2470+
setActionsMenuOpen(false)
2471+
setResetUsageDialogOpen(true)
2472+
}}>
24512473
<RefreshCcw className="mr-2 h-4 w-4" />
24522474
<span>{t('userDialog.resetUsage', { defaultValue: 'Reset usage' })}</span>
24532475
</DropdownMenuItem>

dashboard/src/components/users/action-buttons.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
4646
const [isActiveNextPlanModalOpen, setIsActiveNextPlanModalOpen] = useState(false)
4747
const [isSubscriptionClientsModalOpen, setSubscriptionClientsModalOpen] = useState(false)
4848
const [isUserAllIPsModalOpen, setUserAllIPsModalOpen] = useState(false)
49+
const [isActionsMenuOpen, setActionsMenuOpen] = useState(false)
4950
const queryClient = useQueryClient()
5051
const { t } = useTranslation()
5152
const dir = useDirDetection()
@@ -450,36 +451,41 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
450451
<TooltipContent>{copied ? t('usersTable.copied') : t('usersTable.copyConfigs')}</TooltipContent>
451452
</Tooltip>
452453
</TooltipProvider>
453-
<DropdownMenu>
454+
<DropdownMenu modal={false} open={isActionsMenuOpen} onOpenChange={setActionsMenuOpen}>
454455
<DropdownMenuTrigger asChild>
455456
<Button size="icon" variant="ghost">
456457
<EllipsisVertical className="h-4 w-4" />
457458
</Button>
458459
</DropdownMenuTrigger>
459-
<DropdownMenuContent align="end">
460+
<DropdownMenuContent
461+
align="end"
462+
onPointerDownOutside={() => setActionsMenuOpen(false)}
463+
onInteractOutside={() => setActionsMenuOpen(false)}
464+
onEscapeKeyDown={() => setActionsMenuOpen(false)}
465+
>
460466
{/* Edit */}
461-
<DropdownMenuItem className="hidden md:flex" onClick={handleEdit}>
467+
<DropdownMenuItem className="hidden md:flex" onSelect={handleEdit}>
462468
<Pencil className="mr-2 h-4 w-4" />
463469
<span>{t('edit')}</span>
464470
</DropdownMenuItem>
465471

466472
{/* QR Code */}
467-
<DropdownMenuItem onClick={onOpenSubscriptionModal}>
473+
<DropdownMenuItem onSelect={onOpenSubscriptionModal}>
468474
<QrCode className="mr-2 h-4 w-4" />
469475
<span>QR Code</span>
470476
</DropdownMenuItem>
471477

472478
{/* Set Owner: only for sudo admins */}
473479
{currentAdmin?.is_sudo && (
474-
<DropdownMenuItem onClick={handleSetOwner}>
480+
<DropdownMenuItem onSelect={handleSetOwner}>
475481
<User className="mr-2 h-4 w-4" />
476482
<span>{t('setOwnerModal.title')}</span>
477483
</DropdownMenuItem>
478484
)}
479485

480486
{/* Copy Core Username for sudo admins */}
481487
{currentAdmin?.is_sudo && (
482-
<DropdownMenuItem onClick={handleCopyCoreUsername}>
488+
<DropdownMenuItem onSelect={handleCopyCoreUsername}>
483489
<Cpu className="mr-2 h-4 w-4" />
484490
<span>{t('coreUsername')}</span>
485491
</DropdownMenuItem>
@@ -488,40 +494,40 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
488494
<DropdownMenuSeparator />
489495

490496
{/* Revoke Sub */}
491-
<DropdownMenuItem onClick={handleRevokeSubscription}>
497+
<DropdownMenuItem onSelect={handleRevokeSubscription}>
492498
<Link2Off className="mr-2 h-4 w-4" />
493499
<span>{t('userDialog.revokeSubscription')}</span>
494500
</DropdownMenuItem>
495501

496502
{/* Reset Usage */}
497-
<DropdownMenuItem onClick={handleResetUsage}>
503+
<DropdownMenuItem onSelect={handleResetUsage}>
498504
<RefreshCcw className="mr-2 h-4 w-4" />
499505
<span>{t('userDialog.resetUsage')}</span>
500506
</DropdownMenuItem>
501507

502508
{/* Usage State */}
503-
<DropdownMenuItem onClick={handleUsageState}>
509+
<DropdownMenuItem onSelect={handleUsageState}>
504510
<PieChart className="mr-2 h-4 w-4" />
505511
<span>{t('userDialog.usage')}</span>
506512
</DropdownMenuItem>
507513

508514
{/* Active Next Plan */}
509515
{user.next_plan && (
510-
<DropdownMenuItem onClick={handleActiveNextPlan}>
516+
<DropdownMenuItem onSelect={handleActiveNextPlan}>
511517
<ListStart className="mr-2 h-4 w-4" />
512518
<span>{t('usersTable.activeNextPlanSubmit')}</span>
513519
</DropdownMenuItem>
514520
)}
515521

516522
{/* Subscription Info */}
517-
<DropdownMenuItem onClick={() => setSubscriptionClientsModalOpen(true)}>
523+
<DropdownMenuItem onSelect={() => setSubscriptionClientsModalOpen(true)}>
518524
<Users className="mr-2 h-4 w-4" />
519525
<span>{t('subscriptionClients.clients', { defaultValue: 'Clients' })}</span>
520526
</DropdownMenuItem>
521527

522528
{/* View All IPs: only for sudo admins */}
523529
{currentAdmin?.is_sudo && (
524-
<DropdownMenuItem onClick={() => setUserAllIPsModalOpen(true)}>
530+
<DropdownMenuItem onSelect={() => setUserAllIPsModalOpen(true)}>
525531
<Network className="mr-2 h-4 w-4" />
526532
<span>{t('userAllIPs.ipAddresses', { defaultValue: 'IP addresses' })}</span>
527533
</DropdownMenuItem>
@@ -530,7 +536,7 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
530536
<DropdownMenuSeparator />
531537

532538
{/* Trash */}
533-
<DropdownMenuItem onClick={handleDelete} className="text-red-600">
539+
<DropdownMenuItem onSelect={handleDelete} className="text-red-600">
534540
<Trash2 className="mr-2 h-4 w-4" />
535541
<span>{t('remove')}</span>
536542
</DropdownMenuItem>

0 commit comments

Comments
 (0)