From d6d0c3bb416e47b71807f427a4f74d30c46daf0d Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 11:33:59 +0200 Subject: [PATCH] feat(frontend): advanced JSON tab on InboundFormModal.new.tsx (Pattern A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../pages/inbounds/InboundFormModal.new.tsx | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/frontend/src/pages/inbounds/InboundFormModal.new.tsx b/frontend/src/pages/inbounds/InboundFormModal.new.tsx index fee7c51e..2e9c32e7 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.new.tsx +++ b/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 DateTimePicker from '@/components/DateTimePicker'; 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; import type { DBInbound } from '@/models/dbinbound'; @@ -67,6 +70,44 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery'; 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; + path: NamePath; + minHeight?: string; + maxHeight?: string; +}) { + const [text, setText] = useState(() => + JSON.stringify(form.getFieldValue(path) ?? {}, null, 2), + ); + return ( + { + setText(next); + try { + form.setFieldValue(path, JSON.parse(next)); + } catch { + + } + }} + /> + ); +} + const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p })); const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const; const NODE_ELIGIBLE_PROTOCOLS = new Set([ @@ -1855,6 +1896,51 @@ export default function InboundFormModalNew({ ); + const advancedTab = ( + + ), + }, + ...(streamEnabled + ? [{ + key: 'stream', + label: t('pages.inbounds.advanced.stream'), + children: ( + + ), + }] + : []), + { + key: 'sniffing', + label: t('pages.inbounds.advanced.sniffing'), + children: ( + + ), + }, + ]} + /> + ); + const sniffingTab = ( <> @@ -1957,6 +2043,7 @@ export default function InboundFormModalNew({ ] : []), { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab }, + { key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab }, ]} />