mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 16:39:35 +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:
@@ -20,13 +20,6 @@ import type {
|
||||
WireguardOutboundFormSettings,
|
||||
} 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>;
|
||||
|
||||
function asObject(value: unknown): Raw {
|
||||
@@ -348,20 +341,12 @@ export interface RawOutboundRow {
|
||||
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 {
|
||||
const protocol = asString(raw.protocol, 'vless');
|
||||
const settings = asObject(raw.settings);
|
||||
const tag = asString(raw.tag);
|
||||
const sendThrough = asString(raw.sendThrough);
|
||||
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
|
||||
&& typeof raw.streamSettings === 'object'
|
||||
&& 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 STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);
|
||||
|
||||
// Strip UI-only fields the form layered into streamSettings (e.g. the
|
||||
// XHTTP modal's enableXmux toggle that controls section visibility but
|
||||
// has no meaning on the wire). xray-core would ignore unknown fields
|
||||
// anyway but the panel reads back its own emitted JSON, so we keep
|
||||
// the wire shape clean.
|
||||
function dropEmptyStrings(obj: Raw): Raw {
|
||||
const out: Raw = {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v === '') continue;
|
||||
out[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function stripUiOnlyStreamFields(stream: unknown): Raw {
|
||||
const next = { ...(stream as Raw) };
|
||||
const xh = next.xhttpSettings;
|
||||
if (xh && typeof xh === 'object') {
|
||||
const cleaned = { ...(xh as Raw) };
|
||||
delete cleaned.enableXmux;
|
||||
next.xhttpSettings = cleaned;
|
||||
next.xhttpSettings = dropEmptyStrings(cleaned);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -1473,16 +1473,23 @@ export default function OutboundFormModal({
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Uplink HTTP method"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
||||
noStyle
|
||||
shouldUpdate={(prev, curr) =>
|
||||
prev?.streamSettings?.xhttpSettings?.mode !==
|
||||
curr?.streamSettings?.xhttpSettings?.mode
|
||||
}
|
||||
>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const mode = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'mode',
|
||||
]);
|
||||
return (
|
||||
{() => {
|
||||
const mode = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'mode',
|
||||
]);
|
||||
return (
|
||||
<Form.Item
|
||||
label="Uplink HTTP method"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
||||
>
|
||||
<Select
|
||||
placeholder="Default (POST)"
|
||||
options={[
|
||||
{ value: '', label: 'Default (POST)' },
|
||||
{ value: 'POST', label: 'POST' },
|
||||
@@ -1490,9 +1497,9 @@ export default function OutboundFormModal({
|
||||
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Session + sequence + uplinkData placements:
|
||||
@@ -1505,6 +1512,7 @@ export default function OutboundFormModal({
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
||||
>
|
||||
<Select
|
||||
placeholder="Default (path)"
|
||||
options={[
|
||||
{ value: '', label: 'Default (path)' },
|
||||
{ value: 'path', label: 'path' },
|
||||
@@ -1535,6 +1543,7 @@ export default function OutboundFormModal({
|
||||
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
||||
>
|
||||
<Select
|
||||
placeholder="Default (path)"
|
||||
options={[
|
||||
{ value: '', label: 'Default (path)' },
|
||||
{ value: 'path', label: 'path' },
|
||||
|
||||
Reference in New Issue
Block a user