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

This commit is contained in:
jean
2026-08-04 13:01:50 +08:00
committed by GitHub
parent 70db5be257
commit 44784d664c
19 changed files with 157 additions and 4 deletions
+30 -1
View File
@@ -215,7 +215,16 @@ import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { simpleDataGridOrderByMatchesSort, simpleDataGridOrderByReferencesMissingColumn, type DataGridSortDirection, type DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
import { buildOrderedGridRows, type GridInsertRowPosition, type GridNewRowPlacement } from "@/lib/dataGrid/gridNewRowPlacement";
import { DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH, isDataGridToolbarCompact, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAddRowCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar";
import {
DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH,
dataGridDeleteRowToolbarState,
isDataGridToolbarCompact,
type DataGridReloadIntent,
type DataGridToolbarActionCapability,
type DataGridToolbarAddRowCapability,
type DataGridToolbarAutoRefreshCapability,
type DataGridToolbarSaveCapability,
} from "@/lib/dataGrid/dataGridToolbar";
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
@@ -3416,6 +3425,25 @@ const addRowToolbarCapability = computed<DataGridToolbarAddRowCapability>(() =>
onTrigger: addRow,
onSelect: handleAddRowMenuSelect,
}));
const deleteRowToolbarTargetCount = computed(() => deletableRowIds(selectedOrCurrentRowIds()).length);
const deleteRowToolbarState = computed(() =>
dataGridDeleteRowToolbarState({
editable: !!props.editable,
canDeleteRows: canDeleteRows.value,
canDeleteExistingRows: canDeleteExistingRows.value,
deletableTargetCount: deleteRowToolbarTargetCount.value,
isSaving: isSaving.value,
}),
);
const deleteRowToolbarCapability = computed<DataGridToolbarActionCapability>(() => ({
label: deleteRowToolbarTargetCount.value > 1 ? t("grid.deleteRows", { count: deleteRowToolbarTargetCount.value }) : t("grid.deleteRow"),
tooltip: `${t("grid.deleteRow")} (${formatShortcut(settingsStore.editorSettings.shortcuts.deleteCurrentRow)})`,
visible: deleteRowToolbarState.value.visible,
disabled: deleteRowToolbarState.value.disabled,
onTrigger: () => {
deleteCurrentRow();
},
}));
const previewToolbarCapability = computed<DataGridToolbarActionCapability>(() => ({
label: t(previewLabelKey.value),
visible: saveToolbarState.value.showActions && pendingChangeCount.value > 0,
@@ -8479,6 +8507,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
:refresh="refreshToolbarCapability"
:auto-refresh="autoRefreshToolbarCapability"
:add-row="addRowToolbarCapability"
:delete-row="deleteRowToolbarCapability"
:preview="previewToolbarCapability"
:save="saveToolbarCapability"
:rollback="rollbackToolbarCapability"
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { Check, ChevronDown, Copy, Eye, Loader2, Plus, RefreshCcw, RotateCcw, Rows3, Save, TableProperties, Timer, Upload } from "@lucide/vue";
import { Check, ChevronDown, Copy, Eye, Loader2, Plus, RefreshCcw, RotateCcw, Rows3, Save, TableProperties, Timer, Trash2, Upload } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -28,6 +28,7 @@ const props = defineProps<{
refresh: DataGridToolbarActionCapability;
autoRefresh?: DataGridToolbarAutoRefreshCapability;
addRow?: DataGridToolbarAddRowCapability;
deleteRow?: DataGridToolbarActionCapability;
copyData?: DataGridToolbarCopyCapability;
exportData?: DataGridToolbarExportCapability;
transpose?: DataGridToolbarActionCapability;
@@ -148,6 +149,16 @@ function actionLabelClass() {
</DropdownMenu>
</div>
<Tooltip v-if="isDataGridToolbarCapabilityVisible(deleteRow)">
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(deleteRow)" @click="void triggerDataGridToolbarAction(deleteRow)">
<Trash2 class="data-grid-topbar-action-icon h-3 w-3" />
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ deleteRow?.label }}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ deleteRow?.tooltip ?? deleteRow?.label }}</TooltipContent>
</Tooltip>
<DropdownMenu v-if="isDataGridToolbarCapabilityVisible(exportData)">
<Tooltip>
<TooltipTrigger as-child>
@@ -10,7 +10,8 @@ import type { ObjectSourceKind, TableInfo, TableNameFilter, TreeNode, TreeNodeTy
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSidebarTreeToConnectedConnections, resolveSidebarFilterGuards, reuseLiveSidebarTreeNodes } from "@/lib/sidebar/sidebarSearchTree";
import { matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
import { buildTableTreeNodes } from "@/lib/table/tableTree";
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut";
import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick";
import { copyToClipboard } from "@/lib/common/clipboard";
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
@@ -1264,6 +1265,14 @@ function openSidebarDdl(node: TreeNode) {
sidebarDdlOpen.value = true;
}
function openSidebarDdlForSelection(): boolean {
const selectedNodeId = store.selectedTreeNodeId;
const node = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null;
if (!node || !sidebarNodeSupportsDdlView(node)) return false;
openSidebarDdl(node);
return true;
}
function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) {
if (!node.connectionId || !node.database || !objectSourceKindForTreeNode(node.type)) return;
const target = createSidebarActionTarget(node);
@@ -1530,6 +1539,13 @@ function onWindowKeydown(event: KeyboardEvent) {
}
return;
}
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isViewTableDdlShortcut(event, settingsStore.editorSettings.shortcuts)) {
if (openSidebarDdlForSelection()) {
event.preventDefault();
event.stopPropagation();
}
return;
}
}
if (!pointerInsideTree.value || isEditableSidebarTypeSearchTarget(event.target) || isEditableSidebarTypeSearchTarget(document.activeElement)) return;
+1
View File
@@ -5196,6 +5196,7 @@ export default {
shortcutPasteSidebarSelection: "Paste into sidebar",
shortcutEditSidebarConnection: "Edit sidebar connection",
shortcutOpenDataInNewTab: "Open data in new tab (mouse click)",
shortcutViewTableDdl: "View table DDL",
shortcutSendSelectionToAi: "Send selection to AI",
shortcutToggleFold: "Toggle fold",
shortcutScopeGlobal: "Global",
+1
View File
@@ -4933,6 +4933,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "Pegar en la barra lateral",
shortcutEditSidebarConnection: "Editar conexión de la barra lateral",
shortcutOpenDataInNewTab: "Abrir datos en una pestaña nueva (clic del ratón)",
shortcutViewTableDdl: "Ver DDL de la tabla",
shortcutSendSelectionToAi: "Enviar selección a IA",
shortcutToggleFold: "Contraer/expandir código",
shortcutScopeGlobal: "Global",
+1
View File
@@ -4933,6 +4933,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "Incolla nella barra laterale",
shortcutEditSidebarConnection: "Modifica connessione barra laterale",
shortcutOpenDataInNewTab: "Apri dati in una nuova scheda (clic del mouse)",
shortcutViewTableDdl: "Visualizza DDL tabella",
shortcutSendSelectionToAi: "Invia selezione ad AI",
shortcutToggleFold: "Comprimi/espandi codice",
shortcutScopeGlobal: "Globale",
+1
View File
@@ -4955,6 +4955,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "サイドバーに貼り付け",
shortcutEditSidebarConnection: "サイドバー接続を編集",
shortcutOpenDataInNewTab: "新しいデータタブで開く(マウスクリック)",
shortcutViewTableDdl: "テーブルDDLを表示",
shortcutSendSelectionToAi: "選択範囲をAIに送信",
shortcutToggleFold: "折りたたみの切り替え",
shortcutScopeGlobal: "グローバル",
+1
View File
@@ -4791,6 +4791,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "사이드바에 붙여넣기",
shortcutEditSidebarConnection: "사이드바 연결 편집",
shortcutOpenDataInNewTab: "새 탭에서 데이터 열기 (마우스 클릭)",
shortcutViewTableDdl: "테이블 DDL 보기",
shortcutSendSelectionToAi: "선택 영역을 AI로 보내기",
shortcutScopeGlobal: "전역",
shortcutScopeEditor: "SQL 편집기",
+1
View File
@@ -4935,6 +4935,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "Colar na barra lateral",
shortcutEditSidebarConnection: "Editar conexão da barra lateral",
shortcutOpenDataInNewTab: "Abrir dados em nova aba (clique do mouse)",
shortcutViewTableDdl: "Ver DDL da tabela",
shortcutSendSelectionToAi: "Enviar seleção para IA",
shortcutToggleFold: "Recolher/expandir código",
shortcutScopeGlobal: "Global",
+1
View File
@@ -5198,6 +5198,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "粘贴到侧边栏",
shortcutEditSidebarConnection: "编辑侧边栏连接",
shortcutOpenDataInNewTab: "在新数据标签页中打开(鼠标点击)",
shortcutViewTableDdl: "查看表 DDL",
shortcutSendSelectionToAi: "发送选中代码到 AI",
shortcutToggleFold: "切换折叠",
shortcutScopeGlobal: "全局",
+1
View File
@@ -4385,6 +4385,7 @@ export default withEnglishFallback({
shortcutPasteSidebarSelection: "貼到側邊欄",
shortcutEditSidebarConnection: "編輯側邊欄連線",
shortcutOpenDataInNewTab: "在新資料分頁中開啟(滑鼠點擊)",
shortcutViewTableDdl: "檢視資料表 DDL",
shortcutSendSelectionToAi: "傳送選取程式碼至 AI",
shortcutToggleFold: "切換折疊",
shortcutScopeGlobal: "全域",
@@ -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";
}