mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-03 17:07:15 +00:00
feat(clients): selective bulk attach + new bulk detach
Inbounds page: - AttachClientsModal now shows a per-client selection table (email, comment, enabled tag) with search and a live "selected of total" counter; all clients are pre-selected so the old "attach all" workflow stays a single OK click. - New DetachClientsModal on the inbound row menu lets you pick which clients to remove from that inbound (records are kept so they can be re-attached later; for full removal use Delete). Clients page: - New "Attach (N)" bulk-action button + BulkAttachInboundsModal that attaches selected clients to one or more multi-user inbounds. - New "Detach (N)" bulk-action button + BulkDetachInboundsModal that removes selected clients from chosen inbounds; (email, inbound) pairs where the client isn't attached are silently skipped. Backend adds POST /panel/api/clients/bulkDetach, wrapping the existing Detach service for each email and reporting per-email detached/skipped/errors. ClientRecord rows are kept on detach to match the single-client endpoint; bulkDel remains the path for full removal.
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
TagsOutlined,
|
||||
TeamOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
@@ -62,6 +63,8 @@ const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
|
||||
const FilterDrawer = lazy(() => import('./FilterDrawer'));
|
||||
const SubLinksModal = lazy(() => import('./SubLinksModal'));
|
||||
const BulkAssignGroupModal = lazy(() => import('./BulkAssignGroupModal'));
|
||||
const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
|
||||
const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
|
||||
import { emptyFilters, activeFilterCount } from './filters';
|
||||
import type { ClientFilters } from './filters';
|
||||
import './ClientsPage.css';
|
||||
@@ -149,7 +152,7 @@ export default function ClientsPage() {
|
||||
setQuery,
|
||||
inbounds, onlines, loading, fetched, subSettings,
|
||||
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
|
||||
create, update, remove, bulkDelete, bulkAdjust, bulkAssignGroup, attach, detach,
|
||||
create, update, remove, bulkDelete, bulkAdjust, bulkAssignGroup, attach, bulkAttach, detach, bulkDetach,
|
||||
resetTraffic, resetAllTraffics, delDepleted, setEnable,
|
||||
applyTrafficEvent, applyClientStatsEvent,
|
||||
hydrate,
|
||||
@@ -173,6 +176,8 @@ export default function ClientsPage() {
|
||||
const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
|
||||
const [subLinksOpen, setSubLinksOpen] = useState(false);
|
||||
const [bulkGroupOpen, setBulkGroupOpen] = useState(false);
|
||||
const [bulkAttachOpen, setBulkAttachOpen] = useState(false);
|
||||
const [bulkDetachOpen, setBulkDetachOpen] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const initial = readFilterState();
|
||||
@@ -789,6 +794,12 @@ export default function ClientsPage() {
|
||||
<Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
|
||||
{t('pages.clients.assignGroupSelected', { count: selectedRowKeys.length })}
|
||||
</Button>
|
||||
<Button icon={<UsergroupAddOutlined />} onClick={() => setBulkAttachOpen(true)}>
|
||||
{t('pages.clients.attachSelected', { count: selectedRowKeys.length })}
|
||||
</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>
|
||||
@@ -1157,6 +1168,38 @@ export default function ClientsPage() {
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={bulkAttachOpen}>
|
||||
<BulkAttachInboundsModal
|
||||
open={bulkAttachOpen}
|
||||
count={selectedRowKeys.length}
|
||||
inbounds={inbounds}
|
||||
onOpenChange={setBulkAttachOpen}
|
||||
onSubmit={async (inboundIds) => {
|
||||
const msg = await bulkAttach([...selectedRowKeys], inboundIds);
|
||||
if (msg?.success) {
|
||||
setSelectedRowKeys([]);
|
||||
return msg.obj ?? { attached: [], skipped: [], errors: [] };
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={bulkDetachOpen}>
|
||||
<BulkDetachInboundsModal
|
||||
open={bulkDetachOpen}
|
||||
count={selectedRowKeys.length}
|
||||
inbounds={inbounds}
|
||||
onOpenChange={setBulkDetachOpen}
|
||||
onSubmit={async (inboundIds) => {
|
||||
const msg = await bulkDetach([...selectedRowKeys], inboundIds);
|
||||
if (msg?.success) {
|
||||
setSelectedRowKeys([]);
|
||||
return msg.obj ?? { detached: [], skipped: [], errors: [] };
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={filterDrawerOpen}>
|
||||
<FilterDrawer
|
||||
open={filterDrawerOpen}
|
||||
|
||||
Reference in New Issue
Block a user