feat(objects): open routines in source tab on double click

This commit is contained in:
OOMEcho authored and GitHub committed 2026-09-24 17:26:50 +08:00
1 parent 41ec53442c
commit 55d4b0b613
4 files changed
+276 -10

No files matched your search

@@ -80,8 +80,27 @@ describe("doubleClickRowAction", () => {
expect(doubleClickRowAction(row(type, "v_orders"))).toBe("open-table");
});
it("returns open-source for PROCEDURE", () => {
expect(doubleClickRowAction(row("PROCEDURE", "sp_run"))).toBe("open-source");
it.each(["PROCEDURE", "FUNCTION"] as const)("returns open-source-tab for %s (editable source tab, not the side panel)", (type) => {
expect(doubleClickRowAction(row(type, "sp_run"))).toBe("open-source-tab");
});
it("keeps routines on the source tab across database types", () => {
for (const dbType of ["mysql", "postgres", "oracle", "xugu"] as const) {
expect(doubleClickRowAction(row("PROCEDURE", "sp_run"), dbType), String(dbType)).toBe("open-source-tab");
expect(doubleClickRowAction(row("FUNCTION", "fn_run"), dbType), String(dbType)).toBe("open-source-tab");
}
});
it("leaves the other source-backed types on the side panel", () => {
expect(doubleClickRowAction(row("EVENT", "ev_nightly"))).toBe("open-source");
expect(doubleClickRowAction(row("SEQUENCE", "seq_test"))).toBe("open-source");
expect(doubleClickRowAction(row("PACKAGE", "pkg_test"))).toBe("open-source");
expect(doubleClickRowAction(row("PACKAGE_BODY", "pkg_body_test"))).toBe("open-source");
expect(doubleClickRowAction(row("TRIGGER", "trg_test"), "xugu")).toBe("open-source");
expect(doubleClickRowAction(row("TYPE", "app_status"), "xugu")).toBe("open-source");
expect(doubleClickRowAction(row("TYPE_BODY", "app_status"), "xugu")).toBe("open-source");
expect(doubleClickRowAction(row("VIEW", "v_users"))).toBe("open-table");
expect(doubleClickRowAction(row("MATERIALIZED_VIEW", "mv_users"))).toBe("open-table");
});
it("returns none for null/undefined", () => {
@@ -128,6 +147,12 @@ describe("resolveRowClickAction", () => {
expect(result.action).toBe("open-table");
expect(result.isDouble).toBe(true);
});
it.each(["PROCEDURE", "FUNCTION"] as const)("single click on %s returns open-source, double click returns open-source-tab", (type) => {
const routineRow = row(type, "sp_run");
expect(resolveRowClickAction(routineRow, 1, "single")).toEqual({ action: "open-source", isDouble: false });
expect(resolveRowClickAction(routineRow, 2, "single")).toEqual({ action: "open-source-tab", isDouble: true });
});
});
describe("double-click activation mode", () => {
@@ -154,6 +179,12 @@ describe("resolveRowClickAction", () => {
expect(result.action).toBe("open-table");
expect(result.isDouble).toBe(true);
});
it.each(["PROCEDURE", "FUNCTION"] as const)("single click on %s returns open-source, double click returns open-source-tab", (type) => {
const routineRow = row(type, "sp_run");
expect(resolveRowClickAction(routineRow, 1, "double")).toEqual({ action: "open-source", isDouble: false });
expect(resolveRowClickAction(routineRow, 2, "double")).toEqual({ action: "open-source-tab", isDouble: true });
});
});
});
@@ -169,8 +200,15 @@ describe("shouldDeferSingleClick", () => {
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.each(["PROCEDURE", "FUNCTION"] as const)("defers %s open-source so a second click can cancel the side panel", (type) => {
// single → open-source, double → open-source-tab: the 250ms deferral is what
// keeps a fast double click from also opening the side panel (issue #10202).
expect(shouldDeferSingleClick(row(type, "sp_run"), "open-source")).toBe(true);
expect(shouldDeferSingleClick(row(type, "sp_run"), "open-source-tab")).toBe(false);
});
it("does not defer SEQUENCE open-source (same single/double action)", () => {
expect(shouldDeferSingleClick(row("SEQUENCE", "seq_run"), "open-source")).toBe(false);
});
it("does not defer none action", () => {
@@ -2,7 +2,7 @@ import type { DatabaseType } from "@/types/database";
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import type { ObjectBrowserRow } from "@/lib/table/objectBrowserRows";
export type ObjectBrowserRowAction = "table-info" | "type-info" | "open-table" | "open-source" | "none";
export type ObjectBrowserRowAction = "table-info" | "type-info" | "open-table" | "open-source" | "open-source-tab" | "none";
/**
* Determine the action for a single click on an object browser row.
@@ -28,7 +28,8 @@ export function singleClickRowAction(row: ObjectBrowserRow | null | undefined, d
* Determine the action for a double click on an object browser row.
* - 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
* - PROCEDURE/FUNCTION → open-source-tab (editable source tab; single click keeps the side panel)
* - TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source
* - otherwise → none
*/
export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined, dbType?: DatabaseType): ObjectBrowserRowAction {
@@ -37,6 +38,10 @@ export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined, d
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";
// Routines are the only source-backed rows with a dedicated double-click
// gesture (issue #10202): single click keeps the side panel, double click
// hands the object to the source tab. canOpenSource always accepts them.
if (row.type === "PROCEDURE" || row.type === "FUNCTION") return "open-source-tab";
if (canOpenSource(row, dbType)) return "open-source";
return "none";
}
@@ -47,7 +52,8 @@ export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined, d
*
* In both single-click and double-click activation modes, a single click
* triggers the side-panel action (table-info / open-source). When a distinct
* double-click action exists (e.g. TABLE single→table-info, double→open-table),
* double-click action exists (e.g. TABLE single→table-info, double→open-table;
* PROCEDURE single→open-source, double→open-source-tab),
* the caller defers the single-click via shouldDeferSingleClick so the second
* click can cancel it.
*/
@@ -65,8 +71,9 @@ export function resolveRowClickAction(row: ObjectBrowserRow | null | undefined,
* Whether a single-click action should be deferred to distinguish it from a
* possible upcoming double-click. Applies when the row's single-click and
* double-click actions differ (e.g. TABLE: single → table-info, double →
* open-table). For rows whose single and double actions are identical
* (e.g. VIEW → open-source both), no deferral is needed.
* open-table; PROCEDURE/FUNCTION: single → open-source, double →
* open-source-tab). For rows whose single and double actions are identical
* (e.g. SEQUENCE → open-source both), no deferral is needed.
*/
export function shouldDeferSingleClick(row: ObjectBrowserRow | null | undefined, action: ObjectBrowserRowAction, dbType?: DatabaseType): boolean {
if (action === "none") return false;