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: "" },
+ ]);
+});