feat(query): choose new query context

This commit is contained in:
t8y2 2026-05-21 19:16:00 +08:00
parent 9d7ca42acf
commit f5af9da7c7
3 changed files with 203 additions and 8 deletions

View File

@ -29,6 +29,7 @@ import "@/i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { findTreeNodeById, resolveNewQueryTarget } from "@/lib/newQueryContext";
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { isTauriRuntime } from "@/lib/tauriRuntime";
@ -114,6 +115,7 @@ const selectedSql = ref("");
const cursorPos = ref(0);
const formatSqlRequestId = ref(0);
const activeOutputView = ref<"result" | "explain" | "chart">("result");
const newQueryContextSource = ref<"tab" | "sidebar">("tab");
const showSaveSqlDialog = ref(false);
const saveSqlName = ref("");
const saveSqlFolderId = ref("");
@ -206,6 +208,7 @@ const saveSqlFolders = computed(() => {
watch(
() => queryStore.activeTabId,
(id) => {
if (id) newQueryContextSource.value = "tab";
selectedSql.value = "";
activeOutputView.value = "result";
showDriverStore.value = false;
@ -213,6 +216,13 @@ watch(
},
);
watch(
() => connectionStore.selectedTreeNodeId,
(id) => {
if (id) newQueryContextSource.value = "sidebar";
},
);
function toggleAiPanel() {
showAiPanel.value = !showAiPanel.value;
localStorage.setItem("dbx-ai-panel-open", String(showAiPanel.value));
@ -435,16 +445,24 @@ function setConnectionDialogOpen(value: boolean) {
}
async function newQuery() {
const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id;
if (!connId) return;
const conn = connectionStore.getConfig(connId);
const target = resolveNewQueryTarget({
activeTab: activeTab.value,
selectedTreeNode: findTreeNodeById(connectionStore.treeNodes, connectionStore.selectedTreeNodeId),
activeConnectionId: connectionStore.activeConnectionId,
connections: connectionStore.connections,
preferredSource: newQueryContextSource.value,
});
if (!target) return;
const conn = connectionStore.getConfig(target.connectionId);
if (!conn) return;
connectionStore.activeConnectionId = connId;
const tabId = queryStore.createTab(conn.id, resolveDefaultDatabase(conn, []));
connectionStore.activeConnectionId = target.connectionId;
const tabId = queryStore.createTab(conn.id, target.database);
try {
await connectionStore.ensureConnected(connId);
const options = await getDatabaseOptions(connId);
queryStore.updateDatabase(tabId, resolveDefaultDatabase(conn, options));
await connectionStore.ensureConnected(target.connectionId);
if (target.shouldRefreshDefaultDatabase) {
const options = await getDatabaseOptions(target.connectionId);
queryStore.updateDatabase(tabId, resolveDefaultDatabase(conn, options));
}
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}

View File

@ -0,0 +1,64 @@
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import type { ConnectionConfig, QueryTab, TreeNode } from "@/types/database";
export interface NewQueryTarget {
connectionId: string;
database: string;
shouldRefreshDefaultDatabase: boolean;
}
export type NewQueryContextSource = "tab" | "sidebar";
interface ResolveNewQueryTargetInput {
activeTab?: Pick<QueryTab, "connectionId" | "database">;
selectedTreeNode?: Pick<TreeNode, "connectionId" | "database"> | null;
activeConnectionId?: string | null;
connections: Pick<ConnectionConfig, "id" | "database">[];
preferredSource?: NewQueryContextSource;
}
export function findTreeNodeById(nodes: TreeNode[], id: string | null | undefined): TreeNode | null {
if (!id) return null;
for (const node of nodes) {
if (node.id === id) return node;
const found = findTreeNodeById(node.children || [], id);
if (found) return found;
}
return null;
}
export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQueryTarget | null {
const primaryContext = input.preferredSource === "sidebar" ? input.selectedTreeNode || undefined : input.activeTab;
const secondaryContext = input.preferredSource === "sidebar" ? input.activeTab : input.selectedTreeNode || undefined;
const primaryTarget = targetFromContext(primaryContext, input.connections);
if (primaryTarget) return primaryTarget;
const secondaryTarget = targetFromContext(secondaryContext, input.connections);
if (secondaryTarget) return secondaryTarget;
const activeConnection = input.activeConnectionId
? input.connections.find((connection) => connection.id === input.activeConnectionId)
: undefined;
const fallbackConnection = activeConnection || input.connections[0];
return fallbackConnection
? {
connectionId: fallbackConnection.id,
database: resolveDefaultDatabase(fallbackConnection, []),
shouldRefreshDefaultDatabase: true,
}
: null;
}
function targetFromContext(
context: Pick<QueryTab | TreeNode, "connectionId" | "database"> | undefined,
connections: Pick<ConnectionConfig, "id" | "database">[],
): NewQueryTarget | null {
if (!context?.connectionId) return null;
const connection = connections.find((item) => item.id === context.connectionId);
if (!connection) return null;
const database = context.database || resolveDefaultDatabase(connection, []);
return {
connectionId: context.connectionId,
database,
shouldRefreshDefaultDatabase: !context.database,
};
}

View File

@ -0,0 +1,113 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveNewQueryTarget } from "../../apps/desktop/src/lib/newQueryContext.ts";
import type { ConnectionConfig, QueryTab, TreeNode } from "../../apps/desktop/src/types/database.ts";
function connection(id: string, database = ""): ConnectionConfig {
return {
id,
name: id,
db_type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "",
database,
};
}
function queryTab(connectionId: string, database: string, mode: QueryTab["mode"] = "data"): QueryTab {
return {
id: `${connectionId}-${database}`,
title: "users",
connectionId,
database,
sql: "",
isExecuting: false,
isCancelling: false,
isExplaining: false,
mode,
};
}
test("new query target prefers the active data tab context", () => {
const target = resolveNewQueryTarget({
activeTab: queryTab("conn-data", "analytics", "data"),
selectedTreeNode: {
id: "conn-tree:reporting",
label: "reporting",
type: "database",
connectionId: "conn-tree",
database: "reporting",
},
activeConnectionId: "conn-active",
connections: [connection("conn-active"), connection("conn-data"), connection("conn-tree")],
});
assert.deepEqual(target, {
connectionId: "conn-data",
database: "analytics",
shouldRefreshDefaultDatabase: false,
});
});
test("new query target uses the selected sidebar node when there is no active tab", () => {
const selectedTreeNode: TreeNode = {
id: "conn-tree:reporting:public:users",
label: "users",
type: "table",
connectionId: "conn-tree",
database: "reporting",
schema: "public",
};
const target = resolveNewQueryTarget({
activeTab: undefined,
selectedTreeNode,
activeConnectionId: "conn-active",
connections: [connection("conn-active"), connection("conn-tree")],
});
assert.deepEqual(target, {
connectionId: "conn-tree",
database: "reporting",
shouldRefreshDefaultDatabase: false,
});
});
test("new query target prefers the selected sidebar node after sidebar focus", () => {
const target = resolveNewQueryTarget({
activeTab: queryTab("conn-data", "analytics", "data"),
selectedTreeNode: {
id: "conn-tree:reporting:public:users",
label: "users",
type: "table",
connectionId: "conn-tree",
database: "reporting",
},
activeConnectionId: "conn-active",
connections: [connection("conn-active"), connection("conn-data"), connection("conn-tree")],
preferredSource: "sidebar",
});
assert.deepEqual(target, {
connectionId: "conn-tree",
database: "reporting",
shouldRefreshDefaultDatabase: false,
});
});
test("new query target refreshes default database for connection-only sidebar nodes", () => {
const target = resolveNewQueryTarget({
activeTab: undefined,
selectedTreeNode: { id: "conn-tree", label: "conn-tree", type: "connection", connectionId: "conn-tree" },
activeConnectionId: "conn-active",
connections: [connection("conn-active"), connection("conn-tree", "saved_default")],
});
assert.deepEqual(target, {
connectionId: "conn-tree",
database: "saved_default",
shouldRefreshDefaultDatabase: true,
});
});