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