Просмотр исходного кода

feat(frontend): advanced JSON tab on InboundFormModal.new.tsx (Pattern A)

Adds the advanced JSON tab. Each sub-tab (settings / streamSettings /
sniffing) renders an AdvancedSliceEditor — a small CodeMirror-backed
JsonEditor that holds a local text buffer and forwards parsed JSON to
form state on every valid edit.

Invalid JSON sits silently in the local buffer; once the user finishes
balancing braces / quoting, the next valid parse pushes through to the
form. No stamping ref, no apply-on-tab-switch ceremony — the form is
the single source of truth.

The buffer seeds once from form state on mount. The Modal's
destroyOnHidden means each open is a fresh editor instance, so external
form mutations during a single open session can't desync the editor
either.

The streamSettings sub-tab is omitted when streamEnabled is false
(matching the legacy modal's behavior for protocols like Http / Mixed
that have no stream layer).
MHSanaei 10 часов назад
Родитель
Сommit
d6d0c3bb41
1 измененных файлов с 87 добавлено и 0 удалено
  1. 87 0
      frontend/src/pages/inbounds/InboundFormModal.new.tsx

+ 87 - 0
frontend/src/pages/inbounds/InboundFormModal.new.tsx

@@ -55,6 +55,9 @@ import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
 import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
 import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
 import DateTimePicker from '@/components/DateTimePicker';
 import DateTimePicker from '@/components/DateTimePicker';
 import InputAddon from '@/components/InputAddon';
 import InputAddon from '@/components/InputAddon';
+import JsonEditor from '@/components/JsonEditor';
+import type { FormInstance } from 'antd';
+import type { NamePath } from 'antd/es/form/interface';
 
 
 const { TextArea } = Input;
 const { TextArea } = Input;
 import type { DBInbound } from '@/models/dbinbound';
 import type { DBInbound } from '@/models/dbinbound';
@@ -67,6 +70,44 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery';
 
 
 const { Text } = Typography;
 const { Text } = Typography;
 
 
+// Sub-editor for one slice of the form (settings, streamSettings, sniffing).
+// Holds a local text buffer so the user can type freely; on every keystroke
+// we try to JSON.parse and forward the result to form state. Invalid JSON
+// is held in the buffer until the next valid moment — no panic on partial
+// input. The buffer seeds once on mount; the modal's destroyOnHidden makes
+// each open a fresh editor instance, so we don't need to re-sync on outer
+// form changes.
+function AdvancedSliceEditor({
+  form,
+  path,
+  minHeight,
+  maxHeight,
+}: {
+  form: FormInstance<InboundFormValues>;
+  path: NamePath;
+  minHeight?: string;
+  maxHeight?: string;
+}) {
+  const [text, setText] = useState(() =>
+    JSON.stringify(form.getFieldValue(path) ?? {}, null, 2),
+  );
+  return (
+    <JsonEditor
+      value={text}
+      minHeight={minHeight}
+      maxHeight={maxHeight}
+      onChange={(next) => {
+        setText(next);
+        try {
+          form.setFieldValue(path, JSON.parse(next));
+        } catch {
+
+        }
+      }}
+    />
+  );
+}
+
 const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
 const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
 const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
 const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
 const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
 const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
@@ -1855,6 +1896,51 @@ export default function InboundFormModalNew({
     </>
     </>
   );
   );
 
 
+  const advancedTab = (
+    <Tabs
+      items={[
+        {
+          key: 'settings',
+          label: t('pages.inbounds.advanced.settings'),
+          children: (
+            <AdvancedSliceEditor
+              form={form}
+              path="settings"
+              minHeight="320px"
+              maxHeight="540px"
+            />
+          ),
+        },
+        ...(streamEnabled
+          ? [{
+            key: 'stream',
+            label: t('pages.inbounds.advanced.stream'),
+            children: (
+              <AdvancedSliceEditor
+                form={form}
+                path="streamSettings"
+                minHeight="320px"
+                maxHeight="540px"
+              />
+            ),
+          }]
+          : []),
+        {
+          key: 'sniffing',
+          label: t('pages.inbounds.advanced.sniffing'),
+          children: (
+            <AdvancedSliceEditor
+              form={form}
+              path="sniffing"
+              minHeight="240px"
+              maxHeight="420px"
+            />
+          ),
+        },
+      ]}
+    />
+  );
+
   const sniffingTab = (
   const sniffingTab = (
     <>
     <>
       <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
       <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
@@ -1957,6 +2043,7 @@ export default function InboundFormModalNew({
                 ]
                 ]
               : []),
               : []),
             { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
             { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
+            { key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab },
           ]} />
           ]} />
         </Form>
         </Form>
       </Modal>
       </Modal>