mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 23:01:00 +00:00
fix(outbounds): persist optional blocks and fix stale edit reopen
- derive XMUX toggle from saved xmux on load, seed defaults on enable, and drop xmux when disabled (#4654) - save the JSON tab straight from parsed text so sockopt, finalmask (TCP masks), mux, and reverse excludes round-trip instead of being dropped by the form-store bounce - remove the redundant Host/Path fields from HTTP obfuscation that fought the request.headers editor over the same form path - rebuild the outbounds table columns on row content change (rows, not rows.length) so a re-opened edited outbound shows fresh values - add adapter round-trip regression tests Closes #4654
This commit is contained in:
@@ -21,6 +21,7 @@ import InputAddon from '@/components/InputAddon';
|
||||
import JsonEditor from '@/components/JsonEditor';
|
||||
import { Wireguard } from '@/utils';
|
||||
import {
|
||||
XMUX_DEFAULTS,
|
||||
formValuesToWirePayload,
|
||||
rawOutboundToFormValues,
|
||||
} from '@/lib/xray/outbound-form-adapter';
|
||||
@@ -335,6 +336,14 @@ export default function OutboundFormModal({
|
||||
form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
|
||||
}
|
||||
|
||||
function onXmuxToggle(checked: boolean) {
|
||||
if (!checked) return;
|
||||
const existing = form.getFieldValue(['streamSettings', 'xhttpSettings', 'xmux']);
|
||||
const hasValues = existing && typeof existing === 'object' && Object.keys(existing).length > 0;
|
||||
if (hasValues) return;
|
||||
form.setFieldValue(['streamSettings', 'xhttpSettings', 'xmux'], { ...XMUX_DEFAULTS });
|
||||
}
|
||||
|
||||
const duplicateTag = useMemo(() => {
|
||||
const myTag = tag.trim();
|
||||
if (!myTag) return false;
|
||||
@@ -392,17 +401,40 @@ export default function OutboundFormModal({
|
||||
}
|
||||
|
||||
async function onOk() {
|
||||
if (activeKey === '2' && !applyJsonToForm()) return;
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch {
|
||||
let values: OutboundFormValues;
|
||||
if (activeKey === '2') {
|
||||
const raw = jsonText.trim();
|
||||
if (!raw) return;
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch (e) {
|
||||
messageApi.error(`JSON: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
values = rawOutboundToFormValues(parsed);
|
||||
form.resetFields();
|
||||
form.setFieldsValue(values);
|
||||
setJsonDirty(false);
|
||||
} else {
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
values = form.getFieldsValue(true) as OutboundFormValues;
|
||||
}
|
||||
const tagValue = (values.tag ?? '').trim();
|
||||
if (!tagValue) {
|
||||
messageApi.error(t('pages.xray.outboundForm.tagRequired'));
|
||||
return;
|
||||
}
|
||||
if (duplicateTag) {
|
||||
const isDuplicateTag = (existingTags || []).includes(tagValue)
|
||||
&& !(isEdit && (outboundProp?.tag as string | undefined) === tagValue);
|
||||
if (isDuplicateTag) {
|
||||
messageApi.error('Tag already used by another outbound');
|
||||
return;
|
||||
}
|
||||
const values = form.getFieldsValue(true) as OutboundFormValues;
|
||||
onConfirm(formValuesToWirePayload(values));
|
||||
}
|
||||
|
||||
@@ -1188,47 +1220,6 @@ export default function OutboundFormModal({
|
||||
>
|
||||
<Input placeholder="1.1" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('host')}
|
||||
name={[
|
||||
'streamSettings',
|
||||
'tcpSettings',
|
||||
'header',
|
||||
'request',
|
||||
'headers',
|
||||
'Host',
|
||||
]}
|
||||
normalize={(v: unknown) =>
|
||||
typeof v === 'string'
|
||||
? v.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: Array.isArray(v) ? v : []
|
||||
}
|
||||
getValueProps={(v: unknown) => ({
|
||||
value: Array.isArray(v) ? v.join(',') : '',
|
||||
})}
|
||||
>
|
||||
<Input placeholder="example.com,cdn.example.com" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('path')}
|
||||
name={[
|
||||
'streamSettings',
|
||||
'tcpSettings',
|
||||
'header',
|
||||
'request',
|
||||
'path',
|
||||
]}
|
||||
normalize={(v: unknown) =>
|
||||
typeof v === 'string'
|
||||
? v.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: Array.isArray(v) ? v : ['/']
|
||||
}
|
||||
getValueProps={(v: unknown) => ({
|
||||
value: Array.isArray(v) ? v.join(',') : '/',
|
||||
})}
|
||||
>
|
||||
<Input placeholder="/,/api,/static" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('pages.inbounds.form.requestHeaders')}
|
||||
name={[
|
||||
@@ -1676,7 +1667,7 @@ export default function OutboundFormModal({
|
||||
name={['streamSettings', 'xhttpSettings', 'enableXmux']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
<Switch onChange={onXmuxToggle} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
|
||||
@@ -375,7 +375,7 @@ export default function OutboundsTab({
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[t, testMode, rows.length, outboundTestStates, outboundsTraffic],
|
||||
[t, testMode, rows, outboundTestStates, outboundsTraffic],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user