1 parent
90a18a7b23
commit
10d1509d92
6 files changed
+101
-14
No files matched your search
@@ -2284,7 +2284,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
|
||||
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
|
||||
:skip-hover="true"
|
||||
key-field="id"
|
||||
key-field="renderKey"
|
||||
type-field="poolType"
|
||||
list-class="connection-tree-content"
|
||||
flow-mode
|
||||
|
||||
+53
-1
@@ -4,7 +4,7 @@ import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
|
||||
function item(id: string, type: TreeNodeType, depth: number, children?: TreeNode[]): FlatTreeNode {
|
||||
const node: TreeNode = { id, label: id, type, children };
|
||||
return { id, type, depth, node, poolType: type };
|
||||
return { id, renderKey: `test:${depth}:${type}:${id}`, type, depth, node, poolType: type };
|
||||
}
|
||||
|
||||
function createIndex(nodes: FlatTreeNode[]) {
|
||||
@@ -90,16 +90,26 @@ describe("flat-tree range mutations", () => {
|
||||
const connection: TreeNode = { id: "connection", label: "connection", type: "connection", isExpanded: true, children: [database] };
|
||||
const sibling: TreeNode = { id: "sibling", label: "sibling", type: "connection" };
|
||||
const initial = flattenTree([connection, sibling]);
|
||||
const connectionRenderKey = initial[0].renderKey;
|
||||
const databaseRenderKey = initial[1].renderKey;
|
||||
const siblingRenderKey = initial[2].renderKey;
|
||||
|
||||
const expandedDatabase = mutateFlatTreeExpansion(initial, 1, database, true);
|
||||
expect(expandedDatabase.map((entry) => entry.id)).toEqual(["connection", "database", "schema", "sibling"]);
|
||||
expect(expandedDatabase[0].renderKey).toBe(connectionRenderKey);
|
||||
expect(expandedDatabase[1].renderKey).toBe(databaseRenderKey);
|
||||
expect(expandedDatabase.at(-1)?.renderKey).toBe(siblingRenderKey);
|
||||
expect(expandedDatabase.at(-1)?.node).toBe(sibling);
|
||||
|
||||
const expandedSchema = mutateFlatTreeExpansion(expandedDatabase, 2, schema, true);
|
||||
expect(expandedSchema.map((entry) => entry.id)).toEqual(["connection", "database", "schema", "table", "sibling"]);
|
||||
expect(expandedSchema[0].renderKey).toBe(connectionRenderKey);
|
||||
expect(expandedSchema[1].renderKey).toBe(databaseRenderKey);
|
||||
expect(expandedSchema.at(-1)?.renderKey).toBe(siblingRenderKey);
|
||||
|
||||
const collapsedDatabase = mutateFlatTreeExpansion(expandedSchema, 1, database, false);
|
||||
expect(collapsedDatabase.map((entry) => entry.id)).toEqual(["connection", "database", "sibling"]);
|
||||
expect(collapsedDatabase.map((entry) => entry.renderKey)).toEqual([connectionRenderKey, databaseRenderKey, siblingRenderKey]);
|
||||
expect(collapsedDatabase[2].node).toBe(sibling);
|
||||
});
|
||||
|
||||
@@ -115,6 +125,10 @@ describe("flat-tree range mutations", () => {
|
||||
const refreshed = replaceFlatTreeChildren(initial, 1, database);
|
||||
|
||||
expect(refreshed.map((entry) => entry.id)).toEqual(["connection", "database", "new-table", "sibling"]);
|
||||
expect(refreshed[0].renderKey).toBe(initial[0].renderKey);
|
||||
expect(refreshed[1].renderKey).toBe(initial[1].renderKey);
|
||||
expect(refreshed[3].renderKey).toBe(initial[3].renderKey);
|
||||
expect(refreshed[2].renderKey).not.toBe(initial[2].renderKey);
|
||||
expect(refreshed[0]).toBe(initial[0]);
|
||||
expect(refreshed[3]).toBe(initial[3]);
|
||||
});
|
||||
@@ -129,3 +143,41 @@ describe("flat-tree range mutations", () => {
|
||||
expect(result).not.toBe(nodes);
|
||||
});
|
||||
});
|
||||
|
||||
describe("flat-tree render identities", () => {
|
||||
it("isolates equal business ids that belong to different lineages", () => {
|
||||
const collidingId = "connection:a:b";
|
||||
const collidingSchemaId = "connection:a:b:c";
|
||||
const directDatabaseSchema: TreeNode = { id: collidingSchemaId, label: "c", type: "schema" };
|
||||
const directDatabase: TreeNode = { id: collidingId, label: "a:b", type: "database", isExpanded: true, children: [directDatabaseSchema] };
|
||||
const schema: TreeNode = { id: collidingId, label: "b", type: "schema" };
|
||||
const sameTypeSchema: TreeNode = { id: collidingSchemaId, label: "b:c", type: "schema" };
|
||||
const parentDatabase: TreeNode = { id: "connection:a", label: "a", type: "database", isExpanded: true, children: [schema, sameTypeSchema] };
|
||||
const connection: TreeNode = { id: "connection", label: "connection", type: "connection", isExpanded: true, children: [directDatabase, parentDatabase] };
|
||||
|
||||
const flattened = flattenTree([connection]);
|
||||
const collisions = flattened.filter((entry) => entry.id === collidingId);
|
||||
const sameTypeCollisions = flattened.filter((entry) => entry.id === collidingSchemaId);
|
||||
|
||||
expect(collisions.map((entry) => entry.type)).toEqual(["database", "schema"]);
|
||||
expect(new Set(collisions.map((entry) => entry.renderKey))).toHaveLength(2);
|
||||
expect(sameTypeCollisions.map((entry) => entry.type)).toEqual(["schema", "schema"]);
|
||||
expect(new Set(sameTypeCollisions.map((entry) => entry.renderKey))).toHaveLength(2);
|
||||
expect(new Set(flattened.map((entry) => entry.renderKey))).toHaveLength(flattened.length);
|
||||
});
|
||||
|
||||
it("keeps unique and default database business identities unchanged", () => {
|
||||
const defaultDatabase: TreeNode = { id: "connection:", label: "Default", type: "database" };
|
||||
const namedDatabase: TreeNode = { id: "connection:app", label: "app", type: "database" };
|
||||
const connection: TreeNode = { id: "connection", label: "connection", type: "connection", isExpanded: true, children: [defaultDatabase, namedDatabase] };
|
||||
|
||||
const flattened = flattenTree([connection]);
|
||||
|
||||
expect(flattened.map(({ id, type, poolType }) => ({ id, type, poolType }))).toEqual([
|
||||
{ id: "connection", type: "connection", poolType: "connection" },
|
||||
{ id: "connection:", type: "database", poolType: "database" },
|
||||
{ id: "connection:app", type: "database", poolType: "database" },
|
||||
]);
|
||||
expect(new Set(flattened.map((entry) => entry.renderKey))).toHaveLength(flattened.length);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export interface FlatTreeNode {
|
||||
node: TreeNode;
|
||||
depth: number;
|
||||
id: string;
|
||||
renderKey: string;
|
||||
type: TreeNodeType;
|
||||
poolType: string;
|
||||
}
|
||||
@@ -31,26 +32,35 @@ interface FlatTreeIndexOptions {
|
||||
isSchemaContainer: (type: TreeNodeType) => boolean;
|
||||
}
|
||||
|
||||
function walk(children: TreeNode[], depth: number, result: FlatTreeNode[]) {
|
||||
export function appendFlatTreeRenderKey(parentRenderKey: string, node: Pick<TreeNode, "id" | "type">): string {
|
||||
return `${parentRenderKey}${node.type.length}:${node.type}${node.id.length}:${node.id}`;
|
||||
}
|
||||
|
||||
function walk(children: TreeNode[], depth: number, result: FlatTreeNode[], parentRenderKey: string) {
|
||||
for (const node of children) {
|
||||
// Business ids are delimiter-composed and can collide across different
|
||||
// database/schema paths, while the recycler requires a unique row key.
|
||||
const renderKey = appendFlatTreeRenderKey(parentRenderKey, node);
|
||||
result.push({
|
||||
node,
|
||||
depth,
|
||||
id: node.id,
|
||||
renderKey,
|
||||
type: node.type,
|
||||
poolType: node.type === "connection-group" ? `${node.type}:${node.id}` : node.type,
|
||||
});
|
||||
if (node.isExpanded && node.children) {
|
||||
walk(node.children, depth + 1, result);
|
||||
walk(node.children, depth + 1, result, renderKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flatTreeNode(node: TreeNode, depth: number): FlatTreeNode {
|
||||
function flatTreeNode(node: TreeNode, depth: number, renderKey: string): FlatTreeNode {
|
||||
return {
|
||||
node,
|
||||
depth,
|
||||
id: node.id,
|
||||
renderKey,
|
||||
type: node.type,
|
||||
poolType: node.type === "connection-group" ? `${node.type}:${node.id}` : node.type,
|
||||
};
|
||||
@@ -66,20 +76,21 @@ function visibleDescendantEnd(nodes: readonly FlatTreeNode[], parentIndex: numbe
|
||||
|
||||
export function flattenTree(nodes: TreeNode[]): FlatTreeNode[] {
|
||||
const result: FlatTreeNode[] = [];
|
||||
walk(nodes, 0, result);
|
||||
walk(nodes, 0, result, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
export function replaceFlatTreeChildren(nodes: readonly FlatTreeNode[], parentIndex: number, parent: TreeNode): FlatTreeNode[] {
|
||||
if (parentIndex < 0 || parentIndex >= nodes.length || nodes[parentIndex].id !== parent.id) return [...nodes];
|
||||
|
||||
const parentItem = nodes[parentIndex];
|
||||
const end = visibleDescendantEnd(nodes, parentIndex);
|
||||
const replacement: FlatTreeNode[] = [];
|
||||
if (parent.isExpanded && parent.children) walk(parent.children, nodes[parentIndex].depth + 1, replacement);
|
||||
if (parent.isExpanded && parent.children) walk(parent.children, parentItem.depth + 1, replacement, parentItem.renderKey);
|
||||
|
||||
// One splice-shaped replacement keeps recycled-list consumers from observing
|
||||
// an intermediate state where old and new child ranges coexist.
|
||||
return [...nodes.slice(0, parentIndex), flatTreeNode(parent, nodes[parentIndex].depth), ...replacement, ...nodes.slice(end)];
|
||||
return [...nodes.slice(0, parentIndex), flatTreeNode(parent, parentItem.depth, parentItem.renderKey), ...replacement, ...nodes.slice(end)];
|
||||
}
|
||||
|
||||
export function mutateFlatTreeExpansion(nodes: readonly FlatTreeNode[], parentIndex: number, parent: TreeNode, expanded: boolean): FlatTreeNode[] {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { appendFlatTreeRenderKey, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
|
||||
const simpleObjectParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema"]);
|
||||
@@ -59,6 +59,7 @@ export function insertSidebarTableSearchControls(
|
||||
node,
|
||||
depth: item.depth + 1,
|
||||
id: node.id,
|
||||
renderKey: appendFlatTreeRenderKey(item.renderKey, node),
|
||||
type: node.type,
|
||||
poolType: `${node.type}:${node.id}`,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user