Procházet zdrojové kódy

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.
MHSanaei před 14 hodinami
rodič
revize
bf1b488a63

+ 67 - 39
frontend/src/pages/clients/ClientsPage.tsx

@@ -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>
                       }
                     >

+ 4 - 0
web/translation/en-US.json

@@ -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.",

+ 4 - 0
web/translation/fa-IR.json

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