feat(routing): add client picker to user rules (#6271)

* feat(routing): add client picker to user rules

Replace the free-text user criterion with a searchable multi-select backed by existing panel clients. Preserve saved values that no longer exist so editing legacy rules remains lossless.

* feat(routing): polish user picker states

Align the routing user selector with the inbound-tag multi-select, including search, clear, loading, empty, and error states. Localize the new copy across every supported locale and cover legacy saved users with a regression test.

* fix(routing): keep custom user identifiers

Use tags mode with comma tokenization so the user picker suggests panel clients without rejecting HTTP, Mixed, or raw-template identifiers. Restore the comma hint and cover custom entries with a regression test.
This commit is contained in:
Zane
2026-08-23 05:13:34 +08:00
committed by GitHub
parent 326009e9d3
commit 02002dc1c3
16 changed files with 206 additions and 4 deletions
@@ -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')} <QuestionCircleOutlined aria-hidden="true" />
</Tooltip>
}
transform={{
input: (value) => csv(typeof value === 'string' ? value : ''),
output: (value) => (Array.isArray(value) ? value.join(',') : ''),
}}
>
<Input placeholder="email address" />
<Select
mode="tags"
tokenSeparators={[',']}
allowClear
loading={clientsLoading}
placeholder={t('pages.xray.ruleForm.userPlaceholder')}
showSearch={{ optionFilterProp: 'label' }}
notFoundContent={
clientsLoading
? t('loading')
: clientsError
? t('pages.xray.ruleForm.userLoadError')
: t('pages.xray.ruleForm.userEmpty')
}
options={userOptions}
/>
</FormField>
<FormField