This is an automated email from the ASF dual-hosted git repository.

moonming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ec818cf5 Fix/unsaved plugin config warning (#3333)
2ec818cf5 is described below

commit 2ec818cf53a8f3f99cb4c9e9da93fc9f039c7bd6
Author: Kumar Vanshaj <[email protected]>
AuthorDate: Wed May 20 05:02:07 2026 +0530

    Fix/unsaved plugin config warning (#3333)
    
    * fix(i18n): add missing services.empty translation key in zh, de, es, and 
tr locales
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * fix(i18n): fix button label consistency in es and tr services.empty 
translations
    
    * fix(ux): show confirmation dialog when closing plugin editor with unsaved 
changes
    
    * fix(ux): show confirmation dialog when closing plugin editor with unsaved 
changes
    
    * fix(a11y): add aria-label to plugin editor drawer close button
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../plugin_editor_drawer.unsaved-changes.spec.ts   | 88 ++++++++++++++++++++++
 .../FormItemPlugins/PluginEditorDrawer.tsx         | 24 +++++-
 src/locales/de/common.json                         |  5 ++
 src/locales/en/common.json                         |  5 ++
 src/locales/es/common.json                         |  5 ++
 src/locales/tr/common.json                         |  5 ++
 src/locales/zh/common.json                         |  5 ++
 7 files changed, 133 insertions(+), 4 deletions(-)

diff --git a/e2e/tests/plugin_editor_drawer.unsaved-changes.spec.ts 
b/e2e/tests/plugin_editor_drawer.unsaved-changes.spec.ts
new file mode 100644
index 000000000..63f0a5082
--- /dev/null
+++ b/e2e/tests/plugin_editor_drawer.unsaved-changes.spec.ts
@@ -0,0 +1,88 @@
+/**
+ * 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 { pluginConfigsPom } from '@e2e/pom/plugin_configs';
+import { test } from '@e2e/utils/test';
+import { uiFillMonacoEditor, uiGetMonacoEditor } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+const openResponseRewriteDrawer = async (page: Parameters<typeof 
test>[1]['page']) => {
+  await pluginConfigsPom.toAdd(page);
+  await pluginConfigsPom.isAddPage(page);
+
+  await page.getByRole('button', { name: 'Select Plugins' }).click();
+
+  const selectPluginsDialog = page.getByRole('dialog', { name: 'Select 
Plugins' });
+  await 
selectPluginsDialog.getByPlaceholder('Search').fill('response-rewrite');
+  await selectPluginsDialog
+    .getByTestId('plugin-response-rewrite')
+    .getByRole('button', { name: 'Add' })
+    .click();
+
+  const drawer = page.getByRole('dialog', { name: 'Add Plugin' });
+  await expect(drawer).toBeVisible();
+  return drawer;
+};
+
+test('should show unsaved changes dialog when closing plugin editor with 
unsaved changes', async ({
+  page,
+}) => {
+  const drawer = await openResponseRewriteDrawer(page);
+
+  await test.step('make changes in the editor', async () => {
+    const editor = await uiGetMonacoEditor(page, drawer);
+    await uiFillMonacoEditor(page, editor, '{"body": "test"}');
+  });
+
+  await test.step('closing with unsaved changes shows confirmation dialog', 
async () => {
+    await drawer.getByRole('button', { name: 'Close' }).click();
+
+    const confirmModal = page.getByRole('dialog', { name: 'Unsaved Changes' });
+    await expect(confirmModal).toBeVisible();
+    await expect(
+      confirmModal.getByText('You have unsaved changes. Are you sure you want 
to close?')
+    ).toBeVisible();
+  });
+
+  await test.step('cancel keeps drawer open with changes preserved', async () 
=> {
+    const confirmModal = page.getByRole('dialog', { name: 'Unsaved Changes' });
+    await confirmModal.getByRole('button', { name: 'Cancel' }).click();
+
+    await expect(confirmModal).toBeHidden();
+    await expect(drawer).toBeVisible();
+  });
+
+  await test.step('discard changes closes the drawer', async () => {
+    await drawer.getByRole('button', { name: 'Close' }).click();
+
+    const confirmModal = page.getByRole('dialog', { name: 'Unsaved Changes' });
+    await confirmModal.getByRole('button', { name: 'Discard Changes' 
}).click();
+
+    await expect(confirmModal).toBeHidden();
+    await expect(drawer).toBeHidden();
+  });
+});
+
+test('should close plugin editor silently when there are no unsaved changes', 
async ({ page }) => {
+  const drawer = await openResponseRewriteDrawer(page);
+
+  await test.step('closing without changes does not show confirmation dialog', 
async () => {
+    await drawer.getByRole('button', { name: 'Close' }).click();
+
+    await expect(page.getByRole('dialog', { name: 'Unsaved Changes' 
})).toBeHidden();
+    await expect(drawer).toBeHidden();
+  });
+});
diff --git a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx 
b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
index 1547af8c7..25175b420 100644
--- a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
+++ b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
@@ -14,7 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Drawer, Group, Title } from '@mantine/core';
+import { Drawer, Group, Text, Title } from '@mantine/core';
+import { modals } from '@mantine/modals';
 import { isEmpty, isNil } from 'rambdax';
 import { useEffect } from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
@@ -47,8 +48,21 @@ export const PluginEditorDrawer = (props: 
PluginEditorDrawerProps) => {
     defaultValues: { config: toConfigStr(config) },
   });
   const handleClose = () => {
-    onClose();
-    methods.reset();
+    if (mode !== 'view' && methods.getValues('config') !== 
toConfigStr(config)) {
+      modals.openConfirmModal({
+        centered: true,
+        title: t('info.unsaved.title'),
+        children: <Text size="sm">{t('info.unsaved.content')}</Text>,
+        labels: { confirm: t('info.unsaved.confirm'), cancel: 
t('form.btn.cancel') },
+        onConfirm: () => {
+          onClose();
+          methods.reset();
+        },
+      });
+    } else {
+      onClose();
+      methods.reset();
+    }
   };
 
   useEffect(() => {
@@ -64,6 +78,7 @@ export const PluginEditorDrawer = (props: 
PluginEditorDrawerProps) => {
       closeOnEscape={false}
       opened={opened}
       onClose={handleClose}
+      closeButtonProps={{ 'aria-label': 'Close' }}
       styles={{ body: { paddingTop: '18px' } }}
       {...(mode === 'add' && { title: t('form.plugins.addPlugin') })}
       {...(mode === 'edit' && { title: t('form.plugins.editPlugin') })}
@@ -91,7 +106,8 @@ export const PluginEditorDrawer = (props: 
PluginEditorDrawerProps) => {
               variant="light"
               onClick={methods.handleSubmit(({ config }) => {
                 onSave({ name, config: JSON.parse(config) });
-                handleClose();
+                onClose();
+                methods.reset();
               })}
             >
               {mode === 'add' && t('form.btn.add')}
diff --git a/src/locales/de/common.json b/src/locales/de/common.json
index 89817b6a1..95cfb9ad5 100644
--- a/src/locales/de/common.json
+++ b/src/locales/de/common.json
@@ -304,6 +304,11 @@
     "edit": {
       "success": "{{name}} erfolgreich bearbeitet",
       "title": "{{name}} bearbeiten"
+    },
+    "unsaved": {
+      "confirm": "Änderungen verwerfen",
+      "content": "Sie haben nicht gespeicherte Änderungen. Möchten Sie 
wirklich schließen?",
+      "title": "Nicht gespeicherte Änderungen"
     }
   },
   "mark": {
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 6544bf790..9625981d2 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -304,6 +304,11 @@
     "edit": {
       "success": "Edit {{name}} Successfully",
       "title": "Edit {{name}}"
+    },
+    "unsaved": {
+      "confirm": "Discard Changes",
+      "content": "You have unsaved changes. Are you sure you want to close?",
+      "title": "Unsaved Changes"
     }
   },
   "mark": {
diff --git a/src/locales/es/common.json b/src/locales/es/common.json
index d46f14f9d..b57953e9c 100644
--- a/src/locales/es/common.json
+++ b/src/locales/es/common.json
@@ -304,6 +304,11 @@
     "edit": {
       "success": "Editado {{name}} con éxito",
       "title": "Editar {{name}}"
+    },
+    "unsaved": {
+      "confirm": "Descartar cambios",
+      "content": "Tiene cambios sin guardar. ¿Está seguro de que desea 
cerrar?",
+      "title": "Cambios sin guardar"
     }
   },
   "mark": {
diff --git a/src/locales/tr/common.json b/src/locales/tr/common.json
index 63745f663..46da33e48 100644
--- a/src/locales/tr/common.json
+++ b/src/locales/tr/common.json
@@ -304,6 +304,11 @@
     "edit": {
       "success": "{{name}} başarıyla güncellendi",
       "title": "{{name}} Düzenle"
+    },
+    "unsaved": {
+      "confirm": "Değişiklikleri at",
+      "content": "Kaydedilmemiş değişiklikleriniz var. Kapatmak istediğinizden 
emin misiniz?",
+      "title": "Kaydedilmemiş Değişiklikler"
     }
   },
   "mark": {
diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json
index b5861692f..a8097fa1d 100644
--- a/src/locales/zh/common.json
+++ b/src/locales/zh/common.json
@@ -304,6 +304,11 @@
     "edit": {
       "success": "编辑 {{name}} 成功",
       "title": "编辑 {{name}}"
+    },
+    "unsaved": {
+      "confirm": "放弃更改",
+      "content": "您有未保存的更改,确定要关闭吗?",
+      "title": "未保存的更改"
     }
   },
   "mark": {

Reply via email to