fix(sidebar): preserve objects for ancestor search matches
Closes #5613
This commit is contained in:
1 parent
cce36ae058
commit
e0a7bc996d
9 files changed
+282
-41
No files matched your search
@@ -8,7 +8,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ObjectSourceKind, TableInfo, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSidebarTreeToConnectedConnections, resolveSidebarFilterGuards, reuseLiveSidebarTreeNodes } from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { createSidebarSearchSubtreePreserver, filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSidebarTreeToConnectedConnections, resolveSidebarFilterGuards, resolveSidebarObjectSearchFilter, reuseLiveSidebarTreeNodes } from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
|
||||
import { buildTableTreeNodes } from "@/lib/table/tableTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
@@ -229,8 +229,9 @@ watch(deferredSearchQuery, (newQuery, oldQuery) => {
|
||||
return;
|
||||
}
|
||||
const tasks: Promise<void>[] = [];
|
||||
const preservesSearchSubtree = newQuery ? createSidebarSearchSubtreePreserver(newQuery, searchableNodeTypes.value) : undefined;
|
||||
for (const root of store.treeNodes) {
|
||||
collectExpandedObjectSearchTargets(root, tasks, newQuery ? searchRefreshedNodeIds : undefined);
|
||||
collectExpandedObjectSearchTargets(root, tasks, newQuery ? searchRefreshedNodeIds : undefined, preservesSearchSubtree);
|
||||
}
|
||||
if (!newQuery && oldQuery) {
|
||||
searchRefreshedNodeIds.clear();
|
||||
@@ -250,7 +251,8 @@ function isSimpleObjectSearchParent(node: TreeNode): boolean {
|
||||
return settingsStore.editorSettings.sidebarObjectDisplay === "simple" && simpleObjectParentTypes.has(node.type) && node.isExpanded === true && (!!node.children?.some((child) => simpleObjectChildTypes.has(child.type)) || !!store.sidebarTableSearchQueries[node.id]?.trim());
|
||||
}
|
||||
|
||||
function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>[], refreshedNodeIds?: Set<string>) {
|
||||
function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>[], refreshedNodeIds?: Set<string>, preservesNodeSubtree?: (node: TreeNode) => boolean, ancestorPreservesSearchSubtree = false) {
|
||||
const preservesSearchSubtree = ancestorPreservesSearchSubtree || (!!refreshedNodeIds && !!preservesNodeSubtree?.(node));
|
||||
if (refreshedNodeIds && node.type === "connection" && node.connectionId) {
|
||||
if (store.connectedIds.has(node.connectionId)) {
|
||||
tasks.push(store.loadConnectedConnectionRootForSidebarSearch(node.connectionId));
|
||||
@@ -258,15 +260,27 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>
|
||||
if (node.connectionId !== store.activeConnectionId) return;
|
||||
}
|
||||
if (refreshedNodeIds && isSimpleObjectSearchParent(node)) {
|
||||
refreshedNodeIds.add(node.id);
|
||||
tasks.push(store.refreshTreeNode(node));
|
||||
if (preservesSearchSubtree) {
|
||||
if (refreshedNodeIds.delete(node.id)) {
|
||||
tasks.push(store.loadTreeNodeChildren(node, { force: true, searchFilter: "", allowGlobalSearchMismatch: true, expectedSidebarSearchQuery: store.sidebarSearchQuery }));
|
||||
}
|
||||
} else {
|
||||
refreshedNodeIds.add(node.id);
|
||||
tasks.push(store.refreshTreeNode(node));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (refreshedNodeIds && node.isExpanded && node.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.connectionId && searchableObjectGroupTypes.has(child.type)) {
|
||||
refreshedNodeIds.add(child.id);
|
||||
tasks.push(store.loadObjectGroupChildren(child, { force: true }));
|
||||
if (preservesSearchSubtree) {
|
||||
if (refreshedNodeIds.delete(child.id)) {
|
||||
tasks.push(store.loadObjectGroupChildren(child, { force: true, searchFilter: "", allowGlobalSearchMismatch: true, expectedSidebarSearchQuery: store.sidebarSearchQuery }));
|
||||
}
|
||||
} else {
|
||||
refreshedNodeIds.add(child.id);
|
||||
tasks.push(store.loadObjectGroupChildren(child, { force: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!refreshedNodeIds && searchRefreshedNodeIds.has(node.id)) {
|
||||
@@ -278,7 +292,7 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
collectExpandedObjectSearchTargets(child, tasks, refreshedNodeIds);
|
||||
collectExpandedObjectSearchTargets(child, tasks, refreshedNodeIds, preservesNodeSubtree, preservesSearchSubtree);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,6 +952,12 @@ const pasteHandlerRegistry = createSidebarPasteHandlerRegistry();
|
||||
provide(sidebarTreeContextKey, {
|
||||
getVisibleNodes: () => selectableVisibleNodes.value,
|
||||
getVisibleNodeIndex: (id: string) => selectableVisibleNodeIndexById.value.get(id) ?? -1,
|
||||
getTreeLoadSearchOptions: (node) => {
|
||||
const query = deferredSearchQuery.value;
|
||||
if (!query) return undefined;
|
||||
const searchFilter = resolveSidebarObjectSearchFilter(store.treeNodes, node.id, query, searchableNodeTypes.value);
|
||||
return searchFilter ? undefined : { searchFilter: "", allowGlobalSearchMismatch: true, expectedSidebarSearchQuery: query };
|
||||
},
|
||||
setTableSearchQuery: (parentNodeId, query, local) => {
|
||||
const focusRestore = captureTableSearchFocus(parentNodeId);
|
||||
latestTableSearchInteractionParentId = parentNodeId;
|
||||
|
||||
@@ -566,6 +566,7 @@ async function openDirectNavigationNode(node: TreeNode) {
|
||||
|
||||
async function toggle() {
|
||||
const node = activeNode.value;
|
||||
const treeLoadSearchOptions = sidebarTreeContext?.getTreeLoadSearchOptions?.(node);
|
||||
if (isDirectNavigationTreeNode(node.type)) {
|
||||
try {
|
||||
await openDirectNavigationNode(node);
|
||||
@@ -664,7 +665,7 @@ async function toggle() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await loadSidebarObjectGroup(node, connectionStore)) {
|
||||
if (await loadSidebarObjectGroup(node, connectionStore, treeLoadSearchOptions)) {
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
@@ -697,11 +698,11 @@ async function toggle() {
|
||||
} else if (config?.db_type === "mq") {
|
||||
await connectionStore.loadMqTenants(node.connectionId);
|
||||
} else if (config?.db_type === "nacos") {
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId);
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId, treeLoadSearchOptions);
|
||||
} else if (config?.db_type === "mqtt") {
|
||||
await connectionStore.loadMqttTopics(node.connectionId);
|
||||
} else {
|
||||
await connectionStore.loadDatabases(node.connectionId);
|
||||
await connectionStore.loadDatabases(node.connectionId, treeLoadSearchOptions);
|
||||
}
|
||||
} else if (node.type === "redis-db" && node.connectionId && node.database) {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
@@ -779,30 +780,30 @@ async function toggle() {
|
||||
}
|
||||
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
if (node.catalog && node.catalog !== "internal") {
|
||||
await connectionStore.loadDorisCatalogTables(node);
|
||||
await connectionStore.loadDorisCatalogTables(node, treeLoadSearchOptions);
|
||||
} else {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
||||
if (config?.db_type === "sqlserver") {
|
||||
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
|
||||
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database, treeLoadSearchOptions);
|
||||
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
|
||||
await connectionStore.loadSchemas(node.connectionId, node.database);
|
||||
await connectionStore.loadSchemas(node.connectionId, node.database, treeLoadSearchOptions);
|
||||
} else {
|
||||
await connectionStore.loadTables(node.connectionId, node.database);
|
||||
await connectionStore.loadTables(node.connectionId, node.database, undefined, treeLoadSearchOptions);
|
||||
}
|
||||
}
|
||||
} else if (node.type === "doris-catalog" && node.connectionId) {
|
||||
await connectionStore.loadDorisCatalogDatabases(node);
|
||||
await connectionStore.loadDorisCatalogDatabases(node, treeLoadSearchOptions);
|
||||
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema, treeLoadSearchOptions);
|
||||
} else if (node.type === "linked-server-root" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServers(node.connectionId);
|
||||
await connectionStore.loadSqlServerLinkedServers(node.connectionId, treeLoadSearchOptions);
|
||||
} else if (node.type === "linked-server" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServerCatalogs(node);
|
||||
await connectionStore.loadSqlServerLinkedServerCatalogs(node, treeLoadSearchOptions);
|
||||
} else if (node.type === "linked-server-catalog" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServerSchemas(node);
|
||||
await connectionStore.loadSqlServerLinkedServerSchemas(node, treeLoadSearchOptions);
|
||||
} else if (node.type === "linked-server-schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema, treeLoadSearchOptions);
|
||||
} else if ((node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
||||
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id, node.catalog);
|
||||
} else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
|
||||
+49
-1
@@ -1,10 +1,11 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { createApp, defineComponent, h, nextTick, provide, ref, type App } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { syncSidebarTreeNodeExpansion } from "@/lib/sidebar/sidebarTreeExpansion";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import SidebarTreeRuntimeHost from "@/components/sidebar/SidebarTreeRuntimeHost.vue";
|
||||
|
||||
const connectionStore = {
|
||||
@@ -16,6 +17,9 @@ const connectionStore = {
|
||||
loadPackageMembers: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
loadObjectGroupChildren: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
@@ -44,6 +48,7 @@ afterEach(() => {
|
||||
connectionStore.treeNodes = [];
|
||||
connectionStore.sidebarSearchQuery = "";
|
||||
vi.clearAllMocks();
|
||||
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(true);
|
||||
connectionStore.getConfig.mockReturnValue({ db_type: "mysql" });
|
||||
});
|
||||
|
||||
@@ -116,4 +121,47 @@ describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
expect(packageNode.isExpanded).toBe(true);
|
||||
expect(toggled).toHaveBeenCalledWith(packageNode, true);
|
||||
});
|
||||
|
||||
it("expands an unloaded object group without leaking a matching connection name", async () => {
|
||||
const group: TreeNode = {
|
||||
id: "mysql:basic:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "mysql",
|
||||
database: "basic",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
connectionStore.sidebarSearchQuery = "60307";
|
||||
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(false);
|
||||
|
||||
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
provide(sidebarTreeContextKey, {
|
||||
getVisibleNodes: () => [group],
|
||||
getVisibleNodeIndex: () => 0,
|
||||
getTreeLoadSearchOptions: () => ({ searchFilter: "", allowGlobalSearchMismatch: true, expectedSidebarSearchQuery: "60307" }),
|
||||
});
|
||||
return () => h(SidebarTreeRuntimeHost, { ref: host, node: group, depth: 0 });
|
||||
},
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
|
||||
host.value?.toggleNode(group);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(connectionStore.loadObjectGroupChildren).toHaveBeenCalledWith(group, {
|
||||
searchFilter: "",
|
||||
allowGlobalSearchMismatch: true,
|
||||
expectedSidebarSearchQuery: "60307",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user