feat(grid): add configurable shortcut to focus WHERE condition

This commit is contained in:
关关
2026-09-30 01:20:31 +08:00
committed by GitHub
parent 530bb2bb52
commit 7bc85dfcb0
27 changed files with 325 additions and 0 deletions
+9
View File
@@ -111,6 +111,7 @@ import {
isExecuteSqlInNewResultTabShortcut,
isExecuteSqlShortcut,
isFocusSearchShortcut,
isFocusWhereShortcut,
isGoToColumnShortcut,
isModRShortcut,
handleTabHistoryNavigationShortcut,
@@ -3708,6 +3709,14 @@ async function handleKeydown(e: KeyboardEvent) {
const shortcuts = settingsStore.editorSettings.shortcuts;
if (showTabSwitcher.value) return;
if (isFocusWhereShortcut(e, shortcuts) && !showSettingsPage.value && !showPluginCenter.value && !showDriverStore.value) {
const target = e.target instanceof Element ? e.target : null;
if (!target?.closest('[role="dialog"], [role="alertdialog"]') && contentAreaRef.value?.focusWhere()) {
e.preventDefault();
e.stopPropagation();
return;
}
}
// Grid-scoped shortcuts normally win inside DataGrid. Keep that precedence
// for a data tab even when focus is in a sibling input, where the grid's
// local listener intentionally leaves native editing untouched.
@@ -202,6 +202,7 @@ import {
isDeleteCurrentRowShortcut,
isEditTableStructureShortcut,
isFocusSearchShortcut,
isFocusWhereShortcut,
isGoToColumnShortcut,
isGoToFirstPageShortcut,
isGoToLastPageShortcut,
@@ -1071,6 +1072,7 @@ const transposeScrollRef = ref<HTMLElement | { $el?: HTMLElement }>();
const transposeScrollLeft = ref(0);
const transposeViewportWidth = ref(0);
const { sortColumn: sortCol, sortColumnIndex: sortColIndex, sortDirection: sortDir, sortMode, setSort, clearSort } = useDataGridSort();
const queryControlsRef = ref<InstanceType<typeof DataGridQueryControls>>();
const searchBarRef = ref<{ focus: (select?: boolean) => void } | null>(null);
const replaceOpen = ref(false);
const replacementText = ref("");
@@ -1889,6 +1891,11 @@ function navigateSuggestion(delta: number) {
dataGridSearch.navigateSuggestion(delta);
}
function focusWhere(): boolean {
if (!canUseWhereSearch.value) return false;
return queryControlsRef.value?.focusWhere() ?? false;
}
function focusSearch(target: Element | null = null): boolean {
const tableInfoDrawer = target?.closest<HTMLElement>("[data-table-info-drawer]");
if (tableInfoDrawer) {
@@ -9464,6 +9471,12 @@ function openCellDetailSearch(): boolean {
async function onGridKeydown(event: KeyboardEvent) {
if (event.defaultPrevented) return;
if (isFocusWhereShortcut(event, settingsStore.editorSettings.shortcuts) && focusWhere()) {
event.preventDefault();
event.stopPropagation();
return;
}
const targetAllowsNativeClipboard = eventTargetAllowsNativeClipboard(event);
if (!targetAllowsNativeClipboard && props.context === "table-data" && canOpenTableStructureEditor.value && isEditTableStructureShortcut(event, settingsStore.editorSettings.shortcuts)) {
event.preventDefault();
@@ -11924,6 +11937,7 @@ defineExpose({
setMultiRowTranspose,
toggleMultiRowTranspose,
focusSearch,
focusWhere,
openGoToColumn,
visibleColumnCount,
displayableColumnCount,
@@ -12425,6 +12439,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
</template>
<template v-if="canShowWhereSearch">
<DataGridQueryControls
ref="queryControlsRef"
v-model:where-input="whereFilterInput"
v-model:order-by-input="orderByInput"
v-model:filter-builder-open="filterBuilderOpen"
@@ -76,6 +76,15 @@ const emit = defineEmits<{
const { t } = useI18n();
const containerRef = ref<HTMLDivElement>();
const whereEditorRef = ref<InstanceType<typeof DataGridConditionEditor>>();
function focusWhere(): boolean {
if (!props.canUseWhereSearch || !whereEditorRef.value) return false;
whereEditorRef.value.focus();
return true;
}
defineExpose({ focusWhere });
const filterBuilderRef = ref<InstanceType<typeof DataGridFilterBuilder>>();
const pendingFirstEmptyRuleColumnSearch = ref(false);
let openingFirstEmptyRuleColumnSearch = false;
@@ -262,6 +271,7 @@ onUnmounted(onResizeEnd);
<span v-if="filterButtonCount" class="absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-1 text-[9px] leading-none text-primary-foreground">{{ filterButtonCount }}</span>
</button>
<DataGridConditionEditor
ref="whereEditorRef"
:model-value="whereInput"
kind="where"
:columns="conditionColumns"
@@ -0,0 +1,190 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, markRaw, nextTick, type App, type PropType } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import type { QueryResult } from "@/types/database";
import { TooltipProvider } from "@/components/ui/tooltip";
vi.mock("@/composables/useDataGridColumnResize", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/composables/useDataGridColumnResize")>();
const { ref } = await import("vue");
return {
...actual,
useDataGridColumnResize: () => ({
initColumnWidths: vi.fn(),
onResizeStart: vi.fn(),
autoFitColumn: vi.fn(),
renderedColumnWidths: ref([120]),
totalWidth: ref(120),
columnVars: ref({ "--total-w": "120px" }),
getIsResizing: () => false,
}),
};
});
import DataGrid from "../DataGrid.vue";
import { useSettingsStore } from "@/stores/settingsStore";
const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
const RecycleScroller = defineComponent({
props: {
items: {
type: Array as PropType<unknown[]>,
default: () => [],
},
},
setup(props, { attrs, slots }) {
return () =>
h(
"div",
attrs,
props.items.map((item) => slots.default?.({ item })),
);
},
});
function mountGrid(options: { displayableColumns?: boolean; columns?: string[]; slots?: Record<string, () => ReturnType<typeof h>>; withTableMetadata?: boolean; context?: "table-data" | "results"; shortcut?: string; onExecuteSql?: () => void } = {}) {
const pinia = createPinia();
setActivePinia(pinia);
const settingsStore = useSettingsStore();
settingsStore.updateEditorSettings({
dataGridRenderMode: "canvas",
shortcuts: {
...settingsStore.editorSettings.shortcuts,
focusWhere: options.shortcut ?? "Mod+Shift+L",
},
});
const result = markRaw<QueryResult>({
columns: options.columns ?? ["id"],
rows: [Array.from({ length: (options.columns ?? ["id"]).length }, (_, index) => index + 1)],
affected_rows: 0,
execution_time_ms: 0,
hidden_column_indexes: options.displayableColumns === false ? [0] : undefined,
});
const host = document.createElement("div");
document.body.append(host);
const Root = defineComponent({
setup() {
return () =>
h(
TooltipProvider,
{ delayDuration: 0 },
{
default: () =>
h(
DataGrid,
{
result,
databaseType: "mysql",
context: options.context ?? "table-data",
onExecuteSql: options.onExecuteSql ?? vi.fn(),
...(options.withTableMetadata
? {
connectionId: "test-connection",
database: "test-database",
tableMeta: { tableName: "users", schema: "test-database", columns: [], primaryKeys: [] },
}
: {}),
},
options.slots,
),
},
);
},
});
const app = createApp(Root);
app.use(pinia);
app.use(i18n);
app.component("RecycleScroller", RecycleScroller);
app.mount(host);
const mounted = { app, host };
mountedApps.push(mounted);
return { ...mounted, settingsStore };
}
async function settle() {
await nextTick();
await Promise.resolve();
await nextTick();
}
function gridRoot(host: HTMLElement): HTMLElement {
const root = host.querySelector<HTMLElement>("[data-grid-root]");
if (!root) throw new Error("Data grid root not found");
return root;
}
function focusWhereEvent(target: HTMLElement, options: KeyboardEventInit = {}): KeyboardEvent {
const event = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, ctrlKey: true, shiftKey: true, key: "L", ...options });
target.dispatchEvent(event);
return event;
}
afterEach(() => {
vi.restoreAllMocks();
for (const { app, host } of mountedApps.splice(0)) {
app.unmount();
host.remove();
}
});
describe("DataGrid focus-WHERE shortcut", () => {
it.each(["single", "split"] as const)("focuses WHERE without changing or executing its draft in the %s toolbar", async (layout) => {
const execute = vi.fn();
const { host, settingsStore } = mountGrid({ withTableMetadata: true, onExecuteSql: execute });
settingsStore.updateEditorSettings({ dataGridToolbarLayout: layout });
await settle();
const where = host.querySelector<HTMLTextAreaElement>('textarea[aria-label="WHERE"]')!;
expect(where).not.toBeNull();
where.value = "id = 42";
where.dispatchEvent(new Event("input", { bubbles: true }));
await settle();
const root = gridRoot(host);
root.focus();
const bubbled = vi.fn();
host.addEventListener("keydown", bubbled);
const event = focusWhereEvent(root);
await settle();
expect(event.defaultPrevented).toBe(true);
expect(bubbled).not.toHaveBeenCalled();
expect(document.activeElement).toBe(where);
expect(where.value).toBe("id = 42");
expect(execute).not.toHaveBeenCalled();
});
it("moves focus from ORDER BY to WHERE", async () => {
const { host } = mountGrid({ withTableMetadata: true });
await settle();
const order = host.querySelector<HTMLTextAreaElement>('textarea[aria-label="ORDER BY"]')!;
order.focus();
expect(focusWhereEvent(order).defaultPrevented).toBe(true);
await settle();
expect(document.activeElement).toBe(host.querySelector('textarea[aria-label="WHERE"]'));
});
it.each([{ withTableMetadata: false }, { withTableMetadata: true, context: "results" as const }, { withTableMetadata: true, shortcut: "" }])("leaves the key untouched when WHERE is unavailable or the binding is cleared: %j", async (options) => {
const { host } = mountGrid(options);
await settle();
const root = gridRoot(host);
root.focus();
expect(focusWhereEvent(root).defaultPrevented).toBe(false);
await settle();
expect(document.activeElement).toBe(root);
});
it.each([{ isComposing: true }, { key: "J" }])("ignores composition and unmatched keys: %j", async (options) => {
const { host } = mountGrid({ withTableMetadata: true });
await settle();
const root = gridRoot(host);
root.focus();
expect(focusWhereEvent(root, options).defaultPrevented).toBe(false);
await settle();
expect(document.activeElement).toBe(root);
});
});
@@ -200,6 +200,7 @@ import { isAiRedisConsoleTarget, type AiConversationBinding } from "@/lib/ai/aiC
type DataGridHandle = DataGridColumnLayoutHandle & {
onToolbarRefresh: () => Promise<void> | void;
focusSearch: (target?: Element | null) => boolean;
focusWhere: () => boolean;
openGoToColumn: () => boolean;
openCellDetailSearch: () => boolean;
nullColumnsHidden: boolean;
@@ -1160,6 +1161,11 @@ function focusSearch(target: Element | null = null): boolean {
return dataGridRef.value?.focusSearch(target) ?? false;
}
function focusWhere(): boolean {
if (props.activeTab.mode !== "data") return false;
return dataGridRef.value?.focusWhere() ?? false;
}
function openGoToColumn(): boolean {
if (props.activeTab.mode !== "data") return false;
return dataGridRef.value?.openGoToColumn() ?? false;
@@ -1624,6 +1630,7 @@ function locateExecutionSummaryError(item: ExecutionSummaryItem) {
defineExpose({
focusSearch,
focusWhere,
openGoToColumn,
refreshData,
toggleResultsPane,
@@ -72,6 +72,7 @@ const activeSurfaceRef = ref<QueryEditorSurfaceHandle | null>(null);
defineExpose({
focusSearch: (target: Element | null = null) => activeSurfaceRef.value?.focusSearch(target) ?? false,
focusWhere: () => activeSurfaceRef.value?.focusWhere() ?? false,
openGoToColumn: () => activeSurfaceRef.value?.openGoToColumn() ?? false,
refreshData: () => activeSurfaceRef.value?.refreshData() ?? false,
toggleResultsPane: () => activeSurfaceRef.value?.toggleResultsPane() ?? false,
@@ -24,6 +24,7 @@ const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
defineExpose<QueryEditorSurfaceHandle>({
focusSearch: (target: Element | null = null) => contentAreaRef.value?.focusSearch(target) ?? false,
focusWhere: () => contentAreaRef.value?.focusWhere() ?? false,
openGoToColumn: () => contentAreaRef.value?.openGoToColumn() ?? false,
refreshData: () => contentAreaRef.value?.refreshData() ?? false,
toggleResultsPane: () => contentAreaRef.value?.toggleResultsPane() ?? false,
@@ -73,6 +73,7 @@ defineExpose({
const group = groupForElement(element) ?? activeEditorGroup();
return group?.focusSearch(element) ?? false;
},
focusWhere: () => activeEditorGroup()?.focusWhere() ?? false,
openGoToColumn: () => activeEditorGroup()?.openGoToColumn() ?? false,
refreshData: (target: Element | null = null) => {
const element = commandTargetElement(target);
@@ -18,6 +18,7 @@ vi.mock("splitpanes", () => ({
const editorPreviewCalls = vi.hoisted(() => [] as Array<{ tabId: string; range: unknown }>);
const editorFocusCalls = vi.hoisted(() => [] as Array<{ tabId: string; range: unknown }>);
const groupHandleModRCalls = vi.hoisted(() => [] as Element[]);
const groupFocusWhereCalls = vi.hoisted(() => [] as string[]);
const groupFocusSearchCalls = vi.hoisted(() => [] as Array<Element | null>);
const resultHandleModRCalls = vi.hoisted(() => [] as Element[]);
const resultFocusSearchCalls = vi.hoisted(() => [] as boolean[]);
@@ -41,6 +42,10 @@ vi.mock("@/components/layout/EditorGroup.vue", () => ({
groupHandleModRCalls.push(target);
return true;
},
focusWhere(this: { groupId: string }) {
groupFocusWhereCalls.push(this.groupId);
return true;
},
focusSearch(target?: Element | null) {
groupFocusSearchCalls.push(target ?? null);
return true;
@@ -157,6 +162,7 @@ describe("SqlEditorWorkspace mount contract", () => {
editorFocusCalls.length = 0;
groupHandleModRCalls.length = 0;
groupFocusSearchCalls.length = 0;
groupFocusWhereCalls.length = 0;
resultHandleModRCalls.length = 0;
resultFocusSearchCalls.length = 0;
groupExecutionCalls.capture.length = 0;
@@ -714,4 +720,43 @@ describe("SqlEditorWorkspace mount contract", () => {
app.unmount();
host.remove();
});
it("routes WHERE focus only to the active editor group after switching groups", async () => {
const store = useQueryStore();
store.tabs = [dataTab("data-a"), dataTab("data-b")];
store.activeTabId = "data-a";
store.groups = [
{ id: "g1", tabIds: ["data-a"], activeTabId: "data-a" },
{ id: "g2", tabIds: ["data-b"], activeTabId: "data-b" },
];
store.focusedGroupId = "g1";
store.orientation = "vertical";
store.sizes = [50, 50];
const host = createHost();
const app = createApp(SqlEditorWorkspace, {
activeTab: store.tabs[0],
activeOutputView: "result",
executableSql: "",
formatSqlRequest: null,
compressSqlRequest: null,
selectedSql: "",
cursorPos: 0,
blockDangerousRedisCommands: false,
});
app.use(pinia);
app.use(i18n);
const vm = app.mount(host) as unknown as { focusWhere: () => boolean };
try {
await nextTick();
expect(vm.focusWhere()).toBe(true);
expect(groupFocusWhereCalls).toEqual(["g1"]);
store.focusedGroupId = "g2";
store.activeTabId = "data-b";
await nextTick();
expect(vm.focusWhere()).toBe(true);
expect(groupFocusWhereCalls).toEqual(["g1", "g2"]);
} finally {
app.unmount();
host.remove();
}
});
});
@@ -12,6 +12,7 @@ export interface StatementRange {
export interface QueryEditorSurfaceHandle {
focusSearch(target?: Element | null): boolean;
focusWhere(): boolean;
openGoToColumn(): boolean;
refreshData(target?: Element | null): boolean;
toggleResultsPane(): boolean;
+1
View File
@@ -7881,6 +7881,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Cədvəlin quruluşunu redaktə et",
shortcutCopyCurrentRow: "Cari məlumat sətrini kopyala",
shortcutDeleteCurrentRow: "Cari məlumat sətrini sil",
shortcutFocusWhere: "WHERE şərtinə fokuslan",
shortcutGoToColumn: "Sütuna keç",
shortcutGoToFirstPage: "İlk səhifə",
shortcutGoToPreviousPage: "Əvvəlki səhifə",
+1
View File
@@ -8717,6 +8717,7 @@ export default {
shortcutEditTableStructure: "Edit table structure",
shortcutCopyCurrentRow: "Copy current data row",
shortcutDeleteCurrentRow: "Delete current data row",
shortcutFocusWhere: "Focus WHERE condition",
shortcutGoToColumn: "Go to column",
shortcutGoToFirstPage: "First page",
shortcutGoToPreviousPage: "Previous page",
+1
View File
@@ -7920,6 +7920,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Editar estructura de tabla",
shortcutCopyCurrentRow: "Copiar fila de datos actual",
shortcutDeleteCurrentRow: "Eliminar fila de datos actual",
shortcutFocusWhere: "Enfocar la condición WHERE",
shortcutGoToColumn: "Ir a la columna",
shortcutGoToFirstPage: "Primera página",
shortcutGoToPreviousPage: "Página anterior",
+1
View File
@@ -7845,6 +7845,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Modifica struttura tabella",
shortcutCopyCurrentRow: "Copia riga dati corrente",
shortcutDeleteCurrentRow: "Elimina riga dati corrente",
shortcutFocusWhere: "Attiva il campo della condizione WHERE",
shortcutGoToColumn: "Vai alla colonna",
shortcutGoToFirstPage: "Prima pagina",
shortcutGoToPreviousPage: "Pagina precedente",
+1
View File
@@ -7842,6 +7842,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "テーブル構造を編集",
shortcutCopyCurrentRow: "現在のデータ行をコピー",
shortcutDeleteCurrentRow: "現在のデータ行を削除",
shortcutFocusWhere: "WHERE 条件入力欄にフォーカス",
shortcutGoToColumn: "列へ移動",
shortcutGoToFirstPage: "最初のページ",
shortcutGoToPreviousPage: "前のページ",
+1
View File
@@ -7626,6 +7626,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "테이블 구조 편집",
shortcutCopyCurrentRow: "현재 데이터 행 복사",
shortcutDeleteCurrentRow: "현재 데이터 행 삭제",
shortcutFocusWhere: "WHERE 조건 입력란에 포커스",
shortcutGoToColumn: "컬럼으로 이동",
shortcutGoToFirstPage: "첫 페이지",
shortcutGoToPreviousPage: "이전 페이지",
+1
View File
@@ -7846,6 +7846,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Editar estrutura da tabela",
shortcutCopyCurrentRow: "Copiar linha de dados atual",
shortcutDeleteCurrentRow: "Excluir linha de dados atual",
shortcutFocusWhere: "Focar na condição WHERE",
shortcutGoToColumn: "Ir para a coluna",
shortcutGoToFirstPage: "Primeira página",
shortcutGoToPreviousPage: "Página anterior",
+1
View File
@@ -9117,6 +9117,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Изменить структуру таблицы",
shortcutCopyCurrentRow: "Копировать текущую строку данных",
shortcutDeleteCurrentRow: "Удалить текущую строку данных",
shortcutFocusWhere: "Фокус на условии WHERE",
shortcutGoToColumn: "Перейти к столбцу",
shortcutGoToFirstPage: "Первая страница",
shortcutGoToPreviousPage: "Предыдущая страница",
+1
View File
@@ -7780,6 +7780,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "Tablo yapısını düzenle",
shortcutCopyCurrentRow: "Geçerli veri satırını kopyala",
shortcutDeleteCurrentRow: "Geçerli veri satırını sil",
shortcutFocusWhere: "WHERE koşuluna odaklan",
shortcutGoToColumn: "Sütuna git",
shortcutGoToFirstPage: "İlk sayfa",
shortcutGoToPreviousPage: "Önceki sayfa",
+1
View File
@@ -8683,6 +8683,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "编辑表结构",
shortcutCopyCurrentRow: "复制当前数据行",
shortcutDeleteCurrentRow: "删除当前数据行",
shortcutFocusWhere: "聚焦 WHERE 条件输入框",
shortcutGoToColumn: "跳转列",
shortcutGoToFirstPage: "第一页",
shortcutGoToPreviousPage: "上一页",
+1
View File
@@ -7141,6 +7141,7 @@ export default withEnglishFallback({
shortcutEditTableStructure: "編輯資料表結構",
shortcutCopyCurrentRow: "複製目前資料列",
shortcutDeleteCurrentRow: "刪除目前資料列",
shortcutFocusWhere: "聚焦 WHERE 條件輸入框",
shortcutGoToColumn: "跳轉欄",
shortcutGoToFirstPage: "第一頁",
shortcutGoToPreviousPage: "上一頁",
@@ -6,6 +6,7 @@ import {
isEditTableStructureShortcut,
isExecuteSqlInNewResultTabShortcut,
isGoToColumnShortcut,
isFocusWhereShortcut,
isGoToFirstPageShortcut,
isGoToLastPageShortcut,
isGoToNextPageShortcut,
@@ -20,6 +21,14 @@ import { formatShortcutDisplay, isMacShortcutPlatform } from "@/lib/editor/short
import { shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
describe("keyboard shortcut matching", () => {
it("matches configured WHERE focus on macOS and Windows while respecting a cleared binding", () => {
expect(isFocusWhereShortcut({ key: "L", metaKey: true, shiftKey: true }, { focusWhere: "Mod+Shift+L" }, "MacIntel")).toBe(true);
expect(isFocusWhereShortcut({ key: "L", ctrlKey: true, shiftKey: true }, { focusWhere: "Mod+Shift+L" }, "Win32")).toBe(true);
expect(isFocusWhereShortcut({ key: "L", ctrlKey: true, shiftKey: true }, { focusWhere: "" }, "Win32")).toBe(false);
expect(isFocusWhereShortcut({ key: "L", ctrlKey: true, shiftKey: true }, undefined, "Win32")).toBe(false);
expect(isFocusWhereShortcut({ key: "L", metaKey: true, shiftKey: true }, undefined, "MacIntel")).toBe(false);
});
it("records modifier-only mouse shortcut settings", () => {
expect(eventToModifierOnlyShortcut({ key: "Alt", altKey: true })).toBe("Alt");
expect(eventToModifierOnlyShortcut({ key: "Shift", shiftKey: true })).toBe("Shift");
@@ -82,6 +82,16 @@ describe("shortcutRegistry editor actions", () => {
expect(findShortcutConflict("goToFirstPage", "Mod+F", shortcuts)).toBeNull();
});
it("leaves focus-WHERE unassigned for new and existing settings and detects grid conflicts", () => {
expect(DEFAULT_SHORTCUT_SETTINGS.focusWhere).toBe("");
expect(normalizeShortcutSettings().focusWhere).toBe("");
expect(normalizeShortcutSettings({ executeSql: "Mod+Enter" }).focusWhere).toBe("");
expect(normalizeShortcutSettings({ focusWhere: "" }).focusWhere).toBe("");
const shortcuts = normalizeShortcutSettings({ focusWhere: "Mod+D" });
expect(shortcuts.focusWhere).toBe("Mod+D");
expect(findShortcutConflict("focusWhere", shortcuts.focusWhere, shortcuts)).toBe("copyCurrentRow");
});
it("registers go to column as an unassigned grid shortcut", () => {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "goToColumn");
@@ -273,6 +273,10 @@ export function isDeleteCurrentRowShortcut(event: ShortcutLikeEvent, shortcuts?:
return matchesShortcut(event, actionShortcut("deleteCurrentRow", shortcuts));
}
export function isFocusWhereShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("focusWhere", shortcuts, platform), platform);
}
export function isGoToColumnShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("goToColumn", shortcuts, platform), platform);
}
@@ -35,6 +35,7 @@ export type ShortcutActionId =
| "editTableStructure"
| "copyCurrentRow"
| "deleteCurrentRow"
| "focusWhere"
| "goToColumn"
| "goToFirstPage"
| "goToPreviousPage"
@@ -355,6 +356,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "grid",
defaultShortcut: "Delete",
},
{
id: "focusWhere",
labelKey: "settings.shortcutFocusWhere",
scope: "grid",
defaultShortcut: "",
},
{
id: "goToColumn",
labelKey: "settings.shortcutGoToColumn",
@@ -42,6 +42,8 @@ DBX 的快捷键来自同一份可配置 action registry。**设置 → 快捷
普通复制/粘贴仍遵循平台标准按键,但“复制当前行”“删除当前行”和“切换转置”是 DBX 可配置 action。
「聚焦 WHERE 条件输入框」默认不绑定快捷键。可在 **设置 → 快捷键 → 数据表格** 中自行设置,按下已配置的快捷键后将光标移至当前表数据标签页的 WHERE 输入框,保留现有条件且不执行查询。WHERE 输入框不可用时不生效。
### 全局和导航
| 默认键 | 操作 |
+2
View File
@@ -42,6 +42,8 @@ The current registry has no default “execute all SQL” action. Select a range
Standard copy and paste still follow platform conventions. Copy Current Row, Delete Current Row, and Toggle Transpose are DBX-configurable actions.
**Focus WHERE condition** has no default shortcut. Assign your preferred binding in **Settings → Shortcuts → Data grid** to focus the WHERE input in the active table data tab. It keeps the current condition and does not execute a query. It does nothing when the WHERE input is unavailable.
### Global and Navigation
| Default | Action |