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 7449801a [ISSUE #1615] Prevent duplicate general settings saves (#1625)
7449801a is described below

commit 7449801ac9cc1299d0ef39487b9d543c4fb1fe91
Author: 0 <[email protected]>
AuthorDate: Tue Aug 11 20:31:34 2026 +0800

    [ISSUE #1615] Prevent duplicate general settings saves (#1625)
---
 .../settings/__tests__/GeneralSettingsTab.test.tsx | 75 ++++++++++++++++++++++
 web/src/pages/settings/index.tsx                   |  8 ++-
 2 files changed, 81 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx 
b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
new file mode 100644
index 00000000..f9ceed58
--- /dev/null
+++ b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { App } from 'antd';
+import { getGeneralSettings, saveGeneralSettings } from 
'../../../api/settings';
+import { GeneralSettingsTab } from '../index';
+
+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(),
+}));
+
+describe('GeneralSettingsTab', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    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',
+    });
+  });
+
+  it('ignores a duplicate submit while a save is in flight', async () => {
+    vi.mocked(saveGeneralSettings).mockImplementation(() => new Promise(() => 
{}));
+    render(
+      <App>
+        <GeneralSettingsTab />
+      </App>,
+    );
+
+    const saveButton = await screen.findByRole('button', { name: '保存设置' });
+    await waitFor(() => expect(saveButton).toBeEnabled());
+    const form = saveButton.closest('form');
+    expect(form).not.toBeNull();
+
+    fireEvent.submit(form!);
+    fireEvent.submit(form!);
+
+    await waitFor(() => expect(saveGeneralSettings).toHaveBeenCalledTimes(1));
+  });
+});
diff --git a/web/src/pages/settings/index.tsx b/web/src/pages/settings/index.tsx
index 1cb408bb..eb1384d4 100644
--- a/web/src/pages/settings/index.tsx
+++ b/web/src/pages/settings/index.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import {
   Button,
   Checkbox,
@@ -108,10 +108,11 @@ const withoutSecrets = (values: DataSourceFormValues): 
Partial<DataSource> => {
 
 // ─── General Settings Tab ───────────────────────────────────────────────────
 
-const GeneralSettingsTab = () => {
+export const GeneralSettingsTab = () => {
   const [form] = Form.useForm<GeneralSettingsUpdate>();
   const [loading, setLoading] = useState(true);
   const [saving, setSaving] = useState(false);
+  const saveInFlightRef = useRef(false);
   const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
   const clearApiKey = Form.useWatch('clearApiKey', form);
 
@@ -137,6 +138,8 @@ const GeneralSettingsTab = () => {
   }, [form]);
 
   const handleFinish = async (values: GeneralSettingsUpdate) => {
+    if (saveInFlightRef.current) return;
+    saveInFlightRef.current = true;
     setSaving(true);
     try {
       await saveGeneralSettings(values);
@@ -148,6 +151,7 @@ const GeneralSettingsTab = () => {
     } catch {
       message.error('设置保存失败,请稍后重试');
     } finally {
+      saveInFlightRef.current = false;
       setSaving(false);
     }
   };

Reply via email to