3x-ui/frontend/src/pages/clients/BulkAddToGroupModal.tsx

82 lines
2.2 KiB
TypeScript
Raw Normal View History

feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AutoComplete, Form, Modal, message } from 'antd';
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.
2026-05-28 10:59:20 +00:00
interface BulkAddToGroupModalProps {
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
open: boolean;
count: number;
groups: string[];
onOpenChange: (open: boolean) => void;
onSubmit: (group: string) => Promise<{ affected?: number } | null>;
}
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.
2026-05-28 10:59:20 +00:00
export default function BulkAddToGroupModal({
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
open,
count,
groups,
onOpenChange,
onSubmit,
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.
2026-05-28 10:59:20 +00:00
}: BulkAddToGroupModalProps) {
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (open) setValue('');
}, [open]);
async function submit() {
const next = value.trim();
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.
2026-05-28 10:59:20 +00:00
if (!next) return;
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
setSubmitting(true);
try {
const result = await onSubmit(next);
if (result) {
const affected = result.affected ?? 0;
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.
2026-05-28 10:59:20 +00:00
messageApi.success(t('pages.clients.addToGroupSuccessToast', { count: affected, group: next }));
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
onOpenChange(false);
}
} finally {
setSubmitting(false);
}
}
return (
<>
{messageContextHolder}
<Modal
open={open}
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.
2026-05-28 10:59:20 +00:00
title={t('pages.clients.addToGroupTitle', { count })}
okText={t('add')}
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
cancelText={t('cancel')}
confirmLoading={submitting}
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.
2026-05-28 10:59:20 +00:00
okButtonProps={{ disabled: !value.trim() }}
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
onCancel={() => onOpenChange(false)}
onOk={submit}
destroyOnHidden
>
<Form layout="vertical">
<Form.Item
label={t('pages.clients.group')}
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.
2026-05-28 10:59:20 +00:00
tooltip={t('pages.clients.addToGroupTooltip')}
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
>
<AutoComplete
value={value}
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.
2026-05-28 10:59:20 +00:00
placeholder={t('pages.clients.addToGroupPlaceholder')}
feat(clients,groups): client groups + sub-links export + dedicated groups page Persistent client groups - New ClientGroup model + client_groups table that holds empty (placeholder) groups so a user can define a label before any client references it. ListGroups merges these with the distinct group_name values already stored on clients and reports {name, clientCount}. - ClientRecord gains group_name column; the model.Client wire shape gains a matching `group` JSON field that survives the inbound.settings → SyncInbound round-trip. - Rename/Delete on a group mutates client_groups (rename row / delete row) AND propagates to all matching clients in ClientRecord and in every owning inbound's settings JSON, all in one transaction. Bulk operations - AssignGroup(emails, group) updates clients.group_name + patches each affected inbound's settings JSON in one read-modify-write per inbound. Empty group clears the label. Auto-creates the client_groups row when the user assigns to a brand-new name. - BulkResetTraffic(emails) loops the existing single-reset path so the caller can zero traffic across a whole selection or a whole group. - EmailsByGroup(name) returns just the email list (used by the groups page to fan a single bulk action over every member). Endpoints (all under /panel/api/clients) - GET /groups — summaries with counts - GET /groups/:name/emails — emails in a group - POST /groups/create — empty placeholder group - POST /groups/rename — rename (table + clients + JSON) - POST /groups/delete — drop label everywhere (clients survive) - POST /bulkAssignGroup — assign N selected clients - POST /bulkResetTraffic — reset traffic on a list Clients page UX - New Group column (Actions → Client → Group → Inbounds → …) with a click-to-filter chip. - FilterDrawer gains a multi-select Group filter whose options come from the new ClientPageResponse.groups field (sourced from ListGroups so empty/placeholder groups are pickable too). - Single-client and bulk-add forms gain a Group AutoComplete pre-loaded with all known group names. - New toolbar buttons when selection > 0: "Group ({n})" opens BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal. Sub-links export modal (new SubLinksModal.tsx) - Table of selected clients with their subscription URL (and JSON URL when subJsonEnable is on), per-row copy, Copy all, and Download as sub-links-<timestamp>.txt. Warns when subscription is disabled or none of the selected clients have a subId. Dedicated Groups page (new pages/groups/GroupsPage.tsx) - /groups route + sidebar entry (TagsOutlined icon) + page title key. - Card-based layout matching Clients/Inbounds/Nodes — summary card with Total/Grouped/Empty stats, main card with Add Group button + table. - Per-row More dropdown (icon-first column on the left): Sub links, Adjust (days+traffic), Reset traffic, Rename, Delete clients in group, Delete group (keep clients). Empty groups disable the client-targeted actions. - Reuses SubLinksModal and ClientBulkAdjustModal — emails for the group are fetched on demand from GET /groups/:name/emails. Other polish - /groups + groups-page selectors added to page-shell.css and page-cards.css so the new page inherits the same background, padding, card borders, hover shadow, and summary-card padding. - .card-toolbar gains a small vertical padding so the larger toolbar buttons (now default size, matching Inbounds) don't crowd the top of the card-head on Clients and Groups pages.
2026-05-27 15:30:55 +00:00
options={groups.map((g) => ({ value: g }))}
onChange={(v) => setValue(v ?? '')}
filterOption={(input, option) =>
String(option?.value ?? '').toLowerCase().includes((input || '').toLowerCase())
}
allowClear
style={{ width: '100%' }}
autoFocus
/>
</Form.Item>
</Form>
</Modal>
</>
);
}