perf(web): streamline table actions and destructive dialogs (#5645)

* perf(data-table): autosize action columns

- exclude actions columns from shared table width calculations so action cells size to their content.
- remove fixed size and w-* width overrides from feature action columns to preserve content-based layout.

* perf(data-table): streamline row action controls

- expose common edit and status actions directly while moving secondary actions into overflow menus.
- add shared row action menu helpers so static and table rows use consistent action controls.
- let action columns size to their content instead of relying on fixed widths.

* fix(web): localize destructive dialog copy

- route delete, reset, and batch update confirmation text through i18n.
- add locale entries for affected channel, model, system settings, and user dialogs.

* perf(web): unify destructive dialog actions

- align delete and cleanup confirmation buttons with the shared destructive variant.
- replace custom destructive color overrides with semantic button variants.
- clean up lint errors in touched dialog files before committing.

* fix(web): add user action success translations

- add localized success messages for user delete, status, and role changes.
- keep user management toast copy available across all frontend locales.

* fix(data-table): prevent mobile badge clipping

- expose badge cell slots so mobile card styles can target nested badge wrappers.
- reset badge margins in card rows to keep provider icons fully visible on small screens.
This commit is contained in:
QuentinHsu
2026-06-25 13:13:41 +08:00
committed by GitHub
parent b191f47375
commit 9ba251ce5f
61 changed files with 1340 additions and 1131 deletions
@@ -17,9 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState } from 'react'
import { type Row } from '@tanstack/react-table'
import type { Row } from '@tanstack/react-table'
import {
MoreHorizontal,
Pencil,
Trash2,
Power,
@@ -35,13 +34,16 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { UserSubscriptionsDialog } from '@/features/subscriptions/components/dialogs/user-subscriptions-dialog'
import { manageUser, resetUserPasskey, resetUserTwoFA } from '../api'
@@ -52,7 +54,7 @@ import {
isUserDeleted,
} from '../constants'
import { getUserActionMessage } from '../lib'
import { type User, type ManageUserAction } from '../types'
import type { User, ManageUserAction } from '../types'
import { UserBindingDialog } from './dialogs/user-binding-dialog'
import { useUsers } from './users-provider'
@@ -90,7 +92,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
result.message || t('Failed to {{action}} user', { action })
)
}
} catch (_error) {
} catch {
toast.error(t(ERROR_MESSAGES.UNEXPECTED))
}
}
@@ -104,7 +106,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
} else {
toast.error(result.message || t('Failed to reset Passkey'))
}
} catch (_error) {
} catch {
toast.error(t(ERROR_MESSAGES.UNEXPECTED))
} finally {
setResetPasskeyOpen(false)
@@ -120,7 +122,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
} else {
toast.error(result.message || t('Failed to reset 2FA'))
}
} catch (_error) {
} catch {
toast.error(t(ERROR_MESSAGES.UNEXPECTED))
} finally {
setResetTwoFAOpen(false)
@@ -136,47 +138,42 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
}
return (
<div className='-ml-2'>
<DropdownMenu>
<DropdownMenuTrigger
<div className='-ml-1.5 flex items-center gap-1'>
<Tooltip>
<TooltipTrigger
render={
<Button
variant='ghost'
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
size='icon-sm'
onClick={handleEdit}
aria-label={t('Edit')}
/>
}
>
<MoreHorizontal className='h-4 w-4' />
<span className='sr-only'>{t('Open menu')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[180px]'>
<DropdownMenuItem onClick={handleEdit}>
{t('Edit')}
<Pencil />
</TooltipTrigger>
<TooltipContent>{t('Edit')}</TooltipContent>
</Tooltip>
<DataTableRowActionMenu ariaLabel={t('Open menu')} contentClassName='w-48'>
{isDisabled ? (
<DropdownMenuItem onClick={() => handleManage('enable')}>
{t('Enable')}
<DropdownMenuShortcut>
<Pencil size={16} />
<Power size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
{isDisabled ? (
<DropdownMenuItem onClick={() => handleManage('enable')}>
{t('Enable')}
<DropdownMenuShortcut>
<Power size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={() => handleManage('disable')}
disabled={isRoot}
>
{t('Disable')}
<DropdownMenuShortcut>
<PowerOff size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
)}
) : (
<DropdownMenuItem
onClick={() => handleManage('disable')}
disabled={isRoot}
>
{t('Disable')}
<DropdownMenuShortcut>
<PowerOff size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
)}
{isAdmin && !isRoot && (
<DropdownMenuItem onClick={() => handleManage('demote')}>
@@ -260,15 +257,17 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
<Trash2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DataTableRowActionMenu>
<ConfirmDialog
open={resetPasskeyOpen}
onOpenChange={setResetPasskeyOpen}
title={t('Reset Passkey')}
desc={`Reset Passkey for ${user.username}? The user will need to register a new Passkey before using passwordless login.`}
confirmText='Reset Passkey'
desc={t(
'Reset Passkey for {{username}}? The user will need to register a new Passkey before using passwordless login.',
{ username: user.username }
)}
confirmText={t('Reset Passkey')}
handleConfirm={handleResetPasskey}
/>
@@ -276,8 +275,11 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
open={resetTwoFAOpen}
onOpenChange={setResetTwoFAOpen}
title={t('Reset Two-Factor Authentication')}
desc={`Reset 2FA for ${user.username}? The user must set up 2FA again to continue using it.`}
confirmText='Reset 2FA'
desc={t(
'Reset 2FA for {{username}}? The user must set up 2FA again to continue using it.',
{ username: user.username }
)}
confirmText={t('Reset 2FA')}
handleConfirm={handleResetTwoFA}
/>
@@ -19,16 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { deleteUser } from '../api'
import { ERROR_MESSAGES } from '../constants'
import { getUserActionMessage } from '../lib'
@@ -52,7 +43,7 @@ export function UsersDeleteDialog() {
} else {
toast.error(result.message || t(ERROR_MESSAGES.DELETE_FAILED))
}
} catch (_error) {
} catch {
toast.error(t(ERROR_MESSAGES.UNEXPECTED))
} finally {
setIsDeleting(false)
@@ -60,32 +51,21 @@ export function UsersDeleteDialog() {
}
return (
<AlertDialog
<ConfirmDialog
open={open === 'delete'}
onOpenChange={(open) => !open && setOpen(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('Are you sure?')}</AlertDialogTitle>
<AlertDialogDescription>
{t('This will permanently delete user')}{' '}
<span className='font-semibold'>{currentRow?.username}</span>
{t('. This action cannot be undone.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
{t('Cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
>
{isDeleting ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
title={t('Are you sure?')}
desc={
<>
{t('This will permanently delete user')}{' '}
<span className='font-semibold'>{currentRow?.username}</span>
{t('. This action cannot be undone.')}
</>
}
confirmText={isDeleting ? t('Deleting...') : t('Delete')}
destructive
isLoading={isDeleting}
handleConfirm={handleDelete}
/>
)
}