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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6cacbba1 fix: keep header editor row identities stable (#677)
6cacbba1 is described below

commit 6cacbba138bf1785cd76eb06ee92e0bd9d9faecd
Author: Carlos Acosta <[email protected]>
AuthorDate: Sun Sep 27 00:29:04 2026 -0500

    fix: keep header editor row identities stable (#677)
    
    Co-authored-by: Aeontra <aeontra@localhost>
---
 src/routes/Document/components/HeadersEditor.js    | 42 ++++++++++--
 .../Document/components/HeadersEditor.test.js      | 79 ++++++++++++++++++++++
 2 files changed, 115 insertions(+), 6 deletions(-)

diff --git a/src/routes/Document/components/HeadersEditor.js 
b/src/routes/Document/components/HeadersEditor.js
index f456d392..9671e5c3 100644
--- a/src/routes/Document/components/HeadersEditor.js
+++ b/src/routes/Document/components/HeadersEditor.js
@@ -20,13 +20,40 @@ import React, { Fragment, useEffect, useState } from 
"react";
 
 const { Text } = Typography;
 
+export const getNextEditorRowIndex = (rows) =>
+  rows.reduce(
+    (maxIndex, item) =>
+      Number.isInteger(item.index) ? Math.max(maxIndex, item.index) : maxIndex,
+    -1,
+  ) + 1;
+
+export const normalizeEditorRows = (rows) => {
+  const usedIndexes = new Set();
+  let nextIndex = getNextEditorRowIndex(rows);
+
+  return rows.map((item) => {
+    if (Number.isInteger(item.index) && !usedIndexes.has(item.index)) {
+      usedIndexes.add(item.index);
+      return item;
+    }
+
+    while (usedIndexes.has(nextIndex)) {
+      nextIndex += 1;
+    }
+    const normalized = { ...item, index: nextIndex };
+    usedIndexes.add(nextIndex);
+    nextIndex += 1;
+    return normalized;
+  });
+};
+
 function HeadersEditor(props) {
   const { value: propsValue, onChange, buttonText } = props;
   const jsonObj = JSON.parse(propsValue || "[]");
-  const [value, setValue] = useState(jsonObj);
+  const [value, setValue] = useState(() => normalizeEditorRows(jsonObj));
 
   useEffect(() => {
-    setValue(jsonObj);
+    setValue(normalizeEditorRows(jsonObj));
   }, [propsValue]);
 
   const onChangeItem = (e, key, index) => {
@@ -37,12 +64,15 @@ function HeadersEditor(props) {
     );
   };
 
-  const onDeleteItem = (key) => {
-    changeValue(value.filter((item) => item.key !== key));
+  const onDeleteItem = (index) => {
+    changeValue(value.filter((item) => item.index !== index));
   };
 
   const onAddItem = () => {
-    changeValue([...value, { index: value.length, key: "", value: "" }]);
+    changeValue([
+      ...value,
+      { index: getNextEditorRowIndex(value), key: "", value: "" },
+    ]);
   };
 
   const changeValue = (newValue) => {
@@ -78,7 +108,7 @@ function HeadersEditor(props) {
                 <Icon
                   style={{ fontSize: "16px" }}
                   type="minus-circle-o"
-                  onClick={() => onDeleteItem(item.key)}
+                  onClick={() => onDeleteItem(item.index)}
                 />
               </Text>
             )}
diff --git a/src/routes/Document/components/HeadersEditor.test.js 
b/src/routes/Document/components/HeadersEditor.test.js
new file mode 100644
index 00000000..89c48dda
--- /dev/null
+++ b/src/routes/Document/components/HeadersEditor.test.js
@@ -0,0 +1,79 @@
+/*
+ * 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 React from "react";
+import { fireEvent, render } from "@testing-library/react";
+import HeadersEditor, {
+  getNextEditorRowIndex,
+  normalizeEditorRows,
+} from "./HeadersEditor";
+
+it("assigns unique stable indexes to loaded rows", () => {
+  expect(
+    normalizeEditorRows([
+      { index: 2, key: "A", value: "1" },
+      { index: 2, key: "B", value: "2" },
+      { key: "C", value: "3" },
+    ]).map((item) => item.index),
+  ).toEqual([2, 3, 4]);
+});
+
+it("uses the highest existing index when adding after a deletion", () => {
+  expect(getNextEditorRowIndex([{ index: 0 }, { index: 2 }])).toBe(3);
+});
+
+it("deletes only the clicked row when header names are duplicated", () => {
+  const onChange = jest.fn();
+  const { container } = render(
+    <HeadersEditor
+      value={JSON.stringify([
+        { index: 0, key: "", value: "first" },
+        { index: 1, key: "", value: "second" },
+      ])}
+      onChange={onChange}
+      buttonText="Add"
+    />,
+  );
+
+  fireEvent.click(container.querySelectorAll(".anticon-minus-circle-o")[0]);
+
+  expect(JSON.parse(onChange.mock.calls[0][0])).toEqual([
+    { index: 1, key: "", value: "second" },
+  ]);
+});
+
+it("adds a row without reusing a surviving index", () => {
+  const onChange = jest.fn();
+  const { getByText } = render(
+    <HeadersEditor
+      value={JSON.stringify([
+        { index: 0, key: "A", value: "1" },
+        { index: 2, key: "C", value: "3" },
+      ])}
+      onChange={onChange}
+      buttonText="Add"
+    />,
+  );
+
+  fireEvent.click(getByText("Add"));
+
+  expect(JSON.parse(onChange.mock.calls[0][0])).toEqual([
+    { index: 0, key: "A", value: "1" },
+    { index: 2, key: "C", value: "3" },
+    { index: 3, key: "", value: "" },
+  ]);
+});

Reply via email to