feat: add configurable find/replace shortcuts with Mod-R routing
Add find and replace shortcut definitions to the shortcut registry with Mod+F and Mod+R defaults. Route Mod-R through App → ContentArea to dispatch to either the query editor replace panel or data grid refresh based on focus context. Add isModRShortcut helper and tests.
This commit is contained in:
parent
20fcea82b4
commit
12abe9d0e0
|
|
@ -40,6 +40,7 @@ import {
|
|||
isCloseTabShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isModRShortcut,
|
||||
isNewQueryShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isRefreshDataShortcut,
|
||||
|
|
@ -608,6 +609,8 @@ function onAiRequestAutoExecuteSql(sql: string) {
|
|||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.defaultPrevented) return;
|
||||
|
||||
const shortcuts = settingsStore.editorSettings.shortcuts;
|
||||
|
||||
if (isFocusSearchShortcut(e, shortcuts)) {
|
||||
|
|
@ -659,6 +662,11 @@ function handleKeydown(e: KeyboardEvent) {
|
|||
tryExecute();
|
||||
return;
|
||||
}
|
||||
if (isModRShortcut(e) && e.target instanceof Element && contentAreaRef.value?.handleModRTarget(e.target)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (isDesktop && isBrowserReloadShortcut(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -725,7 +733,7 @@ onMounted(async () => {
|
|||
aiPanelReady.value = true;
|
||||
});
|
||||
applyTheme();
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
if (isDesktop) {
|
||||
document.addEventListener("contextmenu", handleContextMenu);
|
||||
|
|
@ -780,7 +788,7 @@ onUnmounted(() => {
|
|||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
}
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
document.removeEventListener("contextmenu", handleContextMenu);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import {
|
|||
isCopyCurrentRowShortcut,
|
||||
isDeleteCurrentRowShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isModRShortcut,
|
||||
isToggleTransposeShortcut,
|
||||
} from "@/lib/keyboardShortcuts";
|
||||
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter";
|
||||
|
|
@ -2874,12 +2875,24 @@ function commitGridEdit() {
|
|||
nextTick(() => gridRef.value?.focus({ preventScroll: true }));
|
||||
}
|
||||
|
||||
function openCellDetailSearch(): boolean {
|
||||
return getDetailEditor()?.openSearch() ?? false;
|
||||
}
|
||||
|
||||
async function onGridKeydown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
if (isFocusSearchShortcut(event)) {
|
||||
event.preventDefault();
|
||||
focusSearch();
|
||||
return;
|
||||
}
|
||||
if (isModRShortcut(event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
await onToolbarRefresh();
|
||||
return;
|
||||
}
|
||||
if (eventTargetAllowsNativeClipboard(event)) return;
|
||||
if (isCopyCurrentRowShortcut(event, settingsStore.editorSettings.shortcuts) && copyCurrentRow()) {
|
||||
event.preventDefault();
|
||||
|
|
@ -3564,12 +3577,14 @@ defineExpose({
|
|||
toggleColumnVisibility,
|
||||
showAllColumns,
|
||||
invertColumnVisibility,
|
||||
openCellDetailSearch,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="gridRef"
|
||||
data-grid-root
|
||||
class="h-full flex flex-col overflow-hidden outline-none"
|
||||
:style="gridStyle"
|
||||
tabindex="0"
|
||||
|
|
@ -5178,7 +5193,12 @@ defineExpose({
|
|||
@cancel="cancelDetailEdit"
|
||||
@commit="commitDetailEdit"
|
||||
/>
|
||||
<div v-else ref="detailsEditorContainer" class="w-full h-40 rounded border overflow-hidden" />
|
||||
<div
|
||||
v-else
|
||||
ref="detailsEditorContainer"
|
||||
data-cell-detail-editor-root
|
||||
class="w-full h-40 rounded border overflow-hidden"
|
||||
/>
|
||||
<div class="flex gap-1 mt-1">
|
||||
<Button size="sm" class="h-6 text-xs" @click="commitDetailEdit">
|
||||
{{ t("dangerDialog.confirm") }}
|
||||
|
|
@ -5278,6 +5298,7 @@ defineExpose({
|
|||
<div
|
||||
v-else
|
||||
ref="valueEditorContainer"
|
||||
data-cell-detail-editor-root
|
||||
class="min-h-0 flex-1 w-full rounded border overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import type { SqlFormatDialect } from "@/lib/sqlFormatter";
|
|||
type DataGridHandle = {
|
||||
onToolbarRefresh: () => Promise<void> | void;
|
||||
focusSearch: () => boolean;
|
||||
openCellDetailSearch: () => boolean;
|
||||
visibleColumnCount: number;
|
||||
displayableColumnCount: number;
|
||||
hiddenColumnCount: number;
|
||||
|
|
@ -256,7 +257,14 @@ function refreshData(): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData });
|
||||
function handleModRTarget(target: Element): boolean {
|
||||
if (target.closest("[data-query-editor-root]")) return queryEditorRef.value?.openReplace() ?? false;
|
||||
if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false;
|
||||
if (target.closest("[data-grid-root]")) return refreshData();
|
||||
return false;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -384,6 +392,7 @@ defineExpose({ focusSearch, refreshData });
|
|||
<template v-else>
|
||||
<DataGrid
|
||||
v-if="activeTab.result"
|
||||
ref="dataGridRef"
|
||||
:key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"
|
||||
:cache-key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"
|
||||
class="flex-1 min-h-0"
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export function isRefreshDataShortcut(event: ShortcutLikeEvent, shortcuts?: Part
|
|||
return matchesShortcut(event, actionShortcut("refreshData", shortcuts));
|
||||
}
|
||||
|
||||
export function isModRShortcut(event: ShortcutLikeEvent): boolean {
|
||||
return matchesShortcut(event, "Mod+R");
|
||||
}
|
||||
|
||||
export function isToggleTransposeShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("toggleTranspose", shortcuts));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ export type ShortcutActionId =
|
|||
| "newQuery"
|
||||
| "closeTab"
|
||||
| "focusSearch"
|
||||
| "find"
|
||||
| "replace"
|
||||
| "refreshData"
|
||||
| "toggleTranspose"
|
||||
| "cancelSearch";
|
||||
|
|
@ -71,6 +73,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
|||
scope: "global",
|
||||
defaultShortcut: "Mod+F",
|
||||
},
|
||||
{
|
||||
id: "find",
|
||||
labelKey: "settings.shortcutFind",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Mod+F",
|
||||
},
|
||||
{
|
||||
id: "replace",
|
||||
labelKey: "settings.shortcutReplace",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Mod+R",
|
||||
},
|
||||
{
|
||||
id: "refreshData",
|
||||
labelKey: "settings.shortcutRefreshData",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
isCloseTabShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isModRShortcut,
|
||||
isNewQueryShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isRefreshDataShortcut,
|
||||
|
|
@ -22,14 +23,14 @@ test("matches Cmd+Enter for SQL execution", () => {
|
|||
});
|
||||
|
||||
test("matches custom shortcut settings for SQL execution", () => {
|
||||
assert.equal(isExecuteSqlShortcut({ key: "Enter", metaKey: true }, { executeSql: "Shift+Mod+Enter" } as any), false);
|
||||
assert.equal(
|
||||
isExecuteSqlShortcut({ key: "Enter", metaKey: true }, { executeSql: "Shift+Mod+Enter" } as any),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isExecuteSqlShortcut({ key: "Enter", metaKey: true, shiftKey: true } as any, {
|
||||
executeSql: "Shift+Mod+Enter",
|
||||
} as any),
|
||||
isExecuteSqlShortcut(
|
||||
{ key: "Enter", metaKey: true, shiftKey: true } as any,
|
||||
{
|
||||
executeSql: "Shift+Mod+Enter",
|
||||
} as any,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
|
@ -111,6 +112,13 @@ test("detects browser reload shortcuts for desktop suppression", () => {
|
|||
assert.equal(isBrowserReloadShortcut({ key: "r", ctrlKey: true, isComposing: true }), false);
|
||||
});
|
||||
|
||||
test("matches Mod-R without shift or alt for scoped refresh and replace", () => {
|
||||
assert.equal(isModRShortcut({ key: "r", ctrlKey: true }), true);
|
||||
assert.equal(isModRShortcut({ key: "R", metaKey: true }), true);
|
||||
assert.equal(isModRShortcut({ key: "R", metaKey: true, shiftKey: true }), false);
|
||||
assert.equal(isModRShortcut({ key: "r", ctrlKey: true, altKey: true }), false);
|
||||
});
|
||||
|
||||
test("ignores focus search shortcut while composing", () => {
|
||||
assert.equal(isFocusSearchShortcut({ key: "f", ctrlKey: true, isComposing: true }), false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue