✨ refactor(ui): Improve usage log filter responsiveness and mobile UX
Refactor the usage log filter toolbar into a shared reusable component for common, drawing, and task logs. Optimize desktop filters with a responsive grid, move secondary filters into a mobile drawer, standardize filter typography, remove redundant filter icons, and add the missing i18n translations for the new drawer description.
This commit is contained in:
+46
-41
@@ -30,7 +30,8 @@ import {
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { LongText } from '@/components/long-text'
|
||||
import { StatusBadge, dotColorMap } from '@/components/status-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { USER_STATUSES, USER_ROLES, isUserDeleted } from '../constants'
|
||||
import { type User } from '../types'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
@@ -73,7 +74,9 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
<DataTableColumnHeader column={column} title='ID' />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return <div className='w-[60px]'>{row.getValue('id')}</div>
|
||||
return (
|
||||
<TableId value={row.getValue('id') as number} className='w-[60px]' />
|
||||
)
|
||||
},
|
||||
meta: { label: t('ID'), mobileHidden: true },
|
||||
},
|
||||
@@ -140,7 +143,6 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
<StatusBadge
|
||||
label={t(statusConfig.labelKey)}
|
||||
variant={statusConfig.variant}
|
||||
showDot={statusConfig.showDot}
|
||||
copyable={false}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
@@ -276,59 +278,62 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
const inviterId = user.inviter_id || 0
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-1.5 text-xs font-medium'>
|
||||
<span
|
||||
className={cn(
|
||||
'size-1.5 shrink-0 rounded-full',
|
||||
dotColorMap.neutral
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<span className='text-muted-foreground cursor-help' />}
|
||||
>
|
||||
{t('Invited')}: {affCount}
|
||||
</TooltipTrigger>
|
||||
render={
|
||||
<StatusBadge
|
||||
label={`${t('Invited')}: ${affCount}`}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p className='text-xs'>{t('Number of users invited')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<span className='text-muted-foreground cursor-help' />}
|
||||
>
|
||||
{t('Revenue')}: {formatQuota(affHistoryQuota)}
|
||||
</TooltipTrigger>
|
||||
render={
|
||||
<StatusBadge
|
||||
label={`${t('Revenue')}: ${formatQuota(affHistoryQuota)}`}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p className='text-xs'>{t('Total invitation revenue')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{inviterId > 0 && (
|
||||
<>
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className='text-muted-foreground cursor-help' />
|
||||
}
|
||||
>
|
||||
{t('Inviter')}: {inviterId}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className='text-xs'>
|
||||
{t('Invited by user ID')} {inviterId}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={`${t('Inviter')}: ${inviterId}`}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
className='cursor-help'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p className='text-xs'>
|
||||
{t('Invited by user ID')} {inviterId}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{inviterId === 0 && (
|
||||
<>
|
||||
<span className='text-muted-foreground/30'>·</span>
|
||||
<span className='text-muted-foreground'>{t('No Inviter')}</span>
|
||||
</>
|
||||
<StatusBadge
|
||||
label={t('No Inviter')}
|
||||
variant='neutral'
|
||||
copyable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -55,6 +55,13 @@ import {
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
SideDrawerSection,
|
||||
sideDrawerContentClassName,
|
||||
sideDrawerFooterClassName,
|
||||
sideDrawerFormClassName,
|
||||
sideDrawerHeaderClassName,
|
||||
} from '@/components/drawer-layout'
|
||||
import { createUser, updateUser, getUser, getGroups } from '../api'
|
||||
import { BINDING_FIELDS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
|
||||
import {
|
||||
@@ -182,8 +189,10 @@ export function UsersMutateDrawer({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'>
|
||||
<SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
|
||||
<SheetContent
|
||||
className={sideDrawerContentClassName('sm:max-w-[600px]')}
|
||||
>
|
||||
<SheetHeader className={sideDrawerHeaderClassName()}>
|
||||
<SheetTitle>
|
||||
{isUpdate ? t('Update') : t('Create')} {t('User')}
|
||||
</SheetTitle>
|
||||
@@ -197,10 +206,10 @@ export function UsersMutateDrawer({
|
||||
<form
|
||||
id='user-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
|
||||
className={sideDrawerFormClassName()}
|
||||
>
|
||||
{/* Basic Information */}
|
||||
<div className='space-y-4'>
|
||||
<SideDrawerSection>
|
||||
<h3 className='text-sm font-medium'>
|
||||
{t('Basic Information')}
|
||||
</h3>
|
||||
@@ -304,11 +313,11 @@ export function UsersMutateDrawer({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</SideDrawerSection>
|
||||
|
||||
{/* Group & Quota Settings (Update only) */}
|
||||
{isUpdate && (
|
||||
<div className='space-y-4'>
|
||||
<SideDrawerSection>
|
||||
<h3 className='text-sm font-medium'>{t('Group & Quota')}</h3>
|
||||
|
||||
<FormField
|
||||
@@ -405,12 +414,12 @@ export function UsersMutateDrawer({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</SideDrawerSection>
|
||||
)}
|
||||
|
||||
{/* Binding Information (Read-only) */}
|
||||
{isUpdate && (
|
||||
<div className='space-y-4'>
|
||||
<SideDrawerSection>
|
||||
<h3 className='text-sm font-medium'>
|
||||
{t('Binding Information')}
|
||||
</h3>
|
||||
@@ -420,7 +429,7 @@ export function UsersMutateDrawer({
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='flex flex-col gap-3'>
|
||||
{BINDING_FIELDS.map(({ key, label }) => (
|
||||
<div key={key}>
|
||||
<Label className='text-muted-foreground text-xs'>
|
||||
@@ -436,11 +445,11 @@ export function UsersMutateDrawer({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SideDrawerSection>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
|
||||
<SheetFooter className={sideDrawerFooterClassName()}>
|
||||
<SheetClose render={<Button variant='outline' />}>
|
||||
{t('Close')}
|
||||
</SheetClose>
|
||||
|
||||
-3
@@ -41,19 +41,16 @@ export const USER_STATUSES = {
|
||||
labelKey: 'Enabled',
|
||||
variant: 'success' as const,
|
||||
value: USER_STATUS.ENABLED,
|
||||
showDot: true,
|
||||
},
|
||||
[USER_STATUS.DISABLED]: {
|
||||
labelKey: 'Disabled',
|
||||
variant: 'neutral' as const,
|
||||
value: USER_STATUS.DISABLED,
|
||||
showDot: true,
|
||||
},
|
||||
DELETED: {
|
||||
labelKey: 'Deleted',
|
||||
variant: 'danger' as const,
|
||||
value: -1,
|
||||
showDot: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
Reference in New Issue
Block a user