mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 16:28:46 +00:00
feat(grid): add configurable keyboard shortcuts for copy and delete current row
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user