mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 07:27:13 +00:00
refactor(clients): coherent group management — rename, split, extract
This bundles a set of group-related improvements that built up across one session and only make sense together. Terminology / API surface: - Rename "assign group" → "add to group" everywhere: i18n keys, callback names (bulkAddToGroup), component + file names (BulkAddToGroupModal, AddClientsToGroupModal), Go controller/struct names (bulkAddToGroup, AddToGroup), OpenAPI summaries. Nothing keeps the word "assign" anymore. - Move group routes under /panel/api/clients/groups/* (was /bulkAssignGroup at the clients root). - Split add and remove into two endpoints: /groups/bulkAdd now rejects empty group; new /groups/bulkRemove clears the label for the given emails. The old "submit empty to clear" UX is gone — Ungroup is its own action. UI affordances on Clients page: - Promote Group + Ungroup to visible bar buttons next to Attach + Detach. Group reuses BulkAddToGroupModal; Ungroup pops a danger confirm and calls bulkRemoveFromGroup. - Custom UngroupIcon (TagsOutlined with a diagonal strike) for the Ungroup button so the pairing reads at a glance. - Hide the Group column when no clients have a group label yet — removes a column of em-dashes on fresh installs. UI on Groups page: - New per-row Add clients… / Remove clients… actions backed by GroupAddClientsModal and GroupRemoveClientsModal: rich client picker (email / comment / current group / enable) with search and preserveSelectedRowKeys, mirroring the inbounds Attach modal UX. Controller split: - Move all /groups/* routes, handlers, and request bodies out of web/controller/client.go into a dedicated web/controller/group.go (GroupController with leaner clientService + xrayService dependencies). URLs are byte-identical because the new controller registers on the same parent gin.RouterGroup; api_docs_test.go gets a group.go → /panel/api/clients basePath entry so its route extraction keeps working. Invalidation dedup: - Removing a client from a group on the Groups page used to refetch /clients/groups and /clients/onlines three times: once from the mutation's onSuccess, once from a redundant invalidate() in the page's onSubmit, once from the WebSocket invalidate broadcast that the backend fires after every mutation. The manual invalidate() is gone, and a small invalidationTracker module lets websocketBridge skip WS-driven invalidates that arrive within 1.5s of a local invalidate — bringing the refetch count down to one. The WS path still works for changes made by another tab or user.
This commit is contained in:
+9
-9
@@ -3,13 +3,13 @@ import { lazy, useEffect, useMemo, useState } from 'react';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
||||
|
||||
const BulkAssignGroupModal = lazy(() => import('@/pages/clients/BulkAssignGroupModal'));
|
||||
const BulkAddToGroupModal = lazy(() => import('@/pages/clients/BulkAddToGroupModal'));
|
||||
|
||||
interface AssignClientsGroupModalProps {
|
||||
interface AddClientsToGroupModalProps {
|
||||
open: boolean;
|
||||
source: DBInbound | null;
|
||||
onClose: () => void;
|
||||
onAssigned?: () => void;
|
||||
onAdded?: () => void;
|
||||
}
|
||||
|
||||
function readClientEmails(settings: unknown): string[] {
|
||||
@@ -18,12 +18,12 @@ function readClientEmails(settings: unknown): string[] {
|
||||
return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
export default function AssignClientsGroupModal({
|
||||
export default function AddClientsToGroupModal({
|
||||
open,
|
||||
source,
|
||||
onClose,
|
||||
onAssigned,
|
||||
}: AssignClientsGroupModalProps) {
|
||||
onAdded,
|
||||
}: AddClientsToGroupModalProps) {
|
||||
const [groups, setGroups] = useState<string[]>([]);
|
||||
|
||||
const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
|
||||
@@ -41,19 +41,19 @@ export default function AssignClientsGroupModal({
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<BulkAssignGroupModal
|
||||
<BulkAddToGroupModal
|
||||
open={open}
|
||||
count={emails.length}
|
||||
groups={groups}
|
||||
onOpenChange={(o) => { if (!o) onClose(); }}
|
||||
onSubmit={async (group) => {
|
||||
const msg = await HttpUtil.post(
|
||||
'/panel/api/clients/bulkAssignGroup',
|
||||
'/panel/api/clients/groups/bulkAdd',
|
||||
{ emails, group },
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
);
|
||||
if (!msg?.success) return null;
|
||||
onAssigned?.();
|
||||
onAdded?.();
|
||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
||||
}}
|
||||
/>
|
||||
@@ -263,7 +263,7 @@ function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients }: { r
|
||||
if (isInboundMultiUser(record) && hasClients) {
|
||||
items.push({ key: 'attachClients', icon: <UsergroupAddOutlined />, label: t('pages.inbounds.attachClients') });
|
||||
items.push({ key: 'detachClients', icon: <UsergroupDeleteOutlined />, label: t('pages.inbounds.detachClients') });
|
||||
items.push({ key: 'assignGroup', icon: <TagsOutlined />, label: t('pages.inbounds.assignClientsGroup') });
|
||||
items.push({ key: 'addToGroup', icon: <TagsOutlined />, label: t('pages.inbounds.addClientsToGroup') });
|
||||
items.push({ key: 'delAllClients', icon: <UsergroupDeleteOutlined />, danger: true, label: t('pages.inbounds.delAllClients') });
|
||||
}
|
||||
items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
|
||||
|
||||
@@ -40,7 +40,7 @@ const InboundInfoModal = lazy(() => import('./InboundInfoModal'));
|
||||
const QrCodeModal = lazy(() => import('./QrCodeModal'));
|
||||
const AttachClientsModal = lazy(() => import('./AttachClientsModal'));
|
||||
const DetachClientsModal = lazy(() => import('./DetachClientsModal'));
|
||||
const AssignClientsGroupModal = lazy(() => import('./AssignClientsGroupModal'));
|
||||
const AddClientsToGroupModal = lazy(() => import('./AddClientsToGroupModal'));
|
||||
|
||||
type RowAction =
|
||||
| 'edit'
|
||||
@@ -54,7 +54,7 @@ type RowAction =
|
||||
| 'delAllClients'
|
||||
| 'attachClients'
|
||||
| 'detachClients'
|
||||
| 'assignGroup'
|
||||
| 'addToGroup'
|
||||
| 'clone';
|
||||
|
||||
type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
|
||||
@@ -452,7 +452,7 @@ export default function InboundsPage() {
|
||||
// Actions that touch per-client secrets (uuid, password, flow, ...) need
|
||||
// the full payload that the slim list view does not ship. Hydrate first
|
||||
// and then operate on the rehydrated record.
|
||||
const hydratingKeys: RowAction[] = ['edit', 'showInfo', 'qrcode', 'export', 'subs', 'clipboard', 'clone', 'attachClients', 'assignGroup'];
|
||||
const hydratingKeys: RowAction[] = ['edit', 'showInfo', 'qrcode', 'export', 'subs', 'clipboard', 'clone', 'attachClients', 'addToGroup'];
|
||||
let target = dbInbound;
|
||||
if (hydratingKeys.includes(key)) {
|
||||
const hydrated = await hydrateInbound(dbInbound.id);
|
||||
@@ -497,7 +497,7 @@ export default function InboundsPage() {
|
||||
setDetachSource(target);
|
||||
setDetachOpen(true);
|
||||
break;
|
||||
case 'assignGroup':
|
||||
case 'addToGroup':
|
||||
setGroupSource(target);
|
||||
setGroupOpen(true);
|
||||
break;
|
||||
@@ -631,10 +631,10 @@ export default function InboundsPage() {
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={groupOpen}>
|
||||
<AssignClientsGroupModal
|
||||
<AddClientsToGroupModal
|
||||
open={groupOpen}
|
||||
onClose={() => setGroupOpen(false)}
|
||||
onAssigned={refresh}
|
||||
onAdded={refresh}
|
||||
source={groupSource}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
Reference in New Issue
Block a user