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:
MHSanaei
2026-07-15 05:56:58 +02:00
parent 980c023287
commit 5f7c2424ef
2 changed files with 39 additions and 0 deletions
@@ -126,7 +126,13 @@ export default function RuleFormModal({
outboundTag: v.outboundTag === '' ? undefined : v.outboundTag,
balancerTag: v.balancerTag === '' ? undefined : v.balancerTag,
};
const managedKeys = new Set(Object.keys(built));
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)) {
if (v == null) 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' });
});
});