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 506f74a7d fix(settings): abort saves when the fresh settings read 
fails (#5212)
506f74a7d is described below

commit 506f74a7d8b2c7504fc681298984cd465ba642db
Author: 健康宝 <[email protected]>
AuthorDate: Fri Oct 9 11:24:56 2026 +0800

    fix(settings): abort saves when the fresh settings read fails (#5212)
    
    `loadFreshSettings` swallowed a failed read and returned the mount-time 
snapshot. Since
    `buildPayload` sends the whole 14-field record, one read hiccup wrote stale 
values over whatever
    another tab had just saved - contradicting the comment right above it. Let 
the error propagate to
    the existing toast and drop the now-unused fallback parameter; both call 
sites and all four
    `mergeAndSave` consumers stay consistent.
---
 web/src/pages/settings/GeneralSettingsTab.tsx         | 19 +++++++++----------
 .../settings/__tests__/GeneralSettingsTab.test.tsx    | 14 ++++++++++++++
 2 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/web/src/pages/settings/GeneralSettingsTab.tsx 
b/web/src/pages/settings/GeneralSettingsTab.tsx
index b2656dba0..dc9f16113 100644
--- a/web/src/pages/settings/GeneralSettingsTab.tsx
+++ b/web/src/pages/settings/GeneralSettingsTab.tsx
@@ -108,14 +108,13 @@ export const GeneralSettingsTab = () => {
 
   // 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.
-  const loadFreshSettings = async (fallback: GeneralSettings): 
Promise<GeneralSettings> => {
-    try {
-      const fresh = await getGeneralSettings();
-      setSettings(fresh);
-      return fresh;
-    } catch {
-      return fallback;
-    }
+  // When that fresh read fails the save aborts here and the callers report 
it: falling back to
+  // the mount-time snapshot would write a whole record of stale values over 
what another tab
+  // just saved.
+  const loadFreshSettings = async (): Promise<GeneralSettings> => {
+    const fresh = await getGeneralSettings();
+    setSettings(fresh);
+    return fresh;
   };
 
   const persistPreference = async (patch: Partial<GeneralSettings>) => {
@@ -124,7 +123,7 @@ export const GeneralSettingsTab = () => {
     setSettings(next);
     setSavingPreference(true);
     try {
-      const base = await loadFreshSettings(settings);
+      const base = await loadFreshSettings();
       await saveGeneralSettings(buildPayload({ ...base, ...patch }));
       setSettings({ ...base, ...patch });
       message.success(t('settings.saveSuccess'));
@@ -138,7 +137,7 @@ export const GeneralSettingsTab = () => {
   const mergeAndSave = async (patch: Partial<GeneralSettings>) => {
     if (!settings) return false;
     try {
-      const base = await loadFreshSettings(settings);
+      const base = await loadFreshSettings();
       await saveGeneralSettings(buildPayload({ ...base, ...patch }));
       const statePatch = { ...patch };
       delete statePatch.clearDingtalkSigningSecret;
diff --git a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx 
b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
index 6faa0e9ef..7eb329042 100644
--- a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
+++ b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
@@ -200,6 +200,20 @@ describe('GeneralSettingsTab', () => {
     );
   });
 
+  it('aborts the save when the fresh settings read fails', async () => {
+    renderTab();
+
+    const saveButtons = await screen.findAllByRole('button', { name: '保存设置' });
+    await waitFor(() => expect(saveButtons[0]).toBeEnabled());
+    // The fresh read that every save is built from now fails: writing the 
patch on top of the
+    // mount-time snapshot would clobber fields another tab saved in the 
meantime.
+    vi.mocked(getGeneralSettings).mockRejectedValueOnce(new Error('boom'));
+    fireEvent.submit(saveButtons[0].closest('form')!);
+
+    await waitFor(() => 
expect(screen.getByText('设置保存失败,请稍后重试')).toBeInTheDocument());
+    expect(saveGeneralSettings).not.toHaveBeenCalled();
+  });
+
   it('can explicitly clear a configured DingTalk signing secret', async () => {
     vi.mocked(getGeneralSettings).mockResolvedValue({
       theme: 'system',

Reply via email to