mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 09:23:26 +00:00
feat(desktop): add DDL shortcut and grid row delete action
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4955,6 +4955,7 @@ export default withEnglishFallback({
|
||||
shortcutPasteSidebarSelection: "サイドバーに貼り付け",
|
||||
shortcutEditSidebarConnection: "サイドバー接続を編集",
|
||||
shortcutOpenDataInNewTab: "新しいデータタブで開く(マウスクリック)",
|
||||
shortcutViewTableDdl: "テーブルDDLを表示",
|
||||
shortcutSendSelectionToAi: "選択範囲をAIに送信",
|
||||
shortcutToggleFold: "折りたたみの切り替え",
|
||||
shortcutScopeGlobal: "グローバル",
|
||||
|
||||
@@ -4791,6 +4791,7 @@ export default withEnglishFallback({
|
||||
shortcutPasteSidebarSelection: "사이드바에 붙여넣기",
|
||||
shortcutEditSidebarConnection: "사이드바 연결 편집",
|
||||
shortcutOpenDataInNewTab: "새 탭에서 데이터 열기 (마우스 클릭)",
|
||||
shortcutViewTableDdl: "테이블 DDL 보기",
|
||||
shortcutSendSelectionToAi: "선택 영역을 AI로 보내기",
|
||||
shortcutScopeGlobal: "전역",
|
||||
shortcutScopeEditor: "SQL 편집기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5198,6 +5198,7 @@ export default withEnglishFallback({
|
||||
shortcutPasteSidebarSelection: "粘贴到侧边栏",
|
||||
shortcutEditSidebarConnection: "编辑侧边栏连接",
|
||||
shortcutOpenDataInNewTab: "在新数据标签页中打开(鼠标点击)",
|
||||
shortcutViewTableDdl: "查看表 DDL",
|
||||
shortcutSendSelectionToAi: "发送选中代码到 AI",
|
||||
shortcutToggleFold: "切换折叠",
|
||||
shortcutScopeGlobal: "全局",
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user