mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(frontend): xhttp form binding + drop empty strings from JSON (B23)
uplinkHTTPMethod was wrapped Form.Item -> Form.Item(shouldUpdate) -> Select, which broke AntD's value/onChange injection (AntD only clones the immediate child). Restructured so shouldUpdate is the outer wrapper and Form.Item(name) directly wraps the Select. Also drop empty-string fields from xhttpSettings in the wire payload — fields like uplinkHTTPMethod, sessionPlacement, seqPlacement, xPaddingKey default to '' meaning "use server default", so they shouldn't appear in JSON as "field": "". Adds placeholder text to the 3 xhttp Selects so the form reflects the current value after JSON paste.
This commit is contained in:
parent
6e90b24af1
commit
3fdd9765a7
2 changed files with 30 additions and 32 deletions
|
|
@ -20,13 +20,6 @@ import type {
|
||||||
WireguardOutboundFormSettings,
|
WireguardOutboundFormSettings,
|
||||||
} from '@/schemas/forms/outbound-form';
|
} from '@/schemas/forms/outbound-form';
|
||||||
|
|
||||||
// Adapter between the wire-shape outbound JSON the panel stores in
|
|
||||||
// templateSettings.outbounds[] and the typed OutboundFormValues the modal
|
|
||||||
// holds in Form.useForm<T>. No dependency on the legacy Outbound class
|
|
||||||
// hierarchy — the modal hands a wire-shape object in, takes typed values
|
|
||||||
// out, and on submit calls formValuesToWirePayload() to get a plain JS
|
|
||||||
// object ready to pass to onConfirm().
|
|
||||||
|
|
||||||
type Raw = Record<string, unknown>;
|
type Raw = Record<string, unknown>;
|
||||||
|
|
||||||
function asObject(value: unknown): Raw {
|
function asObject(value: unknown): Raw {
|
||||||
|
|
@ -348,20 +341,12 @@ export interface RawOutboundRow {
|
||||||
mux?: unknown;
|
mux?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert wire-shape outbound (the object stored in
|
|
||||||
// templateSettings.outbounds[]) into typed form values. Stream + mux are
|
|
||||||
// minimal placeholders for now — the modal will fold the real stream sub-
|
|
||||||
// form in when those sections come online.
|
|
||||||
export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues {
|
export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues {
|
||||||
const protocol = asString(raw.protocol, 'vless');
|
const protocol = asString(raw.protocol, 'vless');
|
||||||
const settings = asObject(raw.settings);
|
const settings = asObject(raw.settings);
|
||||||
const tag = asString(raw.tag);
|
const tag = asString(raw.tag);
|
||||||
const sendThrough = asString(raw.sendThrough);
|
const sendThrough = asString(raw.sendThrough);
|
||||||
const mux = muxFromWire(raw.mux);
|
const mux = muxFromWire(raw.mux);
|
||||||
// Leave streamSettings undefined when missing or empty — the modal's
|
|
||||||
// stream tab seeds it when the user opens the relevant section. This
|
|
||||||
// keeps Form.useForm from receiving a value that doesn't match the
|
|
||||||
// NetworkSettings DU.
|
|
||||||
const hasStream = raw.streamSettings
|
const hasStream = raw.streamSettings
|
||||||
&& typeof raw.streamSettings === 'object'
|
&& typeof raw.streamSettings === 'object'
|
||||||
&& Object.keys(raw.streamSettings as Raw).length > 0;
|
&& Object.keys(raw.streamSettings as Raw).length > 0;
|
||||||
|
|
@ -554,18 +539,22 @@ function loopbackToWire(s: LoopbackOutboundFormSettings) {
|
||||||
const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
|
const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
|
||||||
const STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);
|
const STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);
|
||||||
|
|
||||||
// Strip UI-only fields the form layered into streamSettings (e.g. the
|
function dropEmptyStrings(obj: Raw): Raw {
|
||||||
// XHTTP modal's enableXmux toggle that controls section visibility but
|
const out: Raw = {};
|
||||||
// has no meaning on the wire). xray-core would ignore unknown fields
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
// anyway but the panel reads back its own emitted JSON, so we keep
|
if (v === '') continue;
|
||||||
// the wire shape clean.
|
out[k] = v;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function stripUiOnlyStreamFields(stream: unknown): Raw {
|
function stripUiOnlyStreamFields(stream: unknown): Raw {
|
||||||
const next = { ...(stream as Raw) };
|
const next = { ...(stream as Raw) };
|
||||||
const xh = next.xhttpSettings;
|
const xh = next.xhttpSettings;
|
||||||
if (xh && typeof xh === 'object') {
|
if (xh && typeof xh === 'object') {
|
||||||
const cleaned = { ...(xh as Raw) };
|
const cleaned = { ...(xh as Raw) };
|
||||||
delete cleaned.enableXmux;
|
delete cleaned.enableXmux;
|
||||||
next.xhttpSettings = cleaned;
|
next.xhttpSettings = dropEmptyStrings(cleaned);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1473,16 +1473,23 @@ export default function OutboundFormModal({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Uplink HTTP method"
|
noStyle
|
||||||
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
shouldUpdate={(prev, curr) =>
|
||||||
|
prev?.streamSettings?.xhttpSettings?.mode !==
|
||||||
|
curr?.streamSettings?.xhttpSettings?.mode
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => {
|
{() => {
|
||||||
const mode = form.getFieldValue([
|
const mode = form.getFieldValue([
|
||||||
'streamSettings', 'xhttpSettings', 'mode',
|
'streamSettings', 'xhttpSettings', 'mode',
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label="Uplink HTTP method"
|
||||||
|
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
|
placeholder="Default (POST)"
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Default (POST)' },
|
{ value: '', label: 'Default (POST)' },
|
||||||
{ value: 'POST', label: 'POST' },
|
{ value: 'POST', label: 'POST' },
|
||||||
|
|
@ -1490,10 +1497,10 @@ export default function OutboundFormModal({
|
||||||
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
|
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{/* Session + sequence + uplinkData placements:
|
{/* Session + sequence + uplinkData placements:
|
||||||
three orthogonal slots Xray uses to thread
|
three orthogonal slots Xray uses to thread
|
||||||
|
|
@ -1505,6 +1512,7 @@ export default function OutboundFormModal({
|
||||||
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
placeholder="Default (path)"
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Default (path)' },
|
{ value: '', label: 'Default (path)' },
|
||||||
{ value: 'path', label: 'path' },
|
{ value: 'path', label: 'path' },
|
||||||
|
|
@ -1535,6 +1543,7 @@ export default function OutboundFormModal({
|
||||||
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
placeholder="Default (path)"
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Default (path)' },
|
{ value: '', label: 'Default (path)' },
|
||||||
{ value: 'path', label: 'path' },
|
{ value: 'path', label: 'path' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue