diff --git a/frontend/src/api/queries/useClientOptions.ts b/frontend/src/api/queries/useClientOptions.ts new file mode 100644 index 000000000..26968a724 --- /dev/null +++ b/frontend/src/api/queries/useClientOptions.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { z } from 'zod'; + +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; +import { keys } from '@/api/queryKeys'; +import { ClientRecordSchema, type ClientRecord } from '@/schemas/client'; + +const ClientRecordListSchema = z + .array(ClientRecordSchema) + .nullable() + .transform((value) => value ?? []); + +async function fetchClients(): Promise { + 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 ?? []; +} + +export function useClientOptions(enabled = true) { + return useQuery({ + queryKey: keys.clients.all(), + queryFn: fetchClients, + enabled, + staleTime: 30_000, + select: (clients) => + clients + .map((client) => client.email.trim()) + .filter(Boolean) + .sort((a, b) => a.localeCompare(b)), + }); +} diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx index 48e602e5d..08c1189f3 100644 --- a/frontend/src/pages/xray/routing/RuleFormModal.tsx +++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx @@ -6,6 +6,7 @@ import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { InputAddon } from '@/components/ui'; import { GeoTokenInput } from '@/components/geodata'; import { FormField } from '@/components/form/rhf'; +import { useClientOptions } from '@/api/queries/useClientOptions'; import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers'; @@ -82,6 +83,21 @@ export default function RuleFormModal({ const { data: inboundOptions } = useInboundOptions(); const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]); + const { + data: clientEmails = [], + isFetching: clientsLoading, + isError: clientsError, + } = useClientOptions(open); + const user = useWatch({ control: methods.control, name: 'user' }) ?? ''; + const selectedUsers = useMemo(() => csv(user), [user]); + const userOptions = useMemo( + () => + [...new Set([...clientEmails, ...selectedUsers])].map((email) => ({ + value: email, + label: email, + })), + [clientEmails, selectedUsers], + ); useEffect(() => { if (!open) return; @@ -287,8 +303,27 @@ export default function RuleFormModal({ {t('pages.xray.ruleForm.user')}