Browse Source

fix(outbound): accept JSON-only configs and sync JSON to basic form on tab switch

Pasting a JSON config and clicking OK failed with "Something went wrong"
because validation read the empty form-side tag input instead of the
JSON's tag. Switching from the JSON tab to Basic also discarded any
JSON the user had pasted.

- onOk now validates and submits from the JSON tab using the parsed JSON
- Tab switch JSON→Basic deserializes the JSON back into the structured form
- Invalid JSON keeps the user on the JSON tab with a clear parse error
- Empty form-tag / duplicate-tag errors are now specific, not generic
MHSanaei 1 day ago
parent
commit
6c6b40e063
1 changed files with 41 additions and 17 deletions
  1. 41 17
      frontend/src/pages/xray/OutboundFormModal.vue

+ 41 - 17
frontend/src/pages/xray/OutboundFormModal.vue

@@ -80,8 +80,17 @@ watch(() => props.open, (next) => {
   primeAdvancedJson();
   primeAdvancedJson();
 });
 });
 
 
-watch(activeKey, (key) => {
-  if (key === '2') primeAdvancedJson();
+let isRevertingTab = false;
+watch(activeKey, (key, prev) => {
+  if (isRevertingTab) { isRevertingTab = false; return; }
+  if (key === '2') {
+    primeAdvancedJson();
+  } else if (key === '1' && prev === '2') {
+    if (!applyAdvancedJsonToForm()) {
+      isRevertingTab = true;
+      activeKey.value = '2';
+    }
+  }
 });
 });
 
 
 function primeAdvancedJson() {
 function primeAdvancedJson() {
@@ -93,6 +102,33 @@ function primeAdvancedJson() {
   }
   }
 }
 }
 
 
+function applyAdvancedJsonToForm() {
+  const raw = advancedJson.value.trim();
+  if (!raw) return true;
+  let currentJson = '';
+  try {
+    currentJson = JSON.stringify(outbound.value?.toJson() ?? {}, null, 2);
+  } catch (_e) { /* fall through */ }
+  if (raw === currentJson.trim()) return true;
+  let parsed;
+  try {
+    parsed = JSON.parse(raw);
+  } catch (e) {
+    message.error(`JSON: ${e.message}`);
+    return false;
+  }
+  try {
+    const fallbackTag = outbound.value?.tag;
+    const next = Outbound.fromJson(parsed);
+    if (!next.tag && fallbackTag) next.tag = fallbackTag;
+    outbound.value = next;
+    return true;
+  } catch (e) {
+    message.error(`JSON: ${e.message}`);
+    return false;
+  }
+}
+
 function close() { emit('update:open', false); }
 function close() { emit('update:open', false); }
 
 
 function onProtocolChange(next) {
 function onProtocolChange(next) {
@@ -131,27 +167,15 @@ const tagHelp = computed(() => {
 // ============== Submit ==============
 // ============== Submit ==============
 function onOk() {
 function onOk() {
   if (!outbound.value) return;
   if (!outbound.value) return;
+  if (activeKey.value === '2' && !applyAdvancedJsonToForm()) return;
   if (!outbound.value.tag?.trim()) {
   if (!outbound.value.tag?.trim()) {
-    message.error(t('somethingWentWrong'));
+    message.error('Tag is required');
     return;
     return;
   }
   }
   if (duplicateTag.value) {
   if (duplicateTag.value) {
-    message.error(t('somethingWentWrong'));
+    message.error('Tag already used by another outbound');
     return;
     return;
   }
   }
-  // If user spent time in the JSON tab, prefer that body — round-trip
-  // it through Outbound.fromJson so the wire shape stays consistent.
-  if (activeKey.value === '2' && advancedJson.value.trim()) {
-    try {
-      const parsed = JSON.parse(advancedJson.value);
-      const built = Outbound.fromJson(parsed);
-      emit('confirm', built.toJson());
-      return;
-    } catch (e) {
-      message.error(`JSON: ${e.message}`);
-      return;
-    }
-  }
   emit('confirm', outbound.value.toJson());
   emit('confirm', outbound.value.toJson());
 }
 }