mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-08 10:46:58 +00:00
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:
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user