mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-03 17:07:15 +00:00
fix(frontend): key the node table by the computed row key, not id
The desktop node table used rowKey="id", but transitive sub-nodes (the
read-only rows surfaced from downstream nodes) all carry id 0, so a topology
with two or more transitive rows gave React duplicate keys. antd's rowKey
prop overrides the row object's own computed `key` (`t-${guid}` for
transitive rows, the numeric id otherwise), so the unique key the code
already builds was ignored — causing row-state/DOM mis-association on any
re-render (heartbeat refetch, address-eye toggle). The mobile card path
already keyed by record.key.
Key the table by "key" so transitive rows get their distinct t-${guid}
identity; direct nodes keep key === id, so row selection (filtered to numeric
keys) is unchanged.
This commit is contained in:
@@ -650,7 +650,7 @@ export default function NodeList({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
size="middle"
|
size="middle"
|
||||||
rowKey="id"
|
rowKey="key"
|
||||||
rowSelection={dataSource.length > 1 ? {
|
rowSelection={dataSource.length > 1 ? {
|
||||||
selectedRowKeys: selectedIds,
|
selectedRowKeys: selectedIds,
|
||||||
onChange: (keys) => onSelectionChange(keys.filter((k) => typeof k === 'number') as number[]),
|
onChange: (keys) => onSelectionChange(keys.filter((k) => typeof k === 'number') as number[]),
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
import NodeList from '@/pages/nodes/NodeList';
|
||||||
|
import type { NodeRecord } from '@/schemas/node';
|
||||||
|
|
||||||
|
import { renderWithProviders } from './test-utils';
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
function sampleNodes(): NodeRecord[] {
|
||||||
|
return [
|
||||||
|
{ id: 1, name: 'parent', guid: 'p1', transitive: false, enable: true, status: 'online' },
|
||||||
|
{ id: 0, name: 'child-a', guid: 'ca', parentGuid: 'p1', transitive: true },
|
||||||
|
{ id: 0, name: 'child-b', guid: 'cb', parentGuid: 'p1', transitive: true },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('NodeList desktop table row keys', () => {
|
||||||
|
afterEach(() => vi.restoreAllMocks());
|
||||||
|
|
||||||
|
it('gives transitive sub-node rows distinct keys instead of colliding on id 0', () => {
|
||||||
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<NodeList
|
||||||
|
nodes={sampleNodes()}
|
||||||
|
isMobile={false}
|
||||||
|
selectedIds={[]}
|
||||||
|
onSelectionChange={noop}
|
||||||
|
onAdd={noop}
|
||||||
|
onMtls={noop}
|
||||||
|
onEdit={noop}
|
||||||
|
onDelete={noop}
|
||||||
|
onProbe={noop}
|
||||||
|
onToggleEnable={noop}
|
||||||
|
onUpdateNode={noop}
|
||||||
|
onUpdateSelected={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateKeyWarning = errorSpy.mock.calls.some((call) =>
|
||||||
|
call.some((arg) => typeof arg === 'string' && arg.includes('same key')),
|
||||||
|
);
|
||||||
|
expect(duplicateKeyWarning).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user