mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(frontend): FinalMaskForm TCP Mask sub-forms + Advanced JSON wrap (B10/B11)
B10 — FinalMaskForm TCP Mask: after adding a mask and picking a Type
(Fragment/Header Custom/Sudoku), the type-specific sub-forms didn't
render. TcpMaskItem read `type` via Form.useWatch on a path inside
Form.List, which doesn't re-fire reliably in AntD 6.4.3 — same root
cause as the earlier B1/B2/B5 reactivity issues. Replaced with a
<Form.Item shouldUpdate> wrapper that reads `type` via getFieldValue
inside the render prop.
B11 — Advanced sub-tabs (settings / streamSettings / sniffing) showed
just the inner value (e.g. `{clients:[],decryption:"none",...}`), but
the legacy modal wrapped each slice with its key envelope (e.g.
`{settings:{...}}`) so the JSON matches the wire shape's slice and
round-trips cleanly from copy-pasted inbound configs. Added a
`wrapKey` prop to AdvancedSliceEditor that wraps/unwraps the value
on render/write; the three sub-tabs now pass settings / streamSettings
/ sniffing as their wrapKey.
This commit is contained in:
parent
60350f93e7
commit
36afdf53af
2 changed files with 77 additions and 52 deletions
|
|
@ -156,7 +156,6 @@ function TcpMaskItem({
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
const path = [...base, 'tcp', index];
|
const path = [...base, 'tcp', index];
|
||||||
const type = Form.useWatch([...path, 'type'], form) as string | undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -176,47 +175,60 @@ function TcpMaskItem({
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{type === 'fragment' && (
|
<Form.Item
|
||||||
<>
|
noStyle
|
||||||
<Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
|
shouldUpdate={(prev, curr) =>
|
||||||
<Select
|
(prev as Record<string, unknown>)[String(path[0])] !== (curr as Record<string, unknown>)[String(path[0])]
|
||||||
options={[
|
}
|
||||||
{ value: 'tlshello', label: 'tlshello' },
|
>
|
||||||
{ value: '1-3', label: '1-3' },
|
{({ getFieldValue }) => {
|
||||||
{ value: '1-5', label: '1-5' },
|
const type = getFieldValue([...path, 'type']) as string | undefined;
|
||||||
]}
|
if (type === 'fragment') {
|
||||||
/>
|
return (
|
||||||
</Form.Item>
|
<>
|
||||||
<Form.Item label="Length" name={[...path, 'settings', 'length']}>
|
<Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
|
||||||
<Input />
|
<Select
|
||||||
</Form.Item>
|
options={[
|
||||||
<Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
|
{ value: 'tlshello', label: 'tlshello' },
|
||||||
<Input />
|
{ value: '1-3', label: '1-3' },
|
||||||
</Form.Item>
|
{ value: '1-5', label: '1-5' },
|
||||||
<Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
|
]}
|
||||||
<Input />
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
<Form.Item label="Length" name={[...path, 'settings', 'length']}>
|
||||||
)}
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
{type === 'sudoku' && (
|
<Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
|
||||||
<>
|
<Input />
|
||||||
<Form.Item label="Password" name={[...path, 'settings', 'password']}><Input /></Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="ASCII" name={[...path, 'settings', 'ascii']}><Input /></Form.Item>
|
<Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
|
||||||
<Form.Item label="Custom Table" name={[...path, 'settings', 'customTable']}><Input /></Form.Item>
|
<Input />
|
||||||
<Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Padding Min" name={[...path, 'settings', 'paddingMin']}>
|
</>
|
||||||
<InputNumber min={0} />
|
);
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item label="Padding Max" name={[...path, 'settings', 'paddingMax']}>
|
if (type === 'sudoku') {
|
||||||
<InputNumber min={0} />
|
return (
|
||||||
</Form.Item>
|
<>
|
||||||
</>
|
<Form.Item label="Password" name={[...path, 'settings', 'password']}><Input /></Form.Item>
|
||||||
)}
|
<Form.Item label="ASCII" name={[...path, 'settings', 'ascii']}><Input /></Form.Item>
|
||||||
|
<Form.Item label="Custom Table" name={[...path, 'settings', 'customTable']}><Input /></Form.Item>
|
||||||
{type === 'header-custom' && (
|
<Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></Form.Item>
|
||||||
<HeaderCustomGroups base={[...path, 'settings']} form={form} />
|
<Form.Item label="Padding Min" name={[...path, 'settings', 'paddingMin']}>
|
||||||
)}
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Padding Max" name={[...path, 'settings', 'paddingMax']}>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'header-custom') {
|
||||||
|
return <HeaderCustomGroups base={[...path, 'settings']} form={form} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,33 +93,40 @@ const { Text } = Typography;
|
||||||
function AdvancedSliceEditor({
|
function AdvancedSliceEditor({
|
||||||
form,
|
form,
|
||||||
path,
|
path,
|
||||||
|
wrapKey,
|
||||||
minHeight,
|
minHeight,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
}: {
|
}: {
|
||||||
form: FormInstance<InboundFormValues>;
|
form: FormInstance<InboundFormValues>;
|
||||||
path: NamePath;
|
path: NamePath;
|
||||||
|
// When set, the editor wraps the inner value with `{ [wrapKey]: ... }` so
|
||||||
|
// the JSON the user sees matches the wire shape's slice envelope (e.g.
|
||||||
|
// `{ "settings": { ... } }`). Edits unwrap the outer key before writing
|
||||||
|
// back to the form. Mirrors the legacy modal's wrappedConfigValue.
|
||||||
|
wrapKey?: string;
|
||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
}) {
|
}) {
|
||||||
// The editor keeps a local text buffer so partial / invalid JSON typing
|
const serialize = (value: unknown): string => {
|
||||||
// doesn't clobber the form. lastEmitRef tracks the serialized form value
|
const inner = value ?? {};
|
||||||
// at the moment we last accepted a write — if useWatch later fires with
|
return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
|
||||||
// a different value than that, the form was changed from elsewhere
|
};
|
||||||
// (Stream tab toggle, sibling JSON tab edit), and we re-sync.
|
|
||||||
const watched = Form.useWatch(path, form);
|
const watched = Form.useWatch(path, form);
|
||||||
const lastEmitRef = useRef<string>('');
|
const lastEmitRef = useRef<string>('');
|
||||||
const [text, setText] = useState(() => {
|
const [text, setText] = useState(() => {
|
||||||
const initial = JSON.stringify(form.getFieldValue(path) ?? {}, null, 2);
|
const initial = serialize(form.getFieldValue(path));
|
||||||
lastEmitRef.current = initial;
|
lastEmitRef.current = initial;
|
||||||
return initial;
|
return initial;
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formStr = JSON.stringify(watched ?? {}, null, 2);
|
const formStr = serialize(watched);
|
||||||
if (formStr === lastEmitRef.current) return;
|
if (formStr === lastEmitRef.current) return;
|
||||||
setText(formStr);
|
setText(formStr);
|
||||||
lastEmitRef.current = formStr;
|
lastEmitRef.current = formStr;
|
||||||
}, [watched]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [watched, wrapKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
|
|
@ -130,8 +137,11 @@ function AdvancedSliceEditor({
|
||||||
setText(next);
|
setText(next);
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(next);
|
const parsed = JSON.parse(next);
|
||||||
form.setFieldValue(path, parsed);
|
const toWrite = wrapKey && parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||||
lastEmitRef.current = JSON.stringify(parsed, null, 2);
|
? (parsed as Record<string, unknown>)[wrapKey] ?? {}
|
||||||
|
: parsed;
|
||||||
|
form.setFieldValue(path, toWrite);
|
||||||
|
lastEmitRef.current = JSON.stringify(wrapKey ? { [wrapKey]: toWrite } : toWrite, null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
// invalid JSON; keep buffer, don't push to form
|
// invalid JSON; keep buffer, don't push to form
|
||||||
}
|
}
|
||||||
|
|
@ -2621,6 +2631,7 @@ export default function InboundFormModal({
|
||||||
<AdvancedSliceEditor
|
<AdvancedSliceEditor
|
||||||
form={form}
|
form={form}
|
||||||
path="settings"
|
path="settings"
|
||||||
|
wrapKey="settings"
|
||||||
minHeight="320px"
|
minHeight="320px"
|
||||||
maxHeight="540px"
|
maxHeight="540px"
|
||||||
/>
|
/>
|
||||||
|
|
@ -2640,6 +2651,7 @@ export default function InboundFormModal({
|
||||||
<AdvancedSliceEditor
|
<AdvancedSliceEditor
|
||||||
form={form}
|
form={form}
|
||||||
path="streamSettings"
|
path="streamSettings"
|
||||||
|
wrapKey="streamSettings"
|
||||||
minHeight="320px"
|
minHeight="320px"
|
||||||
maxHeight="540px"
|
maxHeight="540px"
|
||||||
/>
|
/>
|
||||||
|
|
@ -2659,6 +2671,7 @@ export default function InboundFormModal({
|
||||||
<AdvancedSliceEditor
|
<AdvancedSliceEditor
|
||||||
form={form}
|
form={form}
|
||||||
path="sniffing"
|
path="sniffing"
|
||||||
|
wrapKey="sniffing"
|
||||||
minHeight="240px"
|
minHeight="240px"
|
||||||
maxHeight="420px"
|
maxHeight="420px"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue