From f9bf039aefa62e4df5e54a6958266d8487b0f7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Wed, 15 Jul 2026 20:11:49 +0800 Subject: [PATCH] feat(sidebar): refresh data tables on double click --- .../src/components/sidebar/TreeItem.vue | 38 ++++- apps/desktop/src/lib/sidebar/treeNodeClick.ts | 3 +- .../desktop/src/lib/tabs/dataTabActivation.ts | 13 ++ .../queryStore.tableDataRefresh.spec.ts | 143 ++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 60 +++++--- packages/app-tests/dataTabActivation.test.ts | 96 +++++++++++- packages/app-tests/treeNodeClick.test.ts | 12 +- 7 files changed, 342 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 7769441c3..c31fc7aeb 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -107,7 +107,7 @@ import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPast import { formatSqlInsert } from "@/lib/export/exportFormats"; import { joinExportedDdls } from "@/lib/export/ddlExport"; import { fetchTableDataForExport } from "@/lib/table/tableDataExport"; -import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation"; +import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation"; import { buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, duckDbAttachedDatabaseNameFromPath, supportsCreateDatabaseCharset, uniqueDuckDbAttachedDatabaseName } from "@/lib/database/createDatabaseSql"; import { buildCreateSchemaSql, @@ -838,6 +838,9 @@ function runRowClickAction(clickDetail: number) { const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation); if (!shouldRunTreeNodeRowAction(action, clickDetail)) return; if (action === "open-data") { + if (node.type === "table") { + singleActivationDoubleClickRefreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(findExistingSameTableDataTab()); + } scheduleOpenData(node); } else if (isDocumentBrowserTreeNode(node.type)) { openMongoTreeData(node); @@ -848,6 +851,8 @@ function runRowClickAction(clickDetail: number) { } } +let singleActivationDoubleClickRefreshAllowed = false; + function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper", connectionId: string) { void nextTick(() => { window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } })); @@ -972,6 +977,7 @@ function toggleConnectionMultiSelection(event: MouseEvent) { } function onClick(event: MouseEvent) { + if (props.node.type === "table" && event.detail <= 1) singleActivationDoubleClickRefreshAllowed = false; if (suppressNextTableReferenceClick) { suppressNextTableReferenceClick = false; event.preventDefault(); @@ -1193,6 +1199,8 @@ function onDoubleClick() { if (!props.node.isExpanded) void toggle(); } else if (action === "open-data") { openDataImmediately(props.node); + } else if (action === "refresh-data") { + void refreshData(); } else if (action === "open-source") { openObjectSourceDialog(false); } else if (action === "open-saved-sql") { @@ -1204,6 +1212,34 @@ function onDoubleClick() { } } +async function refreshData() { + const node = props.node; + if (node.type !== "table" || !hasNodeDatabaseContext(node)) return; + const singleActivationRefreshAllowed = singleActivationDoubleClickRefreshAllowed; + singleActivationDoubleClickRefreshAllowed = false; + const activation = settingsStore.editorSettings.sidebarActivation; + if (activation === "single" && !singleActivationRefreshAllowed) return; + const existingSameTableTab = findExistingSameTableDataTab(); + const action = dataTableDoubleClickAction(existingSameTableTab, activation, singleActivationRefreshAllowed); + if (action === "none") return; + if (action === "open") { + openDataImmediately(node); + return; + } + if (!existingSameTableTab) return; + queryStore.switchTab(existingSameTableTab.id); + if (action === "activate") return; + await queryStore.refreshDataTab(existingSameTableTab.id); +} + +function findExistingSameTableDataTab() { + const node = props.node; + if (node.type !== "table" || !hasNodeDatabaseContext(node)) return undefined; + const config = connectionStore.getConfig(node.connectionId); + const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema); + return queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.tableMeta?.catalog || "") === (node.catalog || "") && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label); +} + function openMongoTreeData(node: TreeNode) { if (!node.connectionId || !node.database) return; if (node.type === "mongo-gridfs") { diff --git a/apps/desktop/src/lib/sidebar/treeNodeClick.ts b/apps/desktop/src/lib/sidebar/treeNodeClick.ts index 69ad01e5d..dd1328eee 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeClick.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeClick.ts @@ -2,7 +2,7 @@ import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database" import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts"; export type TreeNodeRowAction = "open-data" | "toggle" | "none"; -export type TreeNodeRowDoubleClickAction = "open-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none"; +export type TreeNodeRowDoubleClickAction = "open-data" | "refresh-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none"; export type SidebarSelectionCopyAction = "copy-name" | "none"; export type SidebarActivation = "single" | "double"; @@ -45,6 +45,7 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai } export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false): TreeNodeRowDoubleClickAction { + if (type === "table") return "refresh-data"; if (activation === "double") { if (dataNodeTypes.has(type)) return "open-data"; if (sourceNodeTypes.has(type)) return "open-source"; diff --git a/apps/desktop/src/lib/tabs/dataTabActivation.ts b/apps/desktop/src/lib/tabs/dataTabActivation.ts index ddf5aae3e..551045909 100644 --- a/apps/desktop/src/lib/tabs/dataTabActivation.ts +++ b/apps/desktop/src/lib/tabs/dataTabActivation.ts @@ -1,5 +1,7 @@ import type { QueryResult, QueryTab } from "@/types/database"; +export type DataTableDoubleClickAction = "activate" | "open" | "refresh" | "none"; + function isErrorResult(result: QueryResult | undefined): boolean { return result?.columns.length === 1 && result.columns[0] === "Error"; } @@ -9,3 +11,14 @@ export function canActivateExistingDataTableTab(tab: QueryTab, options: { activa if (isErrorResult(tab.result)) return false; return !!tab.result || !!tab.results?.length; } + +export function canRefreshDataTableFromSingleActivationDoubleClick(tab: QueryTab | undefined): boolean { + return !!tab && !tab.isExecuting && canActivateExistingDataTableTab(tab); +} + +export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double", singleActivationRefreshAllowed = false): DataTableDoubleClickAction { + if (activation === "single" && !singleActivationRefreshAllowed) return "none"; + if (!tab) return activation === "double" ? "open" : "none"; + if (!canActivateExistingDataTableTab(tab)) return "open"; + return tab.isExecuting ? "activate" : "refresh"; +} diff --git a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts index 6762b126c..9330b00a0 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts @@ -124,4 +124,147 @@ describe("queryStore table data refresh", () => { expect(store.tabs.find((tab) => tab.id === publicTabId)?.result?.rows).toEqual([]); expect(store.tabs.find((tab) => tab.id === archiveTabId)?.result).toBeUndefined(); }); + + it("refreshes one targeted tab while preserving its query context", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const firstTabId = store.createTab("pg-1", "app", "users", "data", "public"); + const secondTabId = store.createTab("pg-1", "app", "users-copy", "data", "public"); + for (const tabId of [firstTabId, secondTabId]) { + store.setTableMeta(tabId, { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["id"], + }); + } + const firstTab = store.tabs.find((tab) => tab.id === firstTabId)!; + firstTab.whereInput = "status = 'ACTIVE'"; + firstTab.resultSortColumn = "created_at"; + firstTab.resultSortDirection = "desc"; + firstTab.resultPageLimit = 25; + firstTab.resultPageOffset = 50; + + const refreshed = await store.refreshDataTab(firstTabId); + + expect(refreshed).toBe(true); + expect(mocks.buildTableSelectSql).toHaveBeenCalledWith( + expect.objectContaining({ + whereInput: "status = 'ACTIVE'", + orderBy: '"created_at" DESC', + limit: 25, + offset: 50, + }), + ); + expect(mocks.executeMulti).toHaveBeenCalledTimes(1); + expect(store.tabs.find((tab) => tab.id === firstTabId)?.result?.rows).toEqual([]); + expect(store.tabs.find((tab) => tab.id === secondTabId)?.result).toBeUndefined(); + }); + + it("rejects a repeated refresh while SQL construction is in progress", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("pg-1", "app", "users", "data", "public"); + store.setTableMeta(tabId, { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["id"], + }); + let resolveSql!: (sql: string) => void; + mocks.buildTableSelectSql.mockReturnValueOnce(new Promise((resolve) => (resolveSql = resolve))); + + const firstRefresh = store.refreshDataTab(tabId); + expect(store.tabs.find((tab) => tab.id === tabId)?.isExecuting).toBe(true); + await expect(store.refreshDataTab(tabId)).resolves.toBe(false); + expect(mocks.buildTableSelectSql).toHaveBeenCalledTimes(1); + expect(mocks.executeMulti).not.toHaveBeenCalled(); + + resolveSql("SELECT id FROM public.users LIMIT 100 OFFSET 0"); + await expect(firstRefresh).resolves.toBe(true); + expect(mocks.executeMulti).toHaveBeenCalledTimes(1); + }); + + it("returns false for SQL build failures, stores an error result, and clears the busy state", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("pg-1", "app", "users", "data", "public"); + store.setTableMeta(tabId, { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [], + primaryKeys: [], + }); + mocks.buildTableSelectSql.mockRejectedValueOnce(new Error("failed to build refresh SQL")); + + await expect(store.refreshDataTab(tabId)).resolves.toBe(false); + + const tab = store.tabs.find((candidate) => candidate.id === tabId)!; + expect(tab.isExecuting).toBe(false); + expect(tab.executionId).toBeUndefined(); + expect(tab.result?.execution_error).toBe(true); + expect(tab.result?.rows).toEqual([["failed to build refresh SQL"]]); + expect(mocks.executeMulti).not.toHaveBeenCalled(); + }); + + it("keeps the bulk refresh supersede and count behavior for busy matching tabs", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("pg-1", "app", "users", "data", "public"); + store.setTableMeta(tabId, { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["id"], + }); + const tab = store.tabs.find((candidate) => candidate.id === tabId)!; + tab.isExecuting = true; + tab.executionId = "previous-execution"; + + await expect( + store.refreshDataTabsForTable({ + connectionId: "pg-1", + database: "app", + schema: "public", + name: "users", + }), + ).resolves.toBe(1); + + expect(mocks.buildTableSelectSql).toHaveBeenCalledTimes(1); + expect(mocks.executeMulti).toHaveBeenCalledTimes(1); + expect(tab.isExecuting).toBe(false); + expect(tab.executionId).toBeUndefined(); + }); + + it("keeps bulk SQL build failures observable to callers", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("pg-1", "app", "users", "data", "public"); + store.setTableMeta(tabId, { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [], + primaryKeys: [], + }); + mocks.buildTableSelectSql.mockRejectedValueOnce(new Error("bulk refresh SQL failed")); + + await expect( + store.refreshDataTabsForTable({ + connectionId: "pg-1", + database: "app", + schema: "public", + name: "users", + }), + ).rejects.toThrow("bulk refresh SQL failed"); + + const tab = store.tabs.find((candidate) => candidate.id === tabId)!; + expect(tab.isExecuting).toBe(false); + expect(tab.result?.execution_error).toBe(true); + expect(mocks.executeMulti).not.toHaveBeenCalled(); + }); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 4c551bd82..145fc5d7d 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1754,25 +1754,28 @@ export const useQueryStore = defineStore("query", () => { closeTabsWhere((tab) => tabMatchesDroppedTableObject(tab, target)); } - async function refreshDataTabsForTable(target: TableDataRefreshTarget): Promise { - const matchingTabs = tabs.value.filter((tab) => tabMatchesTableDataRefreshTarget(tab, target)); - if (matchingTabs.length === 0) return 0; + async function refreshDataTabInternal(id: string, options?: { supersedeBusy?: boolean; propagateBuildError?: boolean }): Promise { + const tab = tabs.value.find((candidate) => candidate.id === id); + if (!tab || tab.mode !== "data" || (tab.isExecuting && !options?.supersedeBusy)) return false; + const tableMeta = tableMetaForDataTab(tab); + if (!tableMeta?.tableName) return false; const settingsStore = useSettingsStore(); - let refreshed = 0; + const connStore = useConnectionStore(); + const conn = connStore.getConfig(tab.connectionId); + const effectiveDbType = effectiveDatabaseTypeForConnection(conn); + const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId); + const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys; + const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined; + const orderBy = tab.orderByInput?.trim() || sortOrder; + const limit = tab.resultPageLimit ?? settingsStore.editorSettings.pageSize ?? tableOpenPageLimit(); + const offset = tab.resultPageOffset ?? 0; + const refreshPreparationId = uuid(); - for (const tab of matchingTabs) { - const tableMeta = tableMetaForDataTab(tab); - if (!tableMeta?.tableName) continue; - const connStore = useConnectionStore(); - const conn = connStore.getConfig(tab.connectionId); - const effectiveDbType = effectiveDatabaseTypeForConnection(conn); - const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId); - const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys; - const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined; - const orderBy = tab.orderByInput?.trim() || sortOrder; - const limit = tab.resultPageLimit ?? settingsStore.editorSettings.pageSize ?? tableOpenPageLimit(); - const offset = tab.resultPageOffset ?? 0; + // Reserve the tab synchronously before SQL construction yields so repeated + // refresh requests cannot build and execute duplicate queries. + setExecutingWithId(tab.id, refreshPreparationId); + try { const sql = await buildTableSelectSql({ databaseType: effectiveDbType, identifierQuote, @@ -1789,12 +1792,34 @@ export const useQueryStore = defineStore("query", () => { limit, offset, }); + if (!sql.trim()) throw new Error("Failed to build table refresh SQL"); + const current = tabs.value.find((candidate) => candidate.id === id); + if (!current || current.executionId !== refreshPreparationId) return false; updateSql(tab.id, sql); await executeTabSql(tab.id, sql, { pagination: { limit, offset }, preserveResultDuringExecution: true, }); - refreshed += 1; + return true; + } catch (error) { + const current = tabs.value.find((candidate) => candidate.id === id); + if (current?.executionId === refreshPreparationId) setErrorResult(id, error); + if (options?.propagateBuildError) throw error; + return false; + } + } + + function refreshDataTab(id: string): Promise { + return refreshDataTabInternal(id); + } + + async function refreshDataTabsForTable(target: TableDataRefreshTarget): Promise { + const matchingTabs = tabs.value.filter((tab) => tabMatchesTableDataRefreshTarget(tab, target)); + if (matchingTabs.length === 0) return 0; + + let refreshed = 0; + for (const tab of matchingTabs) { + if (await refreshDataTabInternal(tab.id, { supersedeBusy: true, propagateBuildError: true })) refreshed += 1; } return refreshed; @@ -4036,6 +4061,7 @@ export const useQueryStore = defineStore("query", () => { closeConnectionTabs, closeDatabaseTabs, closeDroppedTableObjectTabs, + refreshDataTab, refreshDataTabsForTable, releaseConnectionTabs, releaseDatabaseTabs, diff --git a/packages/app-tests/dataTabActivation.test.ts b/packages/app-tests/dataTabActivation.test.ts index efbc8408e..6658d73ab 100644 --- a/packages/app-tests/dataTabActivation.test.ts +++ b/packages/app-tests/dataTabActivation.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { canActivateExistingDataTableTab } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts"; +import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts"; import type { QueryTab } from "../../apps/desktop/src/types/database.ts"; function dataTab(overrides: Partial = {}): QueryTab { @@ -61,3 +61,97 @@ test("reloads existing data table tabs showing an error result", () => { false, ); }); + +test("single activation snapshots only successful idle tabs as refreshable", () => { + assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(undefined), false); + assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab()), false); + assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab({ isExecuting: true })), false); + assert.equal( + canRefreshDataTableFromSingleActivationDoubleClick( + dataTab({ + result: { + columns: ["Error"], + rows: [["connection failed"]], + affected_rows: 0, + execution_time_ms: 0, + }, + }), + ), + false, + ); + assert.equal( + canRefreshDataTableFromSingleActivationDoubleClick( + dataTab({ + result: { + columns: ["id"], + rows: [[1]], + affected_rows: 0, + execution_time_ms: 1, + }, + }), + ), + true, + ); +}); + +test("single activation uses missing, restored, error, and busy first-click snapshots even if the tab succeeds before dblclick", () => { + const successfulAtDoubleClick = dataTab({ + result: { + columns: ["id"], + rows: [[1]], + affected_rows: 0, + execution_time_ms: 1, + }, + }); + const errorAtFirstClick = dataTab({ + result: { + columns: ["Error"], + rows: [["connection failed"]], + affected_rows: 0, + execution_time_ms: 0, + }, + }); + for (const initialTab of [undefined, dataTab(), errorAtFirstClick, dataTab({ isExecuting: true })]) { + const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(initialTab); + assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "none"); + } + const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(successfulAtDoubleClick); + assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "refresh"); +}); + +test("double activation opens a missing table without a first-click snapshot", () => { + assert.equal(dataTableDoubleClickAction(undefined, "double"), "open"); +}); + +test("double activation decisions preserve loading, refresh, and recovery behavior", () => { + assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "double"), "activate"); + assert.equal( + dataTableDoubleClickAction( + dataTab({ + result: { + columns: ["id"], + rows: [[1]], + affected_rows: 0, + execution_time_ms: 1, + }, + }), + "double", + ), + "refresh", + ); + assert.equal(dataTableDoubleClickAction(dataTab(), "double"), "open"); + assert.equal( + dataTableDoubleClickAction( + dataTab({ + result: { + columns: ["Error"], + rows: [["connection failed"]], + affected_rows: 0, + execution_time_ms: 0, + }, + }), + "double", + ), + "open", + ); +}); diff --git a/packages/app-tests/treeNodeClick.test.ts b/packages/app-tests/treeNodeClick.test.ts index 9bd4ee484..be471d233 100644 --- a/packages/app-tests/treeNodeClick.test.ts +++ b/packages/app-tests/treeNodeClick.test.ts @@ -14,9 +14,14 @@ test("double click navigation mode selects rows on single click", () => { assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none"); }); -test("double click navigation mode opens actionable rows on double click", () => { - assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "open-data"); +test("table rows refresh on double click in both navigation modes", () => { + assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "refresh-data"); + assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "refresh-data"); +}); + +test("double click navigation mode opens other actionable rows on double click", () => { assert.equal(treeNodeRowDoubleClickAction("view", true, "double"), "open-data"); + assert.equal(treeNodeRowDoubleClickAction("materialized_view", true, "double"), "open-data"); assert.equal(treeNodeRowDoubleClickAction("procedure", false, "double"), "open-source"); assert.equal(treeNodeRowDoubleClickAction("saved-sql-file", false, "double"), "open-saved-sql"); }); @@ -96,7 +101,8 @@ test("double click navigation mode opens object browser and expands expandable d test("double click does not open object browser for non-browsable rows", () => { assert.equal(treeNodeRowDoubleClickAction("database", false), "none"); - assert.equal(treeNodeRowDoubleClickAction("table", true), "none"); + assert.equal(treeNodeRowDoubleClickAction("view", true), "none"); + assert.equal(treeNodeRowDoubleClickAction("materialized_view", true), "none"); assert.equal(treeNodeRowDoubleClickAction("column", true), "none"); });