mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(grid): add configurable shortcut to focus WHERE condition
This commit is contained in:
@@ -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"
|
||||
|
||||
+190
@@ -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);
|
||||
|
||||
+45
@@ -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;
|
||||
|
||||
@@ -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ə",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -7842,6 +7842,7 @@ export default withEnglishFallback({
|
||||
shortcutEditTableStructure: "テーブル構造を編集",
|
||||
shortcutCopyCurrentRow: "現在のデータ行をコピー",
|
||||
shortcutDeleteCurrentRow: "現在のデータ行を削除",
|
||||
shortcutFocusWhere: "WHERE 条件入力欄にフォーカス",
|
||||
shortcutGoToColumn: "列へ移動",
|
||||
shortcutGoToFirstPage: "最初のページ",
|
||||
shortcutGoToPreviousPage: "前のページ",
|
||||
|
||||
@@ -7626,6 +7626,7 @@ export default withEnglishFallback({
|
||||
shortcutEditTableStructure: "테이블 구조 편집",
|
||||
shortcutCopyCurrentRow: "현재 데이터 행 복사",
|
||||
shortcutDeleteCurrentRow: "현재 데이터 행 삭제",
|
||||
shortcutFocusWhere: "WHERE 조건 입력란에 포커스",
|
||||
shortcutGoToColumn: "컬럼으로 이동",
|
||||
shortcutGoToFirstPage: "첫 페이지",
|
||||
shortcutGoToPreviousPage: "이전 페이지",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9117,6 +9117,7 @@ export default withEnglishFallback({
|
||||
shortcutEditTableStructure: "Изменить структуру таблицы",
|
||||
shortcutCopyCurrentRow: "Копировать текущую строку данных",
|
||||
shortcutDeleteCurrentRow: "Удалить текущую строку данных",
|
||||
shortcutFocusWhere: "Фокус на условии WHERE",
|
||||
shortcutGoToColumn: "Перейти к столбцу",
|
||||
shortcutGoToFirstPage: "Первая страница",
|
||||
shortcutGoToPreviousPage: "Предыдущая страница",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8683,6 +8683,7 @@ export default withEnglishFallback({
|
||||
shortcutEditTableStructure: "编辑表结构",
|
||||
shortcutCopyCurrentRow: "复制当前数据行",
|
||||
shortcutDeleteCurrentRow: "删除当前数据行",
|
||||
shortcutFocusWhere: "聚焦 WHERE 条件输入框",
|
||||
shortcutGoToColumn: "跳转列",
|
||||
shortcutGoToFirstPage: "第一页",
|
||||
shortcutGoToPreviousPage: "上一页",
|
||||
|
||||
@@ -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 输入框不可用时不生效。
|
||||
|
||||
### 全局和导航
|
||||
|
||||
| 默认键 | 操作 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user