mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-28 16:09:36 +00:00
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).
This commit is contained in:
@@ -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<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 TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
||||
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 = (
|
||||
<>
|
||||
<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: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab },
|
||||
]} />
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user