diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index a308393ef4..815fce5cfe 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -152,7 +152,7 @@ import { type ObjectBrowserSortDirection, type ObjectBrowserSortKey, } from "@/lib/table/objectBrowserRows"; -import { isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction"; +import { isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, singleClickRowAction, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction"; import { objectBrowserTableSelectionAnchor, objectBrowserTableSelectionRange } from "@/lib/table/objectBrowserSelection"; import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities"; import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo"; @@ -1036,13 +1036,18 @@ function onRowClick(row: ObjectBrowserRow, event: MouseEvent) { if (row.type === "TABLE") tableSelectionAnchorId.value = row.id; const activation = settingsStore.editorSettings.sidebarActivation; const { action, isDouble } = resolveRowClickAction(row, event.detail, activation, effectiveDatabaseType.value); - // Double click: cancel any pending single-click and fire immediately + // Double click: cancel any pending single-click and fire immediately. When + // the row's single/double actions are identical (e.g. VIEW → open-source), + // this gesture's first click already ran it — re-executing would toggle the + // just-opened side panel back off (or emit open-table twice for MongoDB). if (isDouble) { if (singleClickTimer) { clearTimeout(singleClickTimer); singleClickTimer = null; } - executeRowAction(row, action); + if (action !== singleClickRowAction(row, effectiveDatabaseType.value)) { + executeRowAction(row, action); + } return; } // Single click: defer when the row has a distinct double-click action so a diff --git a/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts b/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts index 9aeae87177..bd88ba3fcd 100644 --- a/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts @@ -76,8 +76,8 @@ describe("doubleClickRowAction", () => { expect(doubleClickRowAction(row("TABLE", "orders"))).toBe("open-table"); }); - it("returns open-source for VIEW", () => { - expect(doubleClickRowAction(row("VIEW", "v_orders"))).toBe("open-source"); + it.each(["VIEW", "MATERIALIZED_VIEW"] as const)("returns open-table for %s like the sidebar data nodes", (type) => { + expect(doubleClickRowAction(row(type, "v_orders"))).toBe("open-table"); }); it("returns open-source for PROCEDURE", () => { @@ -123,9 +123,9 @@ describe("resolveRowClickAction", () => { expect(result.isDouble).toBe(false); }); - it("double click on VIEW returns open-source", () => { + it("double click on VIEW returns open-table", () => { const result = resolveRowClickAction(viewRow, 2, "single"); - expect(result.action).toBe("open-source"); + expect(result.action).toBe("open-table"); expect(result.isDouble).toBe(true); }); }); @@ -149,9 +149,9 @@ describe("resolveRowClickAction", () => { expect(result.isDouble).toBe(true); }); - it("double click on VIEW returns open-source", () => { + it("double click on VIEW returns open-table", () => { const result = resolveRowClickAction(viewRow, 2, "double"); - expect(result.action).toBe("open-source"); + expect(result.action).toBe("open-table"); expect(result.isDouble).toBe(true); }); }); @@ -165,8 +165,12 @@ describe("shouldDeferSingleClick", () => { expect(shouldDeferSingleClick(tableRow, "table-info")).toBe(true); }); - it("does not defer VIEW open-source (same single/double action)", () => { - expect(shouldDeferSingleClick(viewRow, "open-source")).toBe(false); + it("defers VIEW open-source (distinct single/double actions)", () => { + expect(shouldDeferSingleClick(viewRow, "open-source")).toBe(true); + }); + + it("does not defer PROCEDURE open-source (same single/double action)", () => { + expect(shouldDeferSingleClick(row("PROCEDURE", "sp_run"), "open-source")).toBe(false); }); it("does not defer none action", () => { diff --git a/apps/desktop/src/lib/table/objectBrowserRowAction.ts b/apps/desktop/src/lib/table/objectBrowserRowAction.ts index 08e1e3c1a7..fc5468f799 100644 --- a/apps/desktop/src/lib/table/objectBrowserRowAction.ts +++ b/apps/desktop/src/lib/table/objectBrowserRowAction.ts @@ -26,14 +26,15 @@ export function singleClickRowAction(row: ObjectBrowserRow | null | undefined, d /** * Determine the action for a double click on an object browser row. - * - TABLE → open-table (open table data tab) - * - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source + * - TABLE/VIEW/MATERIALIZED_VIEW → open-table (open data tab, matching the + * sidebar's data-node double-click behavior) + * - PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source * - otherwise → none */ export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined, dbType?: DatabaseType): ObjectBrowserRowAction { if (!row) return "none"; if (dbType === "mongodb") return mongoObjectBrowserRowAction(row); - if (row.type === "TABLE") return "open-table"; + if (row.type === "TABLE" || row.type === "VIEW" || row.type === "MATERIALIZED_VIEW") return "open-table"; if (row.type === "EVENT") return "open-source"; if (row.type === "TYPE" && customTypeCapabilities(dbType).details) return "type-info"; if (canOpenSource(row, dbType)) return "open-source";