mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 16:39:35 +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:
@@ -156,7 +156,6 @@ function TcpMaskItem({
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const path = [...base, 'tcp', index];
|
||||
const type = Form.useWatch([...path, 'type'], form) as string | undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -176,47 +175,60 @@ function TcpMaskItem({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{type === 'fragment' && (
|
||||
<>
|
||||
<Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'tlshello', label: 'tlshello' },
|
||||
{ value: '1-3', label: '1-3' },
|
||||
{ value: '1-5', label: '1-5' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Length" name={[...path, 'settings', 'length']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'sudoku' && (
|
||||
<>
|
||||
<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>
|
||||
<Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></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']}>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'header-custom' && (
|
||||
<HeaderCustomGroups base={[...path, 'settings']} form={form} />
|
||||
)}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, curr) =>
|
||||
(prev as Record<string, unknown>)[String(path[0])] !== (curr as Record<string, unknown>)[String(path[0])]
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const type = getFieldValue([...path, 'type']) as string | undefined;
|
||||
if (type === 'fragment') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'tlshello', label: 'tlshello' },
|
||||
{ value: '1-3', label: '1-3' },
|
||||
{ value: '1-5', label: '1-5' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Length" name={[...path, 'settings', 'length']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Delay" name={[...path, 'settings', 'delay']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Split" name={[...path, 'settings', 'maxSplit']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (type === 'sudoku') {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<Form.Item label="Custom Tables" name={[...path, 'settings', 'customTables']}><Input /></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']}>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (type === 'header-custom') {
|
||||
return <HeaderCustomGroups base={[...path, 'settings']} form={form} />;
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,33 +93,40 @@ const { Text } = Typography;
|
||||
function AdvancedSliceEditor({
|
||||
form,
|
||||
path,
|
||||
wrapKey,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
}: {
|
||||
form: FormInstance<InboundFormValues>;
|
||||
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;
|
||||
maxHeight?: string;
|
||||
}) {
|
||||
// The editor keeps a local text buffer so partial / invalid JSON typing
|
||||
// doesn't clobber the form. lastEmitRef tracks the serialized form value
|
||||
// at the moment we last accepted a write — if useWatch later fires with
|
||||
// a different value than that, the form was changed from elsewhere
|
||||
// (Stream tab toggle, sibling JSON tab edit), and we re-sync.
|
||||
const serialize = (value: unknown): string => {
|
||||
const inner = value ?? {};
|
||||
return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
|
||||
};
|
||||
|
||||
const watched = Form.useWatch(path, form);
|
||||
const lastEmitRef = useRef<string>('');
|
||||
const [text, setText] = useState(() => {
|
||||
const initial = JSON.stringify(form.getFieldValue(path) ?? {}, null, 2);
|
||||
const initial = serialize(form.getFieldValue(path));
|
||||
lastEmitRef.current = initial;
|
||||
return initial;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const formStr = JSON.stringify(watched ?? {}, null, 2);
|
||||
const formStr = serialize(watched);
|
||||
if (formStr === lastEmitRef.current) return;
|
||||
setText(formStr);
|
||||
lastEmitRef.current = formStr;
|
||||
}, [watched]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [watched, wrapKey]);
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
@@ -130,8 +137,11 @@ function AdvancedSliceEditor({
|
||||
setText(next);
|
||||
try {
|
||||
const parsed = JSON.parse(next);
|
||||
form.setFieldValue(path, parsed);
|
||||
lastEmitRef.current = JSON.stringify(parsed, null, 2);
|
||||
const toWrite = wrapKey && parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)[wrapKey] ?? {}
|
||||
: parsed;
|
||||
form.setFieldValue(path, toWrite);
|
||||
lastEmitRef.current = JSON.stringify(wrapKey ? { [wrapKey]: toWrite } : toWrite, null, 2);
|
||||
} catch {
|
||||
// invalid JSON; keep buffer, don't push to form
|
||||
}
|
||||
@@ -2621,6 +2631,7 @@ export default function InboundFormModal({
|
||||
<AdvancedSliceEditor
|
||||
form={form}
|
||||
path="settings"
|
||||
wrapKey="settings"
|
||||
minHeight="320px"
|
||||
maxHeight="540px"
|
||||
/>
|
||||
@@ -2640,6 +2651,7 @@ export default function InboundFormModal({
|
||||
<AdvancedSliceEditor
|
||||
form={form}
|
||||
path="streamSettings"
|
||||
wrapKey="streamSettings"
|
||||
minHeight="320px"
|
||||
maxHeight="540px"
|
||||
/>
|
||||
@@ -2659,6 +2671,7 @@ export default function InboundFormModal({
|
||||
<AdvancedSliceEditor
|
||||
form={form}
|
||||
path="sniffing"
|
||||
wrapKey="sniffing"
|
||||
minHeight="240px"
|
||||
maxHeight="420px"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user