fix(forms): validate JSON tabs before applying or saving

InboundFormModal: switching out of the Advanced tab now parses the three
JSON textareas and rebuilds the structured Inbound via Inbound.fromJson,
so the Basic tab reflects what was pasted. Invalid JSON keeps the user
on Advanced with a specific parse error.

XrayPage: Save now parses xraySetting upfront and snaps the user back to
the Advanced tab on invalid JSON instead of letting the backend reject a
generic blob.
This commit is contained in:
MHSanaei
2026-05-13 19:01:12 +02:00
parent b10a9f1de7
commit 5543466fcc
3 changed files with 101 additions and 47 deletions
@@ -70,6 +70,7 @@ const inbound = ref(null);
const dbForm = ref(null);
const saving = ref(false);
const advancedJson = ref({ stream: '', sniffing: '', settings: '' });
const activeTabKey = ref('basic');
// Cached default cert/key paths from /panel/setting/defaultSettings —
// powers the "Set default cert" button on the TLS form.
const defaultCert = ref('');
@@ -241,9 +242,60 @@ watch(() => props.open, (next) => {
dbForm.value = freshDbForm();
primeAdvancedJson();
}
activeTabKey.value = 'basic';
fetchDefaultCertSettings();
});
function applyAdvancedJsonToBasic() {
if (!inbound.value) return true;
let parsedSettings;
let parsedStream;
let parsedSniffing;
try {
parsedSettings = advancedJson.value.settings.trim()
? JSON.parse(advancedJson.value.settings)
: inbound.value.settings?.toJson?.();
} catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; }
try {
parsedStream = advancedJson.value.stream.trim()
? JSON.parse(advancedJson.value.stream)
: inbound.value.stream?.toJson?.();
} catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
try {
parsedSniffing = advancedJson.value.sniffing.trim()
? JSON.parse(advancedJson.value.sniffing)
: inbound.value.sniffing?.toJson?.();
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
try {
inbound.value = Inbound.fromJson({
port: inbound.value.port,
listen: inbound.value.listen,
protocol: inbound.value.protocol,
settings: parsedSettings,
streamSettings: parsedStream,
tag: inbound.value.tag,
sniffing: parsedSniffing,
clientStats: inbound.value.clientStats,
});
} catch (e) {
message.error(`Advanced JSON: ${e.message}`);
return false;
}
return true;
}
let isRevertingTab = false;
watch(activeTabKey, (next, prev) => {
if (isRevertingTab) { isRevertingTab = false; return; }
if (prev === 'advanced' && next !== 'advanced') {
if (!applyAdvancedJsonToBasic()) {
isRevertingTab = true;
activeTabKey.value = 'advanced';
}
}
});
// In add mode, switching protocol restamps settings + re-syncs port.
function onProtocolChange(next) {
if (props.mode === 'edit' || !inbound.value) return;
@@ -572,7 +624,7 @@ watch(
<template>
<a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :confirm-loading="saving"
:mask-closable="false" width="780px" @ok="submit" @cancel="close">
<a-tabs v-if="inbound && dbForm" default-active-key="basic">
<a-tabs v-if="inbound && dbForm" v-model:active-key="activeTabKey">
<!-- ============================== BASICS ============================== -->
<a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
+12 -4
View File
@@ -186,9 +186,6 @@ function onRemoveRoutingRules({ prefix }) {
);
}
// `message` is used by some of the in-progress UX flows (kept around
// because future provisioning errors will surface through it).
void message;
const { isMobile } = useMediaQuery();
const basePath = window.X_UI_BASE_PATH || '';
@@ -230,6 +227,17 @@ function onTabChange(key) {
}
}
function onSaveAll() {
try {
JSON.parse(xraySetting.value);
} catch (e) {
message.error(`Advanced JSON: ${e.message}`);
activeTabKey.value = 'tpl-advanced';
return;
}
saveAll();
}
function syncTabFromHash() {
const key = keyBySlug[window.location.hash.slice(1)];
if (key) activeTabKey.value = key;
@@ -268,7 +276,7 @@ onBeforeUnmount(() => {
<a-row class="header-row">
<a-col :xs="24" :sm="14" class="header-actions">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
<a-button type="primary" :disabled="saveDisabled" @click="onSaveAll">
{{ t('pages.xray.save') }}
</a-button>
<a-button type="primary" danger :disabled="!saveDisabled" @click="confirmRestart">