feat(grid): add configurable keyboard shortcuts for copy and delete current row

This commit is contained in:
t8y2
2026-05-23 12:44:49 +08:00
parent 00fd206a57
commit cd5d1b928d
12 changed files with 141 additions and 19 deletions
@@ -3,6 +3,7 @@ import { ref, watch, shallowRef, computed } from "vue";
import type { EditorView as EditorViewType } from "@codemirror/view";
import { useI18n } from "vue-i18n";
import { CircleHelp, ExternalLink, Loader2, RefreshCw, Settings } from "lucide-vue-next";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
@@ -652,7 +653,7 @@ watch(
<template>
<Dialog :open="open" @update:open="(v: boolean) => emit('update:open', v)">
<DialogContent class="sm:max-w-[860px] max-h-[calc(100vh-80px)] overflow-hidden">
<DialogContent class="sm:max-w-[860px] h-[min(660px,calc(100vh-80px))] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Settings class="h-4 w-4" />
@@ -660,7 +661,7 @@ watch(
</DialogTitle>
</DialogHeader>
<div class="flex min-h-[520px] flex-col gap-3 overflow-hidden sm:flex-row">
<div class="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden sm:min-h-[520px] sm:flex-row">
<nav
class="settingsCategoryNav flex shrink-0 gap-1 overflow-x-auto border-b pb-3 sm:w-40 sm:flex-col sm:overflow-visible sm:border-b-0 sm:border-r sm:pb-0 sm:pr-3"
>
@@ -921,30 +922,40 @@ watch(
</div>
</section>
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex flex-col gap-5 py-2">
<div class="space-y-3">
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex flex-col gap-2 py-2">
<div class="overflow-hidden rounded-md border bg-background">
<div
v-for="definition in SHORTCUT_DEFINITIONS"
:key="definition.id"
class="grid gap-2 rounded-md border bg-background p-3 sm:grid-cols-[minmax(0,1fr)_220px] sm:items-center"
class="-mt-px grid gap-2 border-t border-border px-3 py-2 sm:first:mt-0 sm:first:border-t-0 sm:grid-cols-[minmax(0,1fr)_208px] sm:items-center"
>
<div class="min-w-0 space-y-1">
<Label>{{ t(definition.labelKey) }}</Label>
<p class="text-xs text-muted-foreground">
{{ t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`) }}
</p>
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-2">
<Label class="min-w-0 truncate leading-none">{{ t(definition.labelKey) }}</Label>
<Badge variant="outline" class="h-5 shrink-0 rounded-md px-1.5 text-[11px] text-muted-foreground">
{{
t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`)
}}
</Badge>
</div>
</div>
<div class="space-y-1.5">
<div class="space-y-1">
<div class="flex gap-2">
<Input
:model-value="formatShortcut(editShortcuts[definition.id])"
readonly
:aria-invalid="shortcutConflicts.includes(definition.id)"
:placeholder="t('settings.shortcutPressShortcut')"
class="font-mono"
class="h-9 font-mono"
@keydown="(event: KeyboardEvent) => onShortcutKeydown(definition.id, event)"
/>
<Button type="button" variant="outline" class="shrink-0" @click="resetShortcut(definition.id)">
<Button
type="button"
variant="outline"
size="sm"
class="h-9 shrink-0 px-3"
@click="resetShortcut(definition.id)"
>
{{ t("settings.reset") }}
</Button>
</div>
@@ -1263,7 +1274,7 @@ watch(
<DialogFooter
v-if="hasSettingsApplyFooter(activeSettingsTab as SettingsCategory)"
class="mx-0 mb-0 shrink-0 rounded-none border-t-0 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3"
>
<Button variant="outline" @click="resetDefaults">
{{ t("settings.resetDefaults") }}
@@ -1279,7 +1290,7 @@ watch(
<DialogFooter
v-else-if="activeSettingsTab === 'ai'"
class="mx-0 mb-0 shrink-0 rounded-none border-t-0 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3"
>
<div class="flex flex-1 items-center gap-2">
<Button
@@ -1313,7 +1324,7 @@ watch(
<DialogFooter
v-else-if="activeSettingsTab === 'security' && isWeb"
class="mx-0 mb-0 shrink-0 rounded-none border-t-0 bg-transparent px-0 pb-0 pt-3"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3"
>
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
@@ -1328,7 +1339,7 @@ watch(
<DialogFooter
v-else-if="activeSettingsTab === 'about'"
class="mx-0 mb-0 shrink-0 rounded-none border-t-0 bg-transparent px-0 pb-0 pt-3"
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3"
>
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
@@ -217,6 +217,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
},
{
key: shortcutToCodeMirrorKey(shortcuts.executeSql),
preventDefault: true,
run: () => {
if (view.value) emit("execute", executableSqlFromView(view.value));
return true;
@@ -224,6 +225,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
},
{
key: shortcutToCodeMirrorKey(shortcuts.saveSql),
preventDefault: true,
run: () => {
emit("save");
return true;
+49 -1
View File
@@ -121,7 +121,13 @@ import {
type DateTimeFormatterUnit,
} from "@/lib/columnFormatter";
import { temporalCellEditorKind, type TemporalCellEditorKind } from "@/lib/dataGridTemporalEditor";
import { isCancelSearchShortcut, isFocusSearchShortcut, isToggleTransposeShortcut } from "@/lib/keyboardShortcuts";
import {
isCancelSearchShortcut,
isCopyCurrentRowShortcut,
isDeleteCurrentRowShortcut,
isFocusSearchShortcut,
isToggleTransposeShortcut,
} from "@/lib/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter";
import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGridSaveUi";
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition } from "@/lib/dataGridColumnFilter";
@@ -2441,6 +2447,40 @@ function editSelectedCell(): boolean {
return true;
}
function selectedOrCurrentRowIds(): number[] {
const affected = affectedRowIds();
if (affected.length > 0) return affected;
const position = currentSelectedCellPosition();
if (!position) return [];
const item = displayItems.value[position.rowIndex];
return item ? [item.id] : [];
}
function copyCurrentRow(): boolean {
const rowIds = selectedOrCurrentRowIds();
if (rowIds.length === 0) return false;
if (rowIds.length === 1) {
cloneRow(rowIds[0]);
return true;
}
cloneRows(rowIds);
return true;
}
function deleteCurrentRow(): boolean {
const rowIds = selectedOrCurrentRowIds();
if (rowIds.length === 0) return false;
const deletableRowIds = rowIds.filter((rowId) => canDeleteRowItem(getRowItem(rowId)));
if (deletableRowIds.length === 0) return false;
if (deletableRowIds.length === 1) {
requestDeleteRow(deletableRowIds[0]);
return true;
}
requestDeleteRows(deletableRowIds);
return true;
}
function commitGridEdit() {
commitEdit();
nextTick(() => gridRef.value?.focus({ preventScroll: true }));
@@ -2453,6 +2493,14 @@ async function onGridKeydown(event: KeyboardEvent) {
return;
}
if (eventTargetAllowsNativeClipboard(event)) return;
if (isCopyCurrentRowShortcut(event, settingsStore.editorSettings.shortcuts) && copyCurrentRow()) {
event.preventDefault();
return;
}
if (isDeleteCurrentRowShortcut(event, settingsStore.editorSettings.shortcuts) && deleteCurrentRow()) {
event.preventDefault();
return;
}
if (isToggleTransposeShortcut(event, settingsStore.editorSettings.shortcuts) && toggleKeyboardTranspose()) {
event.preventDefault();
return;
+2
View File
@@ -1255,6 +1255,8 @@ export default {
redisScanPageSizeOption: "{count} keys",
shortcutExecuteSql: "Execute SQL",
shortcutSaveSql: "Save SQL",
shortcutCopyCurrentRow: "Copy current data row",
shortcutDeleteCurrentRow: "Delete current data row",
shortcutNewQuery: "New query",
shortcutCloseTab: "Close tab",
shortcutFocusSearch: "Focus search",
+2
View File
@@ -1154,6 +1154,8 @@ export default {
redisScanPageSizeOption: "{count} claves",
shortcutExecuteSql: "Ejecutar SQL",
shortcutSaveSql: "Guardar SQL",
shortcutCopyCurrentRow: "Copiar fila de datos actual",
shortcutDeleteCurrentRow: "Eliminar fila de datos actual",
shortcutNewQuery: "Nueva consulta",
shortcutCloseTab: "Cerrar pestaña",
shortcutFocusSearch: "Enfocar búsqueda",
+2
View File
@@ -1230,6 +1230,8 @@ export default {
redisScanPageSizeOption: "{count} 个 Key",
shortcutExecuteSql: "执行 SQL",
shortcutSaveSql: "保存 SQL",
shortcutCopyCurrentRow: "复制当前数据行",
shortcutDeleteCurrentRow: "删除当前数据行",
shortcutNewQuery: "新建查询",
shortcutCloseTab: "关闭标签页",
shortcutFocusSearch: "聚焦搜索",
@@ -102,6 +102,14 @@ export function isObjectSourceSaveShortcutTarget(
return !!target?.closest("[data-object-source-editor], [data-object-source-preview]");
}
export function isCopyCurrentRowShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("copyCurrentRow", shortcuts));
}
export function isDeleteCurrentRowShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("deleteCurrentRow", shortcuts));
}
export function isCancelSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("cancelSearch", shortcuts));
}
+14
View File
@@ -1,6 +1,8 @@
export type ShortcutActionId =
| "executeSql"
| "saveSql"
| "copyCurrentRow"
| "deleteCurrentRow"
| "newQuery"
| "closeTab"
| "focusSearch"
@@ -32,6 +34,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "editor",
defaultShortcut: "Mod+S",
},
{
id: "copyCurrentRow",
labelKey: "settings.shortcutCopyCurrentRow",
scope: "grid",
defaultShortcut: "Mod+D",
},
{
id: "deleteCurrentRow",
labelKey: "settings.shortcutDeleteCurrentRow",
scope: "grid",
defaultShortcut: "Delete",
},
{
id: "newQuery",
labelKey: "settings.shortcutNewQuery",