feat(sidebar): locate query table from cursor
This commit is contained in:
1 parent
2ef29c004a
commit
2ec8ed1523
5 files changed
+385
-24
No files matched your search
@@ -5,12 +5,13 @@ import { Search, X, ListFilter, Crosshair, Server, Database, FolderTree, Table2,
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { QueryTab, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebarSearchTree";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport";
|
||||
import { connectionUsesDatabaseObjectTreeMode } from "@/lib/jdbcDialect";
|
||||
import { findSidebarNodeForActiveTab, findNodePathForActiveTab, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection } from "@/lib/sidebarActiveTabTarget";
|
||||
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget } from "@/lib/sidebarActiveTabTarget";
|
||||
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/queryCursorTableTarget";
|
||||
import { SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, scrollTopForExpandedTreeNode, shouldAutoScrollExpandedTreeNode, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
|
||||
import TreeItem from "./TreeItem.vue";
|
||||
@@ -211,8 +212,14 @@ async function locateActiveTabInSidebar() {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the tree is loaded deep enough to contain the target node
|
||||
await ensureTreeLoadedForTab(tab);
|
||||
const config = connId ? store.getConfig(connId) : undefined;
|
||||
const cursorCandidate = queryCursorTableCandidate(tab, effectiveDatabaseTypeForConnection(config));
|
||||
const fallbackTarget = queryContextTargetFromCandidate(tab, cursorCandidate) ?? activeTabSidebarTarget(tab);
|
||||
const initialTarget = cursorCandidate ? tableTargetFromCandidate(cursorCandidate) : fallbackTarget;
|
||||
if (!initialTarget) return;
|
||||
|
||||
// Ensure the tree is loaded deep enough to contain the preferred target.
|
||||
await ensureTreeLoadedForTarget(initialTarget);
|
||||
|
||||
// Clear any active search filter so the node is visible
|
||||
if (isFiltering.value) {
|
||||
@@ -221,15 +228,24 @@ async function locateActiveTabInSidebar() {
|
||||
clearSearchScopeFilter();
|
||||
}
|
||||
|
||||
let nodePath = findNodePathForActiveTab(tab, store.treeNodes);
|
||||
let target = resolveLoadedLocateTarget(initialTarget, cursorCandidate);
|
||||
let nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null;
|
||||
if (!nodePath) {
|
||||
// The first load may have served a stale schema cache whose async refresh
|
||||
// replaced the database node before its tables finished loading, so the
|
||||
// table isn't in the tree yet. Force a synchronous reload and retry once so
|
||||
// locate reaches the table, not just the database (issue #715).
|
||||
await ensureTreeLoadedForTab(tab, { force: true });
|
||||
nodePath = findNodePathForActiveTab(tab, store.treeNodes);
|
||||
await ensureTreeLoadedForTarget(initialTarget, { force: true });
|
||||
target = resolveLoadedLocateTarget(initialTarget, cursorCandidate);
|
||||
nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null;
|
||||
}
|
||||
|
||||
if (!nodePath && cursorCandidate && fallbackTarget) {
|
||||
await ensureTreeLoadedForTarget(fallbackTarget);
|
||||
target = fallbackTarget;
|
||||
nodePath = findNodePathForTarget(fallbackTarget, store.treeNodes);
|
||||
}
|
||||
|
||||
if (!nodePath) return;
|
||||
|
||||
for (const ancestor of nodePath) {
|
||||
@@ -240,7 +256,7 @@ async function locateActiveTabInSidebar() {
|
||||
|
||||
await nextTick();
|
||||
|
||||
const match = findSidebarNodeForActiveTab(tab, flatNodes.value);
|
||||
const match = target ? findSidebarNodeForTarget(target, flatNodes.value) : null;
|
||||
if (!match) return;
|
||||
|
||||
store.selectedTreeNodeId = match.id;
|
||||
@@ -255,8 +271,24 @@ async function locateActiveTabInSidebar() {
|
||||
await scrollToSidebarNode(match.id);
|
||||
}
|
||||
|
||||
async function ensureTreeLoadedForTab(tab: QueryTab, opts?: { force?: boolean }) {
|
||||
const connId = tab.connectionId;
|
||||
function tableTargetFromCandidate(candidate: QueryCursorTableCandidate): ActiveTabSidebarTarget {
|
||||
return {
|
||||
type: "table",
|
||||
connectionId: candidate.connectionId,
|
||||
database: candidate.database,
|
||||
schema: candidate.schema,
|
||||
tableName: candidate.tableName,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLoadedLocateTarget(target: ActiveTabSidebarTarget, candidate: QueryCursorTableCandidate | null): ActiveTabSidebarTarget | null {
|
||||
if (!candidate) return target;
|
||||
return findLoadedTableTargetForCandidate(store.treeNodes, candidate);
|
||||
}
|
||||
|
||||
async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?: { force?: boolean }) {
|
||||
if (target.type === "saved-sql-file" || target.type === "etcd-root") return;
|
||||
const connId = target.connectionId;
|
||||
if (!connId) return;
|
||||
|
||||
const config = store.getConfig(connId);
|
||||
@@ -287,37 +319,73 @@ async function ensureTreeLoadedForTab(tab: QueryTab, opts?: { force?: boolean })
|
||||
}
|
||||
}
|
||||
|
||||
if (!tab.database) return;
|
||||
if (!("database" in target) || !target.database) return;
|
||||
|
||||
// Find the database node
|
||||
const dbNode = findDatabaseNode(store.treeNodes, connId, tab.database);
|
||||
const dbNode = findDatabaseNode(store.treeNodes, connId, target.database);
|
||||
if (!dbNode) return;
|
||||
if (!force && dbNode.children && dbNode.children.length > 0) return;
|
||||
const targetSchema = "schema" in target ? target.schema : undefined;
|
||||
const databaseChildrenLoaded = !!dbNode.children && dbNode.children.length > 0;
|
||||
const shouldLoadSchemaTables = target.type === "table" && !!targetSchema && usesTreeSchemaMode(config.db_type) && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
if (!force && databaseChildrenLoaded && !shouldLoadSchemaTables) return;
|
||||
|
||||
// Load database contents
|
||||
try {
|
||||
if (config.db_type === "sqlserver") {
|
||||
await store.loadSqlServerDatabaseObjects(connId, tab.database, loadOptions);
|
||||
if (force || !databaseChildrenLoaded) {
|
||||
await store.loadSqlServerDatabaseObjects(connId, target.database, loadOptions);
|
||||
}
|
||||
} else if (usesTreeSchemaMode(config.db_type) && !connectionUsesDatabaseObjectTreeMode(config)) {
|
||||
await store.loadSchemas(connId, tab.database, loadOptions);
|
||||
if (force || !databaseChildrenLoaded) {
|
||||
await store.loadSchemas(connId, target.database, loadOptions);
|
||||
}
|
||||
// If we have a schema, also load tables under that schema
|
||||
if (tab.schema) {
|
||||
const schemaNode = findSchemaNode(store.treeNodes, connId, tab.database, tab.schema);
|
||||
if (targetSchema) {
|
||||
const schemaNode = findSchemaNode(store.treeNodes, connId, target.database, targetSchema);
|
||||
if (schemaNode && (force || !schemaNode.children || schemaNode.children.length === 0)) {
|
||||
await store.loadTables(connId, tab.database, tab.schema, loadOptions);
|
||||
await store.loadTables(connId, target.database, targetSchema, loadOptions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await store.loadTables(connId, tab.database, undefined, loadOptions);
|
||||
await store.loadTables(connId, target.database, undefined, loadOptions);
|
||||
}
|
||||
|
||||
if (target.type === "table") {
|
||||
await ensureTableObjectGroupsLoaded(target, loadOptions);
|
||||
}
|
||||
} catch {
|
||||
// Node just won't have children loaded
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTableObjectGroupsLoaded(target: Extract<ActiveTabSidebarTarget, { type: "table" }>, options?: { force?: boolean }) {
|
||||
const groups = findTableObjectGroupNodes(store.treeNodes, target);
|
||||
for (const group of groups) {
|
||||
if (!options?.force && group.children && group.children.length > 0) continue;
|
||||
await store.loadObjectGroupChildren(group, options);
|
||||
}
|
||||
}
|
||||
|
||||
function findTableObjectGroupNodes(nodes: TreeNode[], target: Extract<ActiveTabSidebarTarget, { type: "table" }>): TreeNode[] {
|
||||
const matches: TreeNode[] = [];
|
||||
for (const node of nodes) {
|
||||
if ((node.type === "group-tables" || node.type === "group-views") && node.connectionId === target.connectionId && sameTreeName(node.database, target.database) && (!target.schema || sameTreeName(node.schema, target.schema))) {
|
||||
matches.push(node);
|
||||
}
|
||||
if (node.children) {
|
||||
matches.push(...findTableObjectGroupNodes(node.children, target));
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function sameTreeName(left: string | undefined, right: string | undefined): boolean {
|
||||
return (left || "").toLowerCase() === (right || "").toLowerCase();
|
||||
}
|
||||
|
||||
function findDatabaseNode(nodes: TreeNode[], connId: string, database: string): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "database" && node.connectionId === connId && node.database === database) {
|
||||
if (node.type === "database" && node.connectionId === connId && sameTreeName(node.database, database)) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
@@ -330,7 +398,7 @@ function findDatabaseNode(nodes: TreeNode[], connId: string, database: string):
|
||||
|
||||
function findSchemaNode(nodes: TreeNode[], connId: string, database: string, schema: string): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "schema" && node.connectionId === connId && node.database === database && node.label === schema) {
|
||||
if (node.type === "schema" && node.connectionId === connId && sameTreeName(node.database, database) && sameTreeName(node.label, schema)) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
|
||||
@@ -3382,12 +3382,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
.tree-item-highlight {
|
||||
background-color: rgb(253 225 167) !important;
|
||||
background-color: oklch(0.92 0.08 85) !important;
|
||||
transition: background-color 0.8s ease-out 0.6s;
|
||||
transition: background-color 0.28s ease-out;
|
||||
}
|
||||
|
||||
:root.dark .tree-item-highlight {
|
||||
background-color: rgb(110 67 0) !important;
|
||||
background-color: oklch(0.42 0.12 80) !important;
|
||||
transition: background-color 0.8s ease-out 0.6s;
|
||||
transition: background-color 0.28s ease-out;
|
||||
}
|
||||
|
||||
.tree-item-connection-tint.tree-item-highlight::before {
|
||||
background-color: rgb(253 225 167) !important;
|
||||
background-color: oklch(0.92 0.08 85) !important;
|
||||
}
|
||||
|
||||
:root.dark .tree-item-connection-tint.tree-item-highlight::before {
|
||||
background-color: rgb(110 67 0) !important;
|
||||
background-color: oklch(0.42 0.12 80) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user