This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 0565bbc04 fix(settings): keep unsaved edits when the display language
changes (#4588)
0565bbc04 is described below
commit 0565bbc0403fc1542d22a0ad8d676f545609aca4
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Oct 1 17:19:40 2026 +0800
fix(settings): keep unsaved edits when the display language changes (#4588)
The general settings tab loads the record once and fills the notification
and
security forms from it. The load effect depended on the translation
function,
whose identity changes with the display language, so switching the language
re-ran the effect and overwrote whatever the operator had typed but not yet
saved, without any warning.
Key the effect on mount only and reach the current translation through a
ref,
so unsaved input survives a language change.
Co-authored-by: unbridled-41
<[email protected]>
---
web/src/pages/settings/GeneralSettingsTab.tsx | 11 ++-
.../__tests__/GeneralSettingsFormResidue.test.tsx | 110 +++++++++++++++++++++
2 files changed, 119 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/settings/GeneralSettingsTab.tsx
b/web/src/pages/settings/GeneralSettingsTab.tsx
index bfc204cad..b2656dba0 100644
--- a/web/src/pages/settings/GeneralSettingsTab.tsx
+++ b/web/src/pages/settings/GeneralSettingsTab.tsx
@@ -70,9 +70,16 @@ export const GeneralSettingsTab = () => {
const [testingChannel, setTestingChannel] = useState<string>();
const securityInFlightRef = useRef(false);
const notifyInFlightRef = useRef(false);
+ const translationRef = useRef(t);
const [securityForm] = Form.useForm();
const [notifyForm] = Form.useForm();
+ // The load effect below fills both forms from the server snapshot and must
therefore run only
+ // once per mount, while the translation function changes identity with the
display language.
+ useEffect(() => {
+ translationRef.current = t;
+ }, [t]);
+
useEffect(() => {
let cancelled = false;
void getGeneralSettings()
@@ -88,7 +95,7 @@ export const GeneralSettingsTab = () => {
});
})
.catch(() => {
- if (!cancelled) message.error(t('settings.loadFailed'));
+ if (!cancelled)
message.error(translationRef.current('settings.loadFailed'));
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -97,7 +104,7 @@ export const GeneralSettingsTab = () => {
return () => {
cancelled = true;
};
- }, [message, notifyForm, securityForm, t]);
+ }, [message, notifyForm, securityForm]);
// Other tabs (AI assistant settings) write the same settings record while
this tab stays
// mounted, so every save must be built from a fresh read instead of the
mount-time snapshot.
diff --git
a/web/src/pages/settings/__tests__/GeneralSettingsFormResidue.test.tsx
b/web/src/pages/settings/__tests__/GeneralSettingsFormResidue.test.tsx
new file mode 100644
index 000000000..e7b82b05d
--- /dev/null
+++ b/web/src/pages/settings/__tests__/GeneralSettingsFormResidue.test.tsx
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import { getGeneralSettings, saveGeneralSettings } from
'../../../api/settings';
+import { LangProvider, useLang } from '../../../i18n/LangContext';
+import { ThemeProvider } from '../../../theme/ThemeProvider';
+import { GeneralSettingsTab } from '../GeneralSettingsTab';
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+vi.mock('../../../api/settings', () => ({
+ createDataSource: vi.fn(),
+ deleteDataSource: vi.fn(),
+ getGeneralSettings: vi.fn(),
+ listDataSources: vi.fn(),
+ saveGeneralSettings: vi.fn(),
+ testDataSource: vi.fn(),
+ updateDataSource: vi.fn(),
+}));
+
+const LanguageSwitch = () => {
+ const { setLang } = useLang();
+ return (
+ <button type="button" onClick={() => setLang('en')}>
+ switch-language
+ </button>
+ );
+};
+
+const renderTab = () =>
+ render(
+ <App>
+ <LangProvider>
+ <LanguageSwitch />
+ <ThemeProvider>
+ <GeneralSettingsTab />
+ </ThemeProvider>
+ </LangProvider>
+ </App>,
+ );
+
+describe('GeneralSettingsTab unsaved input', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ localStorage.clear();
+ vi.mocked(saveGeneralSettings).mockResolvedValue(undefined);
+ vi.mocked(getGeneralSettings).mockResolvedValue({
+ theme: 'system',
+ compact: false,
+ desktopNotify: false,
+ notifySound: false,
+ sessionTimeout: 30,
+ requireLogin: true,
+ llmProvider: 'openai',
+ apiKeyConfigured: false,
+ model: 'test-model',
+ baseUrl: 'https://example.test/v1',
+ dingtalkWebhook:
'https://oapi.dingtalk.com/robot/send?access_token=server',
+ emailRecipients: '[email protected]',
+ });
+ });
+
+ it('keeps unsaved notification edits when the display language changes',
async () => {
+ const user = userEvent.setup();
+ renderTab();
+
+ const webhook = await screen.findByDisplayValue(
+ 'https://oapi.dingtalk.com/robot/send?access_token=server',
+ );
+ await user.clear(webhook);
+ await user.type(webhook,
'https://oapi.dingtalk.com/robot/send?access_token=edited');
+
expect(webhook).toHaveValue('https://oapi.dingtalk.com/robot/send?access_token=edited');
+
+ await user.click(screen.getByRole('button', { name: 'switch-language' }));
+
+
expect(webhook).toHaveValue('https://oapi.dingtalk.com/robot/send?access_token=edited');
+ }, 20_000);
+});