feat(desktop): add DDL shortcut and grid row delete action

This commit is contained in:
jean authored and GitHub committed 2026-08-04 13:01:50 +08:00
1 parent 70db5be257
commit 44784d664c
19 files changed
+157 -4

No files matched your search

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import {
DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH,
dataGridDeleteRowToolbarState,
dataGridToolbarCompactBreakpoint,
dataGridToolbarIntervalOptions,
isDataGridToolbarCompact,
@@ -32,6 +33,17 @@ function autoRefreshCapability(overrides: Partial<DataGridToolbarAutoRefreshCapa
}
describe("data grid toolbar capabilities", () => {
it("shows delete only for editable grids with a supported deletion path", () => {
expect(dataGridDeleteRowToolbarState({ editable: false, canDeleteRows: true, canDeleteExistingRows: true, deletableTargetCount: 1, isSaving: false })).toEqual({ visible: false, disabled: false });
expect(dataGridDeleteRowToolbarState({ editable: true, canDeleteRows: true, canDeleteExistingRows: true, deletableTargetCount: 0, isSaving: false })).toEqual({ visible: true, disabled: true });
expect(dataGridDeleteRowToolbarState({ editable: true, canDeleteRows: true, canDeleteExistingRows: false, deletableTargetCount: 0, isSaving: false })).toEqual({ visible: false, disabled: true });
});
it("keeps unsaved deletable rows available when existing-row deletion is unsupported", () => {
expect(dataGridDeleteRowToolbarState({ editable: true, canDeleteRows: true, canDeleteExistingRows: false, deletableTargetCount: 1, isSaving: false })).toEqual({ visible: true, disabled: false });
expect(dataGridDeleteRowToolbarState({ editable: true, canDeleteRows: true, canDeleteExistingRows: true, deletableTargetCount: 2, isSaving: true })).toEqual({ visible: true, disabled: true });
});
it("uses a viewport-relative compact breakpoint within stable bounds", () => {
expect(dataGridToolbarCompactBreakpoint(1920)).toBe(1050);
expect(dataGridToolbarCompactBreakpoint(1280)).toBe(960);
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
const dataGridToolbarSource = readFileSync(new URL("../../../components/grid/DataGridToolbar.vue", import.meta.url), "utf8");
const dataGridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
describe("data grid toolbar delete row", () => {
it("declares and renders the delete-row action button", () => {
expect(dataGridToolbarSource).toMatch(/deleteRow\?: DataGridToolbarActionCapability/);
expect(dataGridToolbarSource).toMatch(/isDataGridToolbarCapabilityVisible\(deleteRow\)/);
expect(dataGridToolbarSource).toMatch(/triggerDataGridToolbarAction\(deleteRow\)/);
});
it("wires the delete-row capability into the toolbar", () => {
expect(dataGridSource).toMatch(/:delete-row="deleteRowToolbarCapability"/);
});
it("keeps the delete-row capability tied to row deletion state", () => {
expect(dataGridSource).toMatch(/deleteRowToolbarTargetCount = computed\(\(\) => deletableRowIds\(selectedOrCurrentRowIds\(\)\)\.length\)/);
expect(dataGridSource).toMatch(/visible: deleteRowToolbarState\.value\.visible/);
expect(dataGridSource).toMatch(/disabled: deleteRowToolbarState\.value\.disabled/);
expect(dataGridSource).toMatch(/onTrigger: \(\) => \{\s*deleteCurrentRow\(\);/);
});
});
@@ -21,7 +21,7 @@ describe("shortcutRegistry editor actions", () => {
"exPasteSqlInCondition",
"toggleFold",
];
const sidebarShortcutActionIds: ShortcutActionId[] = ["copySidebarSelection", "pasteSidebarSelection", "editSidebarConnection"];
const sidebarShortcutActionIds: ShortcutActionId[] = ["copySidebarSelection", "pasteSidebarSelection", "editSidebarConnection", "viewTableDdl"];
it("registers the new-data-tab mouse modifier as a configurable sidebar shortcut", () => {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "openDataInNewTab");
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import type { TreeNode } from "@/types/database";
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut";
function node(type: TreeNode["type"]): TreeNode {
return { id: "n", label: "n", type, connectionId: "c", database: "db" };
}
describe("sidebar tree DDL shortcut", () => {
it("supports table, view and materialized view nodes", () => {
expect(sidebarNodeSupportsDdlView(node("table"))).toBe(true);
expect(sidebarNodeSupportsDdlView(node("view"))).toBe(true);
expect(sidebarNodeSupportsDdlView(node("materialized_view"))).toBe(true);
});
it("rejects nodes without a DDL view", () => {
expect(sidebarNodeSupportsDdlView(node("connection"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("database"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("schema"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("column"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("index"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("procedure"))).toBe(false);
expect(sidebarNodeSupportsDdlView(node("group"))).toBe(false);
});
});
@@ -25,6 +25,14 @@ export interface DataGridToolbarActionCapability {
onTrigger: () => void | Promise<void>;
}
export function dataGridDeleteRowToolbarState(options: { editable: boolean; canDeleteRows: boolean; canDeleteExistingRows: boolean; deletableTargetCount: number; isSaving: boolean }): { visible: boolean; disabled: boolean } {
const deletionAvailable = options.editable && options.canDeleteRows;
return {
visible: deletionAvailable && (options.canDeleteExistingRows || options.deletableTargetCount > 0),
disabled: options.isSaving || options.deletableTargetCount === 0,
};
}
export interface DataGridToolbarSaveCapability extends DataGridToolbarActionCapability {
pendingCount: number;
shortcutLabel?: string;
@@ -223,6 +223,10 @@ export function isEditSidebarConnectionShortcut(event: ShortcutLikeEvent, shortc
return matchesShortcut(event, actionShortcut("editSidebarConnection", shortcuts));
}
export function isViewTableDdlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("viewTableDdl", shortcuts));
}
export function isQuickOpenShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("quickOpen", shortcuts));
}
@@ -55,6 +55,7 @@ export type ShortcutActionId =
| "pasteSidebarSelection"
| "editSidebarConnection"
| "openDataInNewTab"
| "viewTableDdl"
| "sendSelectionToAi";
export type ShortcutScope = "global" | "editor" | "grid" | "search" | "sidebar";
@@ -409,6 +410,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
defaultShortcut: "Alt",
inputKind: "modifier-only",
},
{
id: "viewTableDdl",
labelKey: "settings.shortcutViewTableDdl",
scope: "sidebar",
defaultShortcut: "Shift+Mod+D",
},
{
id: "sendSelectionToAi",
labelKey: "settings.shortcutSendSelectionToAi",
@@ -0,0 +1,9 @@
import type { TreeNode } from "@/types/database";
/**
* 侧边栏中支持“查看 DDL”的节点类型,与右键菜单的
* “查看 DDL”菜单项可见条件保持一致。
*/
export function sidebarNodeSupportsDdlView(node: TreeNode): boolean {
return node.type === "table" || node.type === "view" || node.type === "materialized_view";
}