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 fd98ef24 fix: keep app auth path row identities stable (#678)
fd98ef24 is described below

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

    fix: keep app auth path row identities stable (#678)
    
    Co-authored-by: Aeontra <aeontra@localhost>
---
 src/routes/System/AppAuth/AddTable.js      |  82 +++++++++++++---------
 src/routes/System/AppAuth/AddTable.test.js | 106 +++++++++++++++++++++++++++++
 2 files changed, 156 insertions(+), 32 deletions(-)

diff --git a/src/routes/System/AppAuth/AddTable.js 
b/src/routes/System/AppAuth/AddTable.js
index 9a8166d1..0b88f00e 100644
--- a/src/routes/System/AppAuth/AddTable.js
+++ b/src/routes/System/AppAuth/AddTable.js
@@ -32,7 +32,34 @@ import { getIntlContent } from "../../../utils/IntlUtils";
 const FormItem = Form.Item;
 const { Option } = Select;
 
-class AddTable extends Component {
+export const getNextPathRowKey = (rows) =>
+  rows.reduce(
+    (maxKey, item) =>
+      Number.isInteger(item.key) ? Math.max(maxKey, item.key) : maxKey,
+    -1,
+  ) + 1;
+
+export const normalizePathRows = (rows) => {
+  const usedKeys = new Set();
+  let nextKey = getNextPathRowKey(rows);
+
+  return rows.map((item) => {
+    if (Number.isInteger(item.key) && !usedKeys.has(item.key)) {
+      usedKeys.add(item.key);
+      return item;
+    }
+
+    while (usedKeys.has(nextKey)) {
+      nextKey += 1;
+    }
+    const normalized = { ...item, key: nextKey };
+    usedKeys.add(nextKey);
+    nextKey += 1;
+    return normalized;
+  });
+};
+
+export class AddTableComponent extends Component {
   constructor(props) {
     super(props);
     this.columns = [
@@ -69,7 +96,7 @@ class AddTable extends Component {
           this.state.allData.length > 1 ? (
             <Popconfirm
               title={getIntlContent("SHENYU.COMMON.DELETE")}
-              onConfirm={() => this.handleDelete(record.path)}
+              onConfirm={() => this.handleDelete(record.key)}
             >
               <a>{getIntlContent("SHENYU.COMMON.DELETE.NAME")}</a>
             </Popconfirm>
@@ -77,7 +104,6 @@ class AddTable extends Component {
       },
     ];
     this.state = {
-      tableInput: [],
       allData: [],
       newSelectInput: [],
       pathTableVisible: true,
@@ -88,9 +114,9 @@ class AddTable extends Component {
   handleChange = (value) => {
     this.props.form.setFieldsValue({ appName: value });
     this.setState({
-      allData:
+      allData: normalizePathRows(
         this.props.metaGroup[value] === undefined ||
-        this.props.metaGroup[value] === null
+          this.props.metaGroup[value] === null
           ? [
               {
                 key: 0,
@@ -99,6 +125,7 @@ class AddTable extends Component {
               },
             ]
           : this.props.metaGroup[value],
+      ),
     });
   };
 
@@ -127,38 +154,29 @@ class AddTable extends Component {
   };
 
   handleTableInput = (value, record) => {
-    // eslint-disable-next-line no-unused-vars
-    for (let i in value) {
-      if (Object.prototype.hasOwnProperty.call(value, i)) {
-        record[i] = value[i]; // 这一句是必须的,不然状态无法更改
-        this.setState({
-          // eslint-disable-next-line react/no-access-state-in-setstate
-          tableInput: this.state.tableInput.map((item) =>
-            item.key === record.key ? { ...item, [i]: value[i] } : item,
-          ),
-        });
-      }
-    }
+    this.setState((prev) => ({
+      allData: prev.allData.map((item) =>
+        item.key === record.key ? { ...item, ...value } : item,
+      ),
+    }));
   };
 
   handleAddTd = () => {
-    const allData = this.state.allData;
-
-    const newData = {
-      key: allData.length,
-      path: "",
-      pathDesc: "",
-    };
-
-    this.setState({
-      allData: [...allData, newData],
-    });
+    this.setState((prev) => ({
+      allData: [
+        ...prev.allData,
+        {
+          key: getNextPathRowKey(prev.allData),
+          path: "",
+          pathDesc: "",
+        },
+      ],
+    }));
   };
 
   handleDelete = (key) => {
-    // const allData = [...this.state.allData];
     this.setState((prev) => ({
-      allData: prev.allData.filter((item) => item.path !== key),
+      allData: prev.allData.filter((item) => item.key !== key),
     }));
   };
 
@@ -334,7 +352,7 @@ class AddTable extends Component {
                 columns={columns}
                 dataSource={data}
                 onChange={this.handleTableChange}
-                rowKey={(record) => record.id}
+                rowKey={(record) => record.key}
                 pagination={{ current: this.state.pagination, pageSize: 10 }}
               />
             </div>
@@ -345,4 +363,4 @@ class AddTable extends Component {
   }
 }
 
-export default Form.create()(AddTable);
+export default Form.create()(AddTableComponent);
diff --git a/src/routes/System/AppAuth/AddTable.test.js 
b/src/routes/System/AppAuth/AddTable.test.js
new file mode 100644
index 00000000..bb1e97d8
--- /dev/null
+++ b/src/routes/System/AppAuth/AddTable.test.js
@@ -0,0 +1,106 @@
+/*
+ * 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 {
+  AddTableComponent,
+  getNextPathRowKey,
+  normalizePathRows,
+} from "./AddTable";
+
+jest.mock("../../../utils/IntlUtils", () => ({
+  getIntlContent: (key) => key,
+}));
+
+const makeComponent = (props = {}) => {
+  const component = new AddTableComponent({
+    form: { setFieldsValue: jest.fn() },
+    metaGroup: {},
+    ...props,
+  });
+  component.setState = (update) => {
+    const next =
+      typeof update === "function"
+        ? update(component.state, component.props)
+        : update;
+    component.state = { ...component.state, ...next };
+  };
+  return component;
+};
+
+it("assigns unique client keys to loaded rows", () => {
+  expect(
+    normalizePathRows([
+      { key: 4, path: "/a" },
+      { key: 4, path: "/b" },
+      { id: "server-id", path: "/c" },
+    ]).map((item) => item.key),
+  ).toEqual([4, 5, 6]);
+});
+
+it("deletes only the row with the matching client key", () => {
+  const component = makeComponent();
+  component.state = {
+    ...component.state,
+    allData: [
+      { key: 0, path: "" },
+      { key: 1, path: "" },
+    ],
+  };
+
+  component.handleDelete(0);
+
+  expect(component.state.allData).toEqual([{ key: 1, path: "" }]);
+});
+
+it("does not reuse a surviving key after deletion", () => {
+  expect(getNextPathRowKey([{ key: 0 }, { key: 2 }])).toBe(3);
+
+  const component = makeComponent();
+  component.state = {
+    ...component.state,
+    allData: [
+      { key: 0, path: "/a" },
+      { key: 2, path: "/c" },
+    ],
+  };
+
+  component.handleAddTd();
+
+  expect(component.state.allData[2]).toEqual({
+    key: 3,
+    path: "",
+    pathDesc: "",
+  });
+});
+
+it("updates only the row with the matching client key", () => {
+  const component = makeComponent();
+  component.state = {
+    ...component.state,
+    allData: [
+      { key: 0, path: "/same" },
+      { key: 1, path: "/same" },
+    ],
+  };
+
+  component.handleTableInput({ path: "/changed" }, { key: 1 });
+
+  expect(component.state.allData).toEqual([
+    { key: 0, path: "/same" },
+    { key: 1, path: "/changed" },
+  ]);
+});

Reply via email to