mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-26 21:17:14 +00:00
fix(ui): stop the sniffing form island from clobbering unrendered fields
antd's Form.useWatch only reports registered fields, so while the
sniffing toggle was off the island emitted { enabled: false } upward and
replaced the full Sniffing object in form state. Saving a VLESS reverse
outbound then crashed in sniffingToWire on the missing ipsExcluded
array; the loopback outbound and the inbound sniffing tab shared the
same hole. Watch the store with preserve: true so unrendered fields
keep their values, and seed a missing value from the schema defaults
instead of an empty cast.
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
|
||||||
import SniffingFields from '@/lib/xray/forms/SniffingFields';
|
import SniffingFields from '@/lib/xray/forms/SniffingFields';
|
||||||
import type { Sniffing } from '@/schemas/primitives/sniffing';
|
import { SniffingSchema, type Sniffing } from '@/schemas/primitives/sniffing';
|
||||||
|
|
||||||
interface SniffingFieldProps {
|
interface SniffingFieldProps {
|
||||||
value?: Sniffing;
|
value?: Sniffing;
|
||||||
@@ -12,12 +12,12 @@ interface SniffingFieldProps {
|
|||||||
|
|
||||||
export default function SniffingField({ value, onChange, enableLabel }: SniffingFieldProps) {
|
export default function SniffingField({ value, onChange, enableLabel }: SniffingFieldProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initial] = useState(() => value ?? ({} as Sniffing));
|
const [initial] = useState(() => value ?? SniffingSchema.parse({}));
|
||||||
const onChangeRef = useRef(onChange);
|
const onChangeRef = useRef(onChange);
|
||||||
onChangeRef.current = onChange;
|
onChangeRef.current = onChange;
|
||||||
const lastEmitted = useRef(JSON.stringify(initial));
|
const lastEmitted = useRef(JSON.stringify(initial));
|
||||||
|
|
||||||
const sniffing = Form.useWatch('sniffing', form) as Sniffing | undefined;
|
const sniffing = Form.useWatch('sniffing', { form, preserve: true }) as Sniffing | undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sniffing === undefined) return;
|
if (sniffing === undefined) return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { act } from '@testing-library/react';
|
import { act, fireEvent } from '@testing-library/react';
|
||||||
|
|
||||||
import OutboundFormModal from '@/pages/xray/outbounds/OutboundFormModal';
|
import OutboundFormModal from '@/pages/xray/outbounds/OutboundFormModal';
|
||||||
import {
|
import {
|
||||||
@@ -54,4 +54,41 @@ describe('OutboundFormModal', () => {
|
|||||||
expect(labelsByProto.wireguard).not.toContain('Encryption');
|
expect(labelsByProto.wireguard).not.toContain('Encryption');
|
||||||
}
|
}
|
||||||
}, 30000); // iterates every protocol, re-rendering a heavy modal each time — slow on CI runners
|
}, 30000); // iterates every protocol, re-rendering a heavy modal each time — slow on CI runners
|
||||||
|
|
||||||
|
it('saves a vless reverse outbound while reverse sniffing stays disabled', async () => {
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
renderWithProviders(
|
||||||
|
<OutboundFormModal
|
||||||
|
open
|
||||||
|
outbound={{
|
||||||
|
protocol: 'vless',
|
||||||
|
tag: 'reverse-out',
|
||||||
|
settings: {
|
||||||
|
vnext: [{
|
||||||
|
address: 'example.com',
|
||||||
|
port: 443,
|
||||||
|
users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
|
||||||
|
}],
|
||||||
|
reverse: { tag: 'r1', sniffing: {} },
|
||||||
|
},
|
||||||
|
streamSettings: { network: 'tcp', security: 'none' },
|
||||||
|
}}
|
||||||
|
existingTags={[]}
|
||||||
|
onClose={() => {}}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
|
||||||
|
|
||||||
|
const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
|
||||||
|
expect(ok).toBeTruthy();
|
||||||
|
await act(async () => { fireEvent.click(ok); });
|
||||||
|
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
|
||||||
|
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = onConfirm.mock.calls[0][0] as {
|
||||||
|
settings: { reverse?: { tag?: string } };
|
||||||
|
};
|
||||||
|
expect(payload.settings.reverse?.tag).toBe('r1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user