diff --git a/frontend/src/api/queryKeys.ts b/frontend/src/api/queryKeys.ts index ab74ec40..4f7f5037 100644 --- a/frontend/src/api/queryKeys.ts +++ b/frontend/src/api/queryKeys.ts @@ -19,6 +19,7 @@ export const keys = { clients: { root: () => ['clients'] as const, list: (params: unknown) => ['clients', 'list', params] as const, + all: () => ['clients', 'all'] as const, onlines: () => ['clients', 'onlines'] as const, lastOnline: () => ['clients', 'lastOnline'] as const, groups: () => ['clients', 'groups'] as const, diff --git a/frontend/src/pages/groups/GroupsPage.tsx b/frontend/src/pages/groups/GroupsPage.tsx index 6a0f1750..12690c0f 100644 --- a/frontend/src/pages/groups/GroupsPage.tsx +++ b/frontend/src/pages/groups/GroupsPage.tsx @@ -34,6 +34,7 @@ import { UsergroupDeleteOutlined, } from '@ant-design/icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { z } from 'zod'; import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; @@ -44,9 +45,16 @@ import { setMessageInstance } from '@/utils/messageBus'; import AppSidebar from '@/components/AppSidebar'; import LazyMount from '@/components/LazyMount'; import { keys } from '@/api/queryKeys'; -import { GroupSummaryListSchema, type GroupSummary } from '@/schemas/client'; +import { + ClientRecordSchema, + GroupSummaryListSchema, + type ClientRecord, + type GroupSummary, +} from '@/schemas/client'; import { parseMsg } from '@/utils/zodValidate'; +const ClientRecordListSchema = z.array(ClientRecordSchema).nullable().transform((v) => v ?? []); + const SubLinksModal = lazy(() => import('../clients/SubLinksModal')); const ClientBulkAdjustModal = lazy(() => import('../clients/ClientBulkAdjustModal')); const GroupAddClientsModal = lazy(() => import('./GroupAddClientsModal')); @@ -81,7 +89,7 @@ export default function GroupsPage() { useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); const queryClient = useQueryClient(); - const { clients, subSettings, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, bulkDelete } = useClients(); + const { subSettings, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, bulkDelete } = useClients(); const groupsQuery = useQuery({ queryKey: keys.clients.groups(), @@ -133,6 +141,19 @@ export default function GroupsPage() { const [groupEmails, setGroupEmails] = useState([]); const [groupForAction, setGroupForAction] = useState(null); + const allClientsQuery = useQuery({ + queryKey: keys.clients.all(), + queryFn: async () => { + const msg = await HttpUtil.get('/panel/api/clients/list', undefined, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to load clients'); + const validated = parseMsg(msg, ClientRecordListSchema, 'clients/list'); + return validated.obj ?? []; + }, + enabled: addClientsOpen || removeClientsOpen || subLinksOpen, + staleTime: 30_000, + }); + const allClients = allClientsQuery.data ?? []; + const totalGroups = groups.length; const totalClients = useMemo( () => groups.reduce((acc, g) => acc + (g.clientCount || 0), 0), @@ -529,7 +550,7 @@ export default function GroupsPage() { @@ -561,7 +582,7 @@ export default function GroupsPage() { c.group !== groupForAction?.name)} + candidates={allClients.filter((c) => c.group !== groupForAction?.name)} onClose={() => setAddClientsOpen(false)} onSubmit={async (emails) => { const msg = await bulkAddToGroup(emails, groupForAction?.name ?? ''); @@ -577,7 +598,7 @@ export default function GroupsPage() { c.group === groupForAction?.name)} + members={allClients.filter((c) => c.group === groupForAction?.name)} onClose={() => setRemoveClientsOpen(false)} onSubmit={async (emails) => { const msg = await bulkRemoveFromGroup(emails);