feat(clients): tidier bulk action toolbar

When at least one client is selected, the toolbar now collapses to a
small selection indicator plus the three most-used actions instead of
spreading six count-suffixed buttons across the row:

- Replaces every per-button "(N)" with a single closable "{N} selected"
  tag on the left — one click on its × clears the selection.
- Hides "+ Add Clients" while a selection is active (focus mode).
- Keeps Attach, Detach, and Delete as visible buttons; Delete is pushed
  to the right with auto margin so it doesn't sit flush against the
  non-destructive actions.
- Folds Adjust, Group, and Sub links into the existing "more"
  dropdown, which is now context-aware: selection-scoped overflow when
  rows are picked, global actions (Add Bulk / Reset all / Del depleted)
  otherwise.

On mobile the new buttons collapse to icon-only the same way as the
rest of the toolbar.
This commit is contained in:
MHSanaei
2026-05-28 11:24:21 +02:00
parent 8d6d845262
commit bf1b488a63
3 changed files with 75 additions and 39 deletions

View File

@@ -783,28 +783,25 @@ export default function ClientsPage() {
hoverable
title={
<div className="card-toolbar">
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
{!isMobile && t('pages.clients.addClients')}
</Button>
{selectedRowKeys.length > 0 && (
{selectedRowKeys.length === 0 ? (
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
{!isMobile && t('pages.clients.addClients')}
</Button>
) : (
<>
<Button icon={<ClockCircleOutlined />} onClick={() => setBulkAdjustOpen(true)}>
{t('pages.clients.adjustSelected', { count: selectedRowKeys.length })}
</Button>
<Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
{t('pages.clients.assignGroupSelected', { count: selectedRowKeys.length })}
</Button>
<Tag
color="blue"
closable
onClose={() => setSelectedRowKeys([])}
style={{ marginInlineEnd: 0, padding: '4px 8px', fontSize: 13 }}
>
{t('pages.clients.selectedCount', { count: selectedRowKeys.length })}
</Tag>
<Button icon={<UsergroupAddOutlined />} onClick={() => setBulkAttachOpen(true)}>
{t('pages.clients.attachSelected', { count: selectedRowKeys.length })}
{!isMobile && t('pages.clients.attach')}
</Button>
<Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
{t('pages.clients.detachSelected', { count: selectedRowKeys.length })}
</Button>
<Button icon={<LinkOutlined />} onClick={() => setSubLinksOpen(true)}>
{t('pages.clients.subLinksSelected', { count: selectedRowKeys.length })}
</Button>
<Button danger icon={<DeleteOutlined />} onClick={onBulkDelete}>
{t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
{!isMobile && t('pages.clients.detach')}
</Button>
</>
)}
@@ -812,33 +809,64 @@ export default function ClientsPage() {
trigger={['click']}
placement="bottomRight"
menu={{
items: [
{
key: 'bulk',
icon: <UsergroupAddOutlined />,
label: t('pages.clients.bulk'),
onClick: () => setBulkAddOpen(true),
},
{
key: 'resetAll',
icon: <RetweetOutlined />,
label: t('pages.clients.resetAllTraffics'),
onClick: onResetAllTraffics,
},
{
key: 'delDepleted',
icon: <RestOutlined />,
label: t('pages.clients.delDepleted'),
danger: true,
onClick: onDelDepleted,
},
],
items: selectedRowKeys.length > 0
? [
{
key: 'adjust',
icon: <ClockCircleOutlined />,
label: t('pages.clients.adjust'),
onClick: () => setBulkAdjustOpen(true),
},
{
key: 'group',
icon: <TagsOutlined />,
label: t('pages.clients.group'),
onClick: () => setBulkGroupOpen(true),
},
{
key: 'subLinks',
icon: <LinkOutlined />,
label: t('pages.clients.subLinks'),
onClick: () => setSubLinksOpen(true),
},
]
: [
{
key: 'bulk',
icon: <UsergroupAddOutlined />,
label: t('pages.clients.bulk'),
onClick: () => setBulkAddOpen(true),
},
{
key: 'resetAll',
icon: <RetweetOutlined />,
label: t('pages.clients.resetAllTraffics'),
onClick: onResetAllTraffics,
},
{
key: 'delDepleted',
icon: <RestOutlined />,
label: t('pages.clients.delDepleted'),
danger: true,
onClick: onDelDepleted,
},
],
}}
>
<Button icon={<MoreOutlined />}>
{!isMobile && t('more')}
</Button>
</Dropdown>
{selectedRowKeys.length > 0 && (
<Button
danger
icon={<DeleteOutlined />}
onClick={onBulkDelete}
style={{ marginInlineStart: 'auto' }}
>
{!isMobile && t('delete')}
</Button>
)}
</div>
}
>

View File

@@ -542,6 +542,10 @@
"assignGroupPlaceholder": "Group name (leave blank to clear)",
"assignGroupAssignedToast": "Assigned {count} client(s) to {group}",
"assignGroupClearedToast": "Cleared group from {count} client(s)",
"attach": "Attach",
"adjust": "Adjust",
"subLinks": "Sub links",
"selectedCount": "{count} selected",
"attachSelected": "Attach ({count})",
"attachToInboundsTitle": "Attach {count} client(s) to inbound(s)",
"attachToInboundsDesc": "Attaches the selected {count} client(s) (same UUID/password and shared traffic) to the chosen inbound(s). They keep their existing attachments too.",

View File

@@ -513,6 +513,10 @@
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
"deleteSelected": "حذف ({count})",
"adjustSelected": "تنظیم ({count})",
"attach": "اتصال",
"adjust": "تنظیم",
"subLinks": "لینک‌های ساب",
"selectedCount": "{count} انتخاب‌شده",
"attachSelected": "اتصال ({count})",
"attachToInboundsTitle": "اتصال {count} کلاینت به اینباند(ها)",
"attachToInboundsDesc": "{count} کلاینت انتخاب‌شده (با همان UUID/پسورد و ترافیک مشترک) به اینباند(های) انتخابی متصل می‌شوند. روی اینباندهای فعلی هم باقی می‌مانند.",