From 5f7c2424ef58f8692475a89cfafc60cab9208095 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 05:56:58 +0200 Subject: [PATCH] fix(frontend): preserve routing-rule fields the form does not surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/pages/xray/routing/RuleFormModal.tsx | 6 ++++ .../test/rule-form-preserve-fields.test.tsx | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 frontend/src/test/rule-form-preserve-fields.test.tsx diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx index 849678dbb..f214305cc 100644 --- a/frontend/src/pages/xray/routing/RuleFormModal.tsx +++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx @@ -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 = {}; + 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; diff --git a/frontend/src/test/rule-form-preserve-fields.test.tsx b/frontend/src/test/rule-form-preserve-fields.test.tsx new file mode 100644 index 000000000..97ce1f508 --- /dev/null +++ b/frontend/src/test/rule-form-preserve-fields.test.tsx @@ -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( + + + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'Save Changes' })); + + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(onConfirm.mock.calls[0][0]).toMatchObject({ ruleTag: 'my-tag' }); + }); +});