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:
MHSanaei 2026-05-26 16:08:52 +02:00
parent 60350f93e7
commit 36afdf53af
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 77 additions and 52 deletions

View file

@ -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,7 +175,16 @@ function TcpMaskItem({
/> />
</Form.Item> </Form.Item>
{type === 'fragment' && ( <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']}> <Form.Item label="Packets" name={[...path, 'settings', 'packets']}>
<Select <Select
@ -197,9 +205,10 @@ function TcpMaskItem({
<Input /> <Input />
</Form.Item> </Form.Item>
</> </>
)} );
}
{type === 'sudoku' && ( if (type === 'sudoku') {
return (
<> <>
<Form.Item label="Password" name={[...path, 'settings', 'password']}><Input /></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="ASCII" name={[...path, 'settings', 'ascii']}><Input /></Form.Item>
@ -212,11 +221,14 @@ function TcpMaskItem({
<InputNumber min={0} /> <InputNumber min={0} />
</Form.Item> </Form.Item>
</> </>
)} );
}
{type === 'header-custom' && ( if (type === 'header-custom') {
<HeaderCustomGroups base={[...path, 'settings']} form={form} /> return <HeaderCustomGroups base={[...path, 'settings']} form={form} />;
)} }
return null;
}}
</Form.Item>
</div> </div>
); );
} }

View file

@ -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"
/> />