mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-23 11:27:13 +00:00
fix(frontend): preserve routing-rule fields the form does not surface
The rule form rebuilt the rule from a fixed literal of only the fields it edits, and RoutingTab replaces the rule wholesale on confirm. Fields the form never exposes — localPort, localIP, process, ruleTag, webhook — are in the rule schema and can arrive via the advanced JSON editor or Import Rules; opening such a rule in the form and saving silently dropped them. Carry over every key of the original rule the form does not manage before applying the form-derived fields, so an edit only touches what it surfaces.
This commit is contained in:
@@ -126,7 +126,13 @@ export default function RuleFormModal({
|
|||||||
outboundTag: v.outboundTag === '' ? undefined : v.outboundTag,
|
outboundTag: v.outboundTag === '' ? undefined : v.outboundTag,
|
||||||
balancerTag: v.balancerTag === '' ? undefined : v.balancerTag,
|
balancerTag: v.balancerTag === '' ? undefined : v.balancerTag,
|
||||||
};
|
};
|
||||||
|
const managedKeys = new Set(Object.keys(built));
|
||||||
const out: Record<string, unknown> = {};
|
const out: Record<string, unknown> = {};
|
||||||
|
if (rule) {
|
||||||
|
for (const [key, value] of Object.entries(rule)) {
|
||||||
|
if (!managedKeys.has(key) && value !== undefined) out[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const [k, v] of Object.entries(built)) {
|
for (const [k, v] of Object.entries(built)) {
|
||||||
if (v == null) continue;
|
if (v == null) continue;
|
||||||
if (Array.isArray(v) && v.length === 0) continue;
|
if (Array.isArray(v) && v.length === 0) continue;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { fireEvent, screen } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import RuleFormModal from '@/pages/xray/routing/RuleFormModal';
|
||||||
|
|
||||||
|
import { renderWithProviders } from './test-utils';
|
||||||
|
|
||||||
|
describe('RuleFormModal edit preserves unsurfaced fields', () => {
|
||||||
|
it('keeps a field the form does not surface (ruleTag) when saving an edit', () => {
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RuleFormModal
|
||||||
|
open
|
||||||
|
rule={{ type: 'field', outboundTag: 'block', ruleTag: 'my-tag', enabled: true }}
|
||||||
|
inboundTags={[]}
|
||||||
|
outboundTags={['block']}
|
||||||
|
balancerTags={[]}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
|
||||||
|
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onConfirm.mock.calls[0][0]).toMatchObject({ ruleTag: 'my-tag' });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user