feat(settings): open query page when opening connections

Closes #2677
This commit is contained in:
二丫讲梵
2026-10-06 00:33:02 +08:00
committed by GitHub
parent 55ca94bd30
commit e983f2432c
19 changed files with 204 additions and 6 deletions
@@ -876,6 +876,7 @@ function clearRememberedConnectionDatabases() {
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive);
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
const editOpenQueryOnConnectionOpen = ref(settingsStore.editorSettings.openQueryOnConnectionOpen);
const editGenerateSqlIncludeDatabaseName = ref(settingsStore.editorSettings.generateSqlIncludeDatabaseName);
const editGenerateSqlQuoteIdentifiers = ref(settingsStore.editorSettings.generateSqlQuoteIdentifiers);
const editFormatSqlOnSqlFileSave = ref(settingsStore.editorSettings.formatSqlOnSqlFileSave);
@@ -1166,6 +1167,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
dataTabReuseMode: editDataTabReuseMode.value,
openDataTabsNextToActive: editOpenDataTabsNextToActive.value,
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
openQueryOnConnectionOpen: editOpenQueryOnConnectionOpen.value,
generateSqlIncludeDatabaseName: editGenerateSqlIncludeDatabaseName.value,
generateSqlQuoteIdentifiers: editGenerateSqlQuoteIdentifiers.value,
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave.value,
@@ -1849,6 +1851,7 @@ function syncEditorSettingsDraftFromStore() {
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
editOpenQueryOnConnectionOpen.value = settingsStore.editorSettings.openQueryOnConnectionOpen;
editGenerateSqlIncludeDatabaseName.value = settingsStore.editorSettings.generateSqlIncludeDatabaseName;
editGenerateSqlQuoteIdentifiers.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers;
editFormatSqlOnSqlFileSave.value = settingsStore.editorSettings.formatSqlOnSqlFileSave;
@@ -2003,6 +2006,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
dataTabReuseMode: editDataTabReuseMode,
openDataTabsNextToActive: editOpenDataTabsNextToActive,
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect,
openQueryOnConnectionOpen: editOpenQueryOnConnectionOpen,
generateSqlIncludeDatabaseName: editGenerateSqlIncludeDatabaseName,
generateSqlQuoteIdentifiers: editGenerateSqlQuoteIdentifiers,
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave,
@@ -2525,6 +2529,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editOpenQueryOnConnectionOpen.value = DEFAULT_EDITOR_SETTINGS.openQueryOnConnectionOpen;
editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName;
editGenerateSqlQuoteIdentifiers.value = DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers;
editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave;
@@ -2725,6 +2730,7 @@ function resetAllDefaults() {
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editOpenQueryOnConnectionOpen.value = DEFAULT_EDITOR_SETTINGS.openQueryOnConnectionOpen;
editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName;
editGenerateSqlQuoteIdentifiers.value = DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers;
editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave;
@@ -8051,6 +8057,14 @@ onUnmounted(() => {
</div>
<Switch id="editor-prefill-new-query" v-model="editPrefillNewQueryWithSelect" class="mt-0.5" />
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-open-query-on-connection">{{ t("settings.openQueryOnConnectionOpen") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.openQueryOnConnectionOpenDescription") }}</p>
</div>
<Switch id="editor-open-query-on-connection" v-model="editOpenQueryOnConnectionOpen" class="mt-0.5" />
</div>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
@@ -142,7 +142,18 @@ import {
isSingleDatabase,
schemaNodeHasLoadableName,
} from "@/lib/database/databaseCapabilities";
import { copyDisplayPathForTreeNode, copyNameForTreeNode, isDirectNavigationTreeNode, isDocumentBrowserTreeNode, isRepeatableNavigationTreeNode, objectSourceTargetForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
import {
copyDisplayPathForTreeNode,
copyNameForTreeNode,
isDirectNavigationTreeNode,
isDocumentBrowserTreeNode,
isRepeatableNavigationTreeNode,
objectSourceTargetForTreeNode,
shouldOpenQueryOnTreeNodeActivation,
shouldRunTreeNodeRowAction,
treeNodeRowAction,
treeNodeRowDoubleClickAction,
} from "@/lib/sidebar/treeNodeClick";
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import { mongoCollectionTableTypeFromNode, mongoCreateDatabasePreview, mongoDropIndexFailureCount } from "@/lib/sidebar/mongoCollectionMutation";
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
@@ -1197,6 +1208,13 @@ function runRowClickAction(clickDetail: number) {
// after that follow-up click has resolved to a real action, otherwise it
// makes the first click's pending connection/expansion request look stale.
const requestId = beginNavigationRequest();
// Activation preference: single-click activation also opens (or focuses)
// the connection's query page. It must stay behind the activation gate
// above so "double" activation keeps single clicks inert, and it must not
// return early: the native row action (expand, browse objects) still runs.
if (shouldOpenQueryOnActivation(node)) {
void newQuery({ reuseQueryTabByScope: true });
}
if (action === "open-data") {
scheduleOpenData(node);
} else if (action === "locate-column") {
@@ -1615,6 +1633,14 @@ function requestDeleteSelectedNode(): boolean {
}
function onDoubleClick(event: MouseEvent) {
// In double-click activation mode the dblclick is the activation moment, so
// the query page opens here once. In single-click activation mode the first
// click of the sequence already opened it, and the trailing dblclick must
// keep its native gestures (for example the connection database browser).
if (settingsStore.editorSettings.sidebarActivation === "double" && shouldOpenQueryOnActivation(activeNode.value)) {
beginNavigationRequest();
void newQuery({ reuseQueryTabByScope: true });
}
if (dataTabOpenModeFromTreeClick(activeNode.value.type, event, settingsStore.editorSettings.shortcuts.openDataInNewTab) === "new-tab") return;
if (activeNode.value.type === "event") {
beginNavigationRequest();
@@ -2050,9 +2076,18 @@ function openDataInNewTabImmediately(node: TreeNode = activeNode.value) {
emit("open-data", node, false, "new-tab", (target, request) => openData(target, request, "new-tab"));
}
async function newQuery() {
function shouldOpenQueryOnActivation(node: TreeNode): boolean {
// The db-type gate matches the context menu's "New Query" entry, so
// specialized workbench connections keep their dedicated surfaces.
return shouldOpenQueryOnTreeNodeActivation(node, currentDatabaseType(), settingsStore.editorSettings.openQueryOnConnectionOpen);
}
async function newQuery(options: { reuseQueryTabByScope?: boolean } = {}) {
const node = activeNode.value;
if (!node.connectionId) return;
// Activation opens (or focuses) "the" query page for the connection scope;
// the context-menu entry keeps creating a fresh tab per invocation.
const reuseOptions = options.reuseQueryTabByScope === true ? [{ reuseQueryTabByScope: true }] : [];
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
@@ -2075,13 +2110,13 @@ async function newQuery() {
openSqlTemplateTab(node.connectionId, node.database, node.schema, node.catalog, sql);
return;
}
queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema, undefined, node.catalog);
queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema, undefined, node.catalog, ...reuseOptions);
return;
}
const connection = connectionStore.getConfig(node.connectionId);
if (!connection) return;
const options = await getDatabaseOptions(node.connectionId);
queryStore.createTab(node.connectionId, resolveDefaultDatabase(connection, options), undefined, "query", connection.default_schema);
const databaseOptions = await getDatabaseOptions(node.connectionId);
queryStore.createTab(node.connectionId, resolveDefaultDatabase(connection, databaseOptions), undefined, "query", connection.default_schema, undefined, undefined, ...reuseOptions);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
openDriverStoreForInstallError(e?.message || String(e));
+2
View File
@@ -8610,6 +8610,8 @@ export default {
savedSqlOpenTargetCurrent: "Current tab",
savedSqlOpenTargetCurrentDescription: "Use the active tab’s connection, database, schema, and catalog when available; otherwise use the saved target.",
prefillNewQueryWithSelect: "Prefill new query with SELECT *",
openQueryOnConnectionOpen: "Open query page when opening a connection",
openQueryOnConnectionOpenDescription: "When enabled, opening a connection or database defaults to the query page.",
prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM <table> based on the active table tab or the table selected in the sidebar.",
generateSqlIncludeDatabaseName: "Include database name in generated SQL",
generateSqlIncludeDatabaseNameDescription: "When supported by the database, generated table SQL uses the qualified name (database.schema.table on SQL Server) instead of only the table name.",
+2
View File
@@ -8601,6 +8601,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "Buscar solo en bases de datos abiertas",
sidebarSearchOpenedDatabasesOnlyDescription: "La búsqueda de la barra lateral solo carga las bases de datos abiertas en la conexión actual (si no hay ninguna abierta, busca en todas). Al desactivarla, se buscará en todas las bases de datos de esa conexión.",
shortcutSelectLineEnds: "Seleccionar finales de línea",
openQueryOnConnectionOpen: "Abrir la página de consulta al abrir la conexión",
openQueryOnConnectionOpenDescription: "Cuando está habilitado, al abrir una conexión o base de datos se accede de forma predeterminada a la página de consulta.",
},
driverStore: {
jreDirRemoveFailed: "No se pudo eliminar el directorio JRE antiguo: {path} (error original: {error})",
+2
View File
@@ -8347,6 +8347,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "Cerca solo nei database aperti",
sidebarSearchOpenedDatabasesOnlyDescription: "La ricerca nella barra laterale carica solo i database aperti nella connessione corrente (se nessuno è aperto, cerca in tutti). Se disattivata, cerca in tutti i database della connessione.",
shortcutSelectLineEnds: "Seleziona fine riga",
openQueryOnConnectionOpen: "Apri la pagina Query all'apertura della connessione",
openQueryOnConnectionOpenDescription: "Se abilitato, all'apertura di una connessione o di un database si apre per impostazione predefinita la pagina Query.",
},
driverStore: {
jreDirRemoveFailed: "Impossibile rimuovere la vecchia directory JRE: {path} (errore originale: {error})",
+2
View File
@@ -8388,6 +8388,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "開いているデータベースのみを検索",
sidebarSearchOpenedDatabasesOnlyDescription: "サイドバー検索は、現在の接続で開いているデータベースのみを読み込みます(1つも開いていない場合はすべてを検索します)。オフにすると、その接続のすべてのデータベースを検索します。",
shortcutSelectLineEnds: "行末を選択",
openQueryOnConnectionOpen: "接続を開いたときにクエリページを開く",
openQueryOnConnectionOpenDescription: "有効にすると、接続またはデータベースを開いたときにデフォルトでクエリページが開きます。",
},
driverStore: {
jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})",
+2
View File
@@ -8122,6 +8122,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "열려 있는 데이터베이스만 검색",
sidebarSearchOpenedDatabasesOnlyDescription: "사이드바 검색은 현재 연결에서 열려 있는 데이터베이스만 로드합니다(열려 있는 데이터베이스가 하나도 없으면 전체를 검색). 끄면 해당 연결의 모든 데이터베이스를 검색합니다.",
shortcutSelectLineEnds: "줄 끝 선택",
openQueryOnConnectionOpen: "연결을 열 때 쿼리 페이지로 이동",
openQueryOnConnectionOpenDescription: "활성화하면 연결 또는 데이터베이스를 열 때 기본적으로 쿼리 페이지로 이동합니다.",
},
driverStore: {
jreDirRemoveFailed: "이전 JRE 디렉터리 제거 실패: {path} (원래 오류: {error})",
+2
View File
@@ -8347,6 +8347,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "Pesquisar apenas bancos de dados abertos",
sidebarSearchOpenedDatabasesOnlyDescription: "A pesquisa da barra lateral carrega apenas os bancos de dados abertos na conexão atual (se nenhum estiver aberto, pesquisa todos). Quando desativada, pesquisará todos os bancos de dados da conexão.",
shortcutSelectLineEnds: "Selecionar fins de linha",
openQueryOnConnectionOpen: "Abrir consulta ao abrir a conexão",
openQueryOnConnectionOpenDescription: "Quando ativado, abre a página de consulta por padrão ao abrir uma conexão ou banco de dados.",
},
driverStore: {
jreDirRemoveFailed: "Não foi possível remover o diretório JRE antigo: {path} (erro original: {error})",
+2
View File
@@ -9566,6 +9566,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "Искать только в открытых базах данных",
sidebarSearchOpenedDatabasesOnlyDescription: "Поиск на боковой панели загружает только открытые базы данных в текущем подключении (если ни одна не открыта, поиск выполняется по всем). Если отключить, поиск будет выполняться по всем базам данных этого подключения.",
shortcutSelectLineEnds: "Выделить концы строк",
openQueryOnConnectionOpen: "Открывать страницу запроса при открытии подключения",
openQueryOnConnectionOpenDescription: "Если включено, при открытии подключения или базы данных по умолчанию открывается страница запроса.",
},
driverStore: {
jreDirRemoveFailed: "Не удалось удалить старый каталог JRE: {path} (исходная ошибка: {error})",
+2
View File
@@ -8593,6 +8593,8 @@ export default withEnglishFallback({
savedSqlOpenTargetCurrent: "当前标签页",
savedSqlOpenTargetCurrentDescription: "优先使用当前标签页的连接、数据库、Schema 和 Catalog;不可用时回退到保存时的目标。",
prefillNewQueryWithSelect: "新建查询时预填充 SELECT 语句",
openQueryOnConnectionOpen: "打开连接时进入查询页",
openQueryOnConnectionOpenDescription: "启用后,打开连接或数据库时默认进入查询页面。",
prefillNewQueryWithSelectDescription: "新建查询时,根据当前激活的数据表标签页或侧边栏选中的表,自动在编辑器中填充 SELECT * FROM <表名>。",
generateSqlIncludeDatabaseName: "生成 SQL 时包含数据库名",
generateSqlIncludeDatabaseNameDescription: "数据库支持时,生成的表 SQL 将使用限定名(SQL Server 为“数据库名.schema.表名”),而不只包含表名。",
+2
View File
@@ -7702,6 +7702,8 @@ export default withEnglishFallback({
sidebarSearchOpenedDatabasesOnly: "僅搜尋已開啟的資料庫",
sidebarSearchOpenedDatabasesOnlyDescription: "側邊欄搜尋只載入目前連線中已開啟的資料庫(若一個都沒開啟則搜尋全部)。關閉後會搜尋該連線的全部資料庫。",
shortcutSelectLineEnds: "選取行尾",
openQueryOnConnectionOpen: "開啟連線時進入查詢頁",
openQueryOnConnectionOpenDescription: "啟用後,開啟連線或資料庫時預設進入查詢頁面。",
},
driverStore: {
jreDirRemoveFailed: "無法刪除舊的 JRE 目錄:{path}(原始錯誤:{error})",
@@ -6,6 +6,7 @@ import {
objectSourceTargetForTreeNode,
shouldActivateTreeNodeOnSingleClick,
shouldBrowseObjectsOnDatabaseActivation,
shouldOpenQueryOnTreeNodeActivation,
shouldRunTreeNodeRowAction,
treeNodeRowAction,
treeNodeRowDoubleClickAction,
@@ -227,4 +228,24 @@ describe("treeNodeClick", () => {
expect(treeNodeRowDoubleClickAction("index", false, "single")).toBe("open-structure-editor");
expect(treeNodeRowDoubleClickAction("index", false, "double")).toBe("open-structure-editor");
});
it("opens the query page on activation only for sql-backed connection scopes", () => {
for (const type of ["connection", "database", "schema", "mongo-db"] as const) {
expect(shouldOpenQueryOnTreeNodeActivation({ type, connectionId: "c1" }, "postgres", true), type).toBe(true);
}
expect(shouldOpenQueryOnTreeNodeActivation({ type: "connection", connectionId: "c1" }, "mongodb", true)).toBe(true);
expect(shouldOpenQueryOnTreeNodeActivation({ type: "connection", connectionId: "c1" }, "postgres", false)).toBe(false);
expect(shouldOpenQueryOnTreeNodeActivation({ type: "connection" }, "postgres", true)).toBe(false);
});
it("keeps specialized workbench connections out of the query-on-activation path", () => {
for (const dbType of ["nacos", "consul", "hbase", "zookeeper", "plugin", "mq", "mqtt", "meilisearch", "salesforce"] as const) {
expect(shouldOpenQueryOnTreeNodeActivation({ type: "connection", connectionId: "c1" }, dbType, true), dbType).toBe(false);
expect(shouldOpenQueryOnTreeNodeActivation({ type: "database", connectionId: "c1" }, dbType, true), dbType).toBe(false);
}
// Object rows and connection-less nodes never hijack activation either.
expect(shouldOpenQueryOnTreeNodeActivation({ type: "table", connectionId: "c1" }, "postgres", true)).toBe(false);
expect(shouldOpenQueryOnTreeNodeActivation({ type: "nacos-namespace" }, "nacos", true)).toBe(false);
expect(shouldOpenQueryOnTreeNodeActivation({ type: "connection" }, undefined, true)).toBe(false);
});
});
@@ -114,6 +114,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"dataTabReuseMode",
"openDataTabsNextToActive",
"prefillNewQueryWithSelect",
"openQueryOnConnectionOpen",
"generateSqlIncludeDatabaseName",
"generateSqlQuoteIdentifiers",
"formatSqlOnSqlFileSave",
@@ -154,6 +154,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" },
{ id: "editor-app-close-unsaved-tabs", category: "editor", titleKey: "settings.appCloseUnsavedTabsMode", descriptionKey: "settings.appCloseUnsavedTabsModeDescription", targetId: "editor" },
{ id: "editor-prefill-query", category: "navigation", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "navigation" },
{ id: "editor-open-query-on-connection", category: "navigation", titleKey: "settings.openQueryOnConnectionOpen", descriptionKey: "settings.openQueryOnConnectionOpenDescription", targetId: "navigation" },
{ id: "editor-generate-sql-include-database", category: "editor", titleKey: "settings.generateSqlIncludeDatabaseName", descriptionKey: "settings.generateSqlIncludeDatabaseNameDescription", targetId: "editor" },
{ id: "editor-generate-sql-quote-identifiers", category: "editor", titleKey: "settings.generateSqlQuoteIdentifiers", descriptionKey: "settings.generateSqlQuoteIdentifiersDescription", targetId: "editor" },
{ id: "editor-format-sql-on-sql-file-save", category: "editor", titleKey: "settings.formatSqlOnSqlFileSave", descriptionKey: "settings.formatSqlOnSqlFileSaveDescription", targetId: "editor" },
@@ -103,6 +103,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"appCloseUnsavedTabsMode",
"savedSqlOpenTargetMode",
"prefillNewQueryWithSelect",
"openQueryOnConnectionOpen",
"generateSqlIncludeDatabaseName",
"generateSqlQuoteIdentifiers",
"formatSqlOnSqlFileSave",
@@ -1,5 +1,6 @@
import type { DatabaseType, ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database";
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import { supportsConnectionQueryActions } from "@/lib/database/databaseFeatureSupport";
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
export type TreeNodeRowAction = "open-data" | "open-source" | "open-extension-details" | "open-event-trigger-details" | "open-saved-sql" | "open-object-browser" | "open-object-browser-and-expand" | "locate-column" | "toggle" | "none";
@@ -67,6 +68,19 @@ const databaseActivationNodeTypes = new Set<TreeNodeType>(["database", "schema",
export function shouldBrowseObjectsOnDatabaseActivation(type: TreeNodeType, enabled: boolean): boolean {
return enabled && databaseActivationNodeTypes.has(type);
}
const queryOnActivationNodeTypes = new Set<TreeNodeType>(["connection", "database", "schema", "mongo-db"]);
/**
* Whether activating this row should open (or focus) the connection's query
* page for the "open query on connection open" preference. Specialized
* workbench connections (nacos, consul, hbase, zookeeper, plugin, mq, mqtt,
* meilisearch, salesforce) keep their dedicated surfaces instead — the same
* `supportsConnectionQueryActions` gate the context menu applies to its
* "New Query" entry.
*/
export function shouldOpenQueryOnTreeNodeActivation(node: Pick<TreeNode, "type" | "connectionId">, dbType: DatabaseType | undefined, enabled: boolean): boolean {
return enabled && !!node.connectionId && queryOnActivationNodeTypes.has(node.type) && supportsConnectionQueryActions(dbType);
}
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "job", "package", "package-body", "type", "type-body"]);
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
const tableChildGroupNodeTypes = new Set<TreeNodeType>(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-constraints", "group-partitions", "group-table-partitions", "group-table-subpartitions"]);
@@ -0,0 +1,77 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("queryStore createTab reuseQueryTabByScope", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
});
vi.stubGlobal("window", { dispatchEvent: vi.fn() });
setActivePinia(createPinia());
});
it("focuses the existing plain query tab instead of stacking duplicates", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const firstId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
const secondId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
expect(secondId).toBe(firstId);
expect(queryStore.tabs.filter((tab) => tab.mode === "query")).toHaveLength(1);
expect(queryStore.activeTabId).toBe(firstId);
});
it("creates separate tabs for separate connection scopes", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const appTabId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
const otherDbTabId = queryStore.createTab("pg-1", "metrics", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
const otherSchemaTabId = queryStore.createTab("pg-1", "app", undefined, "query", "internal", undefined, undefined, { reuseQueryTabByScope: true });
const otherConnectionTabId = queryStore.createTab("pg-2", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
expect(new Set([appTabId, otherDbTabId, otherSchemaTabId, otherConnectionTabId]).size).toBe(4);
});
it("keeps stacking behavior without the option so New Query stays explicit", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const firstId = queryStore.createTab("pg-1", "app", undefined, "query");
const secondId = queryStore.createTab("pg-1", "app", undefined, "query");
expect(secondId).not.toBe(firstId);
expect(queryStore.tabs).toHaveLength(2);
});
it("never takes over saved sql, external file, or object source tabs", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const savedSqlId = queryStore.createTab("pg-1", "app", "report.sql", "query", "public");
queryStore.tabs.find((tab) => tab.id === savedSqlId)!.savedSqlId = "saved-1";
const externalId = queryStore.createTab("pg-1", "app", "dump.sql", "query", "public");
queryStore.tabs.find((tab) => tab.id === externalId)!.externalSqlPath = "/tmp/dump.sql";
const activationId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
expect(activationId).not.toBe(savedSqlId);
expect(activationId).not.toBe(externalId);
});
it("lets forceNew win over scope reuse", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const firstId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true });
const forcedId = queryStore.createTab("pg-1", "app", undefined, "query", "public", undefined, undefined, { reuseQueryTabByScope: true, forceNew: true });
expect(forcedId).not.toBe(firstId);
expect(queryStore.tabs).toHaveLength(2);
});
});
+13 -1
View File
@@ -2834,7 +2834,7 @@ export const useQueryStore = defineStore("query", () => {
schema?: string,
initialSql?: string,
catalog?: string,
options: { forceNew?: boolean; activate?: boolean; forceWordWrap?: boolean; insertAfterActive?: boolean; sourceView?: boolean } = {},
options: { forceNew?: boolean; activate?: boolean; forceWordWrap?: boolean; insertAfterActive?: boolean; sourceView?: boolean; reuseQueryTabByScope?: boolean } = {},
) {
if (title && !options.forceNew) {
const existing = findTabByIdentity(connectionId, database, title, mode, schema, catalog);
@@ -2844,6 +2844,18 @@ export const useQueryStore = defineStore("query", () => {
return existing.id;
}
}
if (options.reuseQueryTabByScope && !options.forceNew && mode === "query") {
// Sidebar activation opens "the" query page for this connection scope:
// focus an existing plain query tab instead of stacking duplicates on
// repeated clicks (and on the click that precedes every dblclick).
// Saved SQL, external SQL files, and object-source tabs are documents
// in their own right and never take over the activation slot.
const existing = tabs.value.find((tab) => tab.mode === "query" && !tab.savedSqlId && !tab.externalSqlPath && !tab.objectSource && tab.connectionId === connectionId && tab.database === database && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
if (existing) {
switchTab(existing.id);
return existing.id;
}
}
const id = uuid();
const dbType = useConnectionStore().getConfig(connectionId)?.db_type;
+4
View File
@@ -1029,6 +1029,7 @@ export interface EditorSettings {
autoSelectActiveSidebarNode: boolean;
sidebarPinDefaultDatabase: boolean;
sidebarBrowseObjectsOnDatabaseActivation: boolean;
openQueryOnConnectionOpen: boolean;
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: number;
openTabsRestoreMode: OpenTabsRestoreMode;
autoReloadRestoredDataTabsOnOpen: boolean;
@@ -1345,6 +1346,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
autoSelectActiveSidebarNode: false,
sidebarPinDefaultDatabase: true,
sidebarBrowseObjectsOnDatabaseActivation: false,
openQueryOnConnectionOpen: false,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
openTabsRestoreMode: "all",
autoReloadRestoredDataTabsOnOpen: false,
@@ -1968,6 +1970,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
sidebarSearchOpenedDatabasesOnly: typeof settings.sidebarSearchOpenedDatabasesOnly === "boolean" ? settings.sidebarSearchOpenedDatabasesOnly : DEFAULT_EDITOR_SETTINGS.sidebarSearchOpenedDatabasesOnly,
autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
sidebarPinDefaultDatabase: typeof settings.sidebarPinDefaultDatabase === "boolean" ? settings.sidebarPinDefaultDatabase : DEFAULT_EDITOR_SETTINGS.sidebarPinDefaultDatabase,
openQueryOnConnectionOpen: typeof settings.openQueryOnConnectionOpen === "boolean" ? settings.openQueryOnConnectionOpen : DEFAULT_EDITOR_SETTINGS.openQueryOnConnectionOpen,
sidebarBrowseObjectsOnDatabaseActivation:
typeof settings.sidebarBrowseObjectsOnDatabaseActivation === "boolean"
? settings.sidebarBrowseObjectsOnDatabaseActivation
@@ -2838,6 +2841,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode;
if (partial.sidebarPinDefaultDatabase !== undefined) editorSettings.value.sidebarPinDefaultDatabase = partial.sidebarPinDefaultDatabase === true;
if (partial.sidebarBrowseObjectsOnDatabaseActivation !== undefined) editorSettings.value.sidebarBrowseObjectsOnDatabaseActivation = partial.sidebarBrowseObjectsOnDatabaseActivation === true;
if (partial.openQueryOnConnectionOpen !== undefined) editorSettings.value.openQueryOnConnectionOpen = partial.openQueryOnConnectionOpen === true;
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
if (partial.autoReloadRestoredDataTabsOnOpen !== undefined) editorSettings.value.autoReloadRestoredDataTabsOnOpen = partial.autoReloadRestoredDataTabsOnOpen === true;
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);