feat(sidebar): open structure editor on field double click

This commit is contained in:
二丫讲梵
2026-10-05 02:01:56 +08:00
committed by GitHub
parent d757238b1b
commit 7fef9698ac
5 changed files with 42 additions and 15 deletions
@@ -1618,8 +1618,8 @@ function onDoubleClick(event: MouseEvent) {
return;
}
const action = treeNodeRowDoubleClickAction(activeNode.value.type, canOpenObjectBrowser.value, settingsStore.editorSettings.sidebarActivation, canExpand.value, currentDatabaseType(), canOpenConnectionDatabaseBrowser.value, settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation);
// In single-click mode the trailing dblclick normally has no action and must
// leave the first click's request ownership intact.
// Most trailing dblclick events are intentionally inert in single-click
// mode, but field and index rows have an explicit editor action.
if (action === "none") return;
const requestId = beginNavigationRequest();
if (action === "open-database-browser") {
@@ -1635,6 +1635,8 @@ function onDoubleClick(event: MouseEvent) {
activateDataTableFromDoubleClick();
} else if (action === "locate-column") {
locateColumnInDataGrid(activeNode.value);
} else if (action === "open-structure-editor") {
if (canOpenStructureEditor.value) openStructureEditor();
} else if (action === "open-source") {
openObjectSourceDialog(false);
} else if (action === "open-extension-details") {
@@ -38,6 +38,7 @@ const settings = {
};
const openDataMock = vi.fn();
const openStructureEditorMock = vi.fn();
vi.mock("@/stores/connectionStore", () => ({
CONNECTION_ATTEMPT_CANCELLED_MESSAGE: "connection attempt cancelled",
@@ -55,7 +56,7 @@ vi.mock("@/composables/useSidebarConnectionMutationRuntime", () => ({ useSidebar
vi.mock("@/composables/useSidebarDatabaseSpecificMutationRuntime", () => ({ useSidebarDatabaseSpecificMutationRuntime: () => ({}) }));
vi.mock("@/composables/useSidebarTableMutationRuntime", () => ({ useSidebarTableMutationRuntime: () => ({}) }));
vi.mock("@/composables/useSidebarTreeExportRuntime", () => ({ useSidebarTreeExportRuntime: () => ({}) }));
vi.mock("@/composables/useSidebarTreeToolRuntime", () => ({ useSidebarTreeToolRuntime: () => ({}) }));
vi.mock("@/composables/useSidebarTreeToolRuntime", () => ({ useSidebarTreeToolRuntime: () => ({ openStructureEditor: openStructureEditorMock }) }));
const mountedApps: App[] = [];
@@ -64,6 +65,7 @@ afterEach(() => {
document.body.innerHTML = "";
connectionStore.treeNodes = [];
openDataMock.mockReset();
openStructureEditorMock.mockReset();
vi.clearAllMocks();
});
@@ -119,7 +121,7 @@ describe("SidebarTreeRuntimeHost column locate", () => {
expect(openDataMock).toHaveBeenCalledWith(expect.objectContaining({ label: "users" }), expect.anything(), "default", { revealColumn: "email" });
});
it("locates column in data grid on double click in double-activation mode", async () => {
it("opens the structure editor for a double-clicked column", async () => {
settings.sidebarActivation = "double";
const tableNode: TreeNode = {
@@ -166,7 +168,8 @@ describe("SidebarTreeRuntimeHost column locate", () => {
host.value?.handleRowDoubleClick(columnNode, new MouseEvent("dblclick"));
await nextTick();
expect(onOpenData).toHaveBeenCalledWith(expect.objectContaining({ label: "users", connectionId: "mysql", database: "app" }), false, "default", expect.any(Function));
expect(openDataMock).toHaveBeenCalledWith(expect.objectContaining({ label: "users" }), expect.anything(), "default", { revealColumn: "age" });
expect(onOpenData).not.toHaveBeenCalled();
expect(openDataMock).not.toHaveBeenCalled();
expect(openStructureEditorMock).toHaveBeenCalledTimes(1);
});
});
@@ -219,10 +219,12 @@ describe("treeNodeClick", () => {
});
});
it("locates column in data grid on single click in single-activation mode and double click in double-activation mode", () => {
it("keeps column single-click navigation and opens structure editor on double click", () => {
expect(treeNodeRowAction("column", false, "single")).toBe("locate-column");
expect(treeNodeRowAction("column", false, "double")).toBe("none");
expect(treeNodeRowDoubleClickAction("column", false, "single")).toBe("none");
expect(treeNodeRowDoubleClickAction("column", false, "double")).toBe("locate-column");
expect(treeNodeRowDoubleClickAction("column", false, "single")).toBe("open-structure-editor");
expect(treeNodeRowDoubleClickAction("column", false, "double")).toBe("open-structure-editor");
expect(treeNodeRowDoubleClickAction("index", false, "single")).toBe("open-structure-editor");
expect(treeNodeRowDoubleClickAction("index", false, "double")).toBe("open-structure-editor");
});
});
+20 -3
View File
@@ -3,7 +3,20 @@ import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database
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";
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-database-browser" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-extension-details" | "open-event-trigger-details" | "open-saved-sql" | "locate-column" | "toggle" | "none";
export type TreeNodeRowDoubleClickAction =
| "open-data"
| "activate-data"
| "open-database-browser"
| "open-object-browser"
| "open-object-browser-and-expand"
| "open-source"
| "open-extension-details"
| "open-event-trigger-details"
| "open-saved-sql"
| "locate-column"
| "open-structure-editor"
| "toggle"
| "none";
export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double";
@@ -160,11 +173,16 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false, dbType?: DatabaseType, canOpenDatabaseBrowser = false, browseObjectsOnDatabaseActivation = false): TreeNodeRowDoubleClickAction {
// Single-click activation already handles the first click in a dblclick
// sequence. Only double-click activation needs a second-stage table action.
// sequence. Most rows have no trailing action, while field/index rows have
// an explicit structure-editor action in either activation mode.
if (type === "table") return activation === "double" ? "activate-data" : "none";
// 双击激活模式下,双击连接节点应当展开树(与单击模式下单击展开一致),
// “打开数据库浏览”只保留给单击激活模式下的双击手势。
if (type === "connection" && canOpenDatabaseBrowser && activation !== "double") return "open-database-browser";
// Field and index rows have an explicit double-click action regardless of
// the sidebar activation preference. Single-click field navigation remains
// responsible for locating a column in the data grid.
if (type === "column" || type === "index") return "open-structure-editor";
if (activation === "double") {
if (canOpenObjectBrowser && shouldBrowseObjectsOnDatabaseActivation(type, browseObjectsOnDatabaseActivation)) {
return canExpand ? "open-object-browser-and-expand" : "open-object-browser";
@@ -173,7 +191,6 @@ export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBr
if (type === "extension") return "open-extension-details";
if (type === "event-trigger") return "open-event-trigger-details";
if (dataNodeTypes.has(type)) return "open-data";
if (type === "column") return "locate-column";
if (type === "type" && customTypeCapabilities(dbType).details) return canExpand ? "toggle" : "none";
if (sourceNodeTypes.has(type) && canOpenTreeNodeSource(type, dbType)) return "open-source";
if (savedSqlNodeTypes.has(type)) return "open-saved-sql";
+6 -3
View File
@@ -102,10 +102,13 @@ test("plain metadata leaf rows do nothing on row clicks", () => {
assert.equal(treeNodeRowAction("index", false), "none");
});
test("column rows locate column in data grid", () => {
test("column rows locate column on single click and open structure editor on double click", () => {
assert.equal(treeNodeRowAction("column", false, "single"), "locate-column");
assert.equal(treeNodeRowAction("column", false, "double"), "none");
assert.equal(treeNodeRowDoubleClickAction("column", false, "double"), "locate-column");
assert.equal(treeNodeRowDoubleClickAction("column", false, "single"), "open-structure-editor");
assert.equal(treeNodeRowDoubleClickAction("column", false, "double"), "open-structure-editor");
assert.equal(treeNodeRowDoubleClickAction("index", false, "single"), "open-structure-editor");
assert.equal(treeNodeRowDoubleClickAction("index", false, "double"), "open-structure-editor");
});
test("maps source-capable sidebar nodes to object source kinds", () => {
@@ -152,7 +155,7 @@ test("double click does not open object browser for non-browsable rows", () => {
assert.equal(treeNodeRowDoubleClickAction("database", false), "none");
assert.equal(treeNodeRowDoubleClickAction("view", true), "none");
assert.equal(treeNodeRowDoubleClickAction("materialized_view", true), "none");
assert.equal(treeNodeRowDoubleClickAction("column", true), "none");
assert.equal(treeNodeRowDoubleClickAction("column", true), "open-structure-editor");
});
test("double click navigation mode copies the selected sidebar row name", () => {