mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 14:16:07 +00:00
fix(ui): match node connection-outbound picker to panel-outbound selector
Group the tags into Outbounds/Balancers, hide blackhole outbounds, and show the 'Direct connection' placeholder on empty via getValueProps so the field never looks unset and an empty default can't read as a second 'direct'.
This commit is contained in:
@@ -34,3 +34,38 @@ export function useOutboundTags(opts?: { excludeBlackhole?: boolean }) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface OutboundTagGroups {
|
||||
outbounds: string[];
|
||||
balancers: string[];
|
||||
}
|
||||
|
||||
// Same data as useOutboundTags, but keeps outbound and balancer tags apart so a
|
||||
// picker can render them in labeled groups (like the panel-outbound selector)
|
||||
// instead of one flat list.
|
||||
export function useOutboundTagGroups(opts?: { excludeBlackhole?: boolean }) {
|
||||
const excludeBlackhole = opts?.excludeBlackhole ?? false;
|
||||
return useQuery({
|
||||
queryKey: keys.xray.config(),
|
||||
queryFn: fetchXrayConfig,
|
||||
staleTime: Infinity,
|
||||
select: (data): OutboundTagGroups => {
|
||||
const outbounds = new Set<string>();
|
||||
for (const o of data?.xraySetting?.outbounds ?? []) {
|
||||
const ob = o as { tag?: string; protocol?: string } | null;
|
||||
if (!ob?.tag) continue;
|
||||
if (excludeBlackhole && ob.protocol === 'blackhole') continue;
|
||||
outbounds.add(ob.tag);
|
||||
}
|
||||
for (const t of data?.subscriptionOutboundTags ?? []) {
|
||||
if (t) outbounds.add(t);
|
||||
}
|
||||
const balancers: string[] = [];
|
||||
const bal = (data?.xraySetting?.routing as { balancers?: Array<{ tag?: string }> } | undefined)?.balancers;
|
||||
for (const b of bal ?? []) {
|
||||
if (b?.tag && !outbounds.has(b.tag)) balancers.push(b.tag);
|
||||
}
|
||||
return { outbounds: [...outbounds], balancers };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { RemoteInboundOption } from '@/api/queries/useNodeMutations';
|
||||
import type { Msg } from '@/utils';
|
||||
import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node';
|
||||
import { antdRule } from '@/utils/zodForm';
|
||||
import { useOutboundTags } from '@/api/queries/useOutboundTags';
|
||||
import { useOutboundTagGroups } from '@/api/queries/useOutboundTags';
|
||||
import './NodeFormModal.css';
|
||||
|
||||
type Mode = 'add' | 'edit';
|
||||
@@ -77,7 +77,23 @@ export default function NodeFormModal({
|
||||
const scheme = Form.useWatch('scheme', form) ?? 'https';
|
||||
const tlsVerifyMode = Form.useWatch('tlsVerifyMode', form) ?? 'verify';
|
||||
const inboundSyncMode = Form.useWatch('inboundSyncMode', form) ?? 'all';
|
||||
const { data: outboundTags } = useOutboundTags({ excludeBlackhole: true });
|
||||
const { data: outboundGroups } = useOutboundTagGroups({ excludeBlackhole: true });
|
||||
|
||||
// Outbounds and balancers share one picker (like the panel-outbound selector);
|
||||
// when balancers exist they get a labeled group so it's clear the selection
|
||||
// routes through a balancer. Empty falls back to the placeholder ("Direct
|
||||
// connection") rather than a synthetic option, so it can't read as a second
|
||||
// "direct" next to a real freedom outbound.
|
||||
const outboundOptions = useMemo<
|
||||
({ label: string; value: string } | { label: string; options: { label: string; value: string }[] })[]
|
||||
>(() => {
|
||||
const outOpts = (outboundGroups?.outbounds ?? []).map((tag) => ({ label: tag, value: tag }));
|
||||
if (!outboundGroups?.balancers.length) return outOpts;
|
||||
return [
|
||||
{ label: t('pages.xray.Outbounds'), options: outOpts },
|
||||
{ label: t('pages.xray.Balancers'), options: outboundGroups.balancers.map((tag) => ({ label: tag, value: tag })) },
|
||||
];
|
||||
}, [outboundGroups, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -364,12 +380,13 @@ export default function NodeFormModal({
|
||||
label={t('pages.nodes.outboundTag')}
|
||||
name="outboundTag"
|
||||
extra={t('pages.nodes.outboundTagHint')}
|
||||
getValueProps={(v) => ({ value: (v as string) || undefined })}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
placeholder={t('pages.nodes.outboundTagPlaceholder')}
|
||||
options={(outboundTags ?? []).map((tag) => ({ value: tag, label: tag }))}
|
||||
options={outboundOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user