mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-02 08:30:35 +00:00
feat(shortcuts): add search focus shortcut
This commit is contained in:
@@ -72,6 +72,7 @@ import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
|
||||
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
|
||||
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
|
||||
import { displayCellValue, type CellValue } from "@/lib/cellValue";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useDataGridExport } from "@/composables/useDataGridExport";
|
||||
@@ -460,6 +461,15 @@ function navigateSuggestion(delta: number) {
|
||||
suggestionIndex.value = Math.min(Math.max(suggestionIndex.value + delta, 0), searchSuggestions.value.length - 1);
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = searchInputRef.value;
|
||||
if (!input) return false;
|
||||
input.focus();
|
||||
input.select();
|
||||
updateSuggestionPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
const PAIRS: Record<string, string> = { "'": "'", '"': '"', "(": ")" };
|
||||
|
||||
function onSearchKeydown(e: KeyboardEvent) {
|
||||
@@ -503,7 +513,7 @@ function onSearchKeydown(e: KeyboardEvent) {
|
||||
acceptSuggestion();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
if (isCancelSearchShortcut(e)) {
|
||||
e.preventDefault();
|
||||
dismissSuggestions();
|
||||
return;
|
||||
@@ -519,7 +529,8 @@ function onSearchKeydown(e: KeyboardEvent) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
if (isCancelSearchShortcut(e)) {
|
||||
e.preventDefault();
|
||||
searchText.value = "";
|
||||
}
|
||||
}
|
||||
@@ -1742,6 +1753,7 @@ defineExpose({
|
||||
onToolbarRollback,
|
||||
showDdl,
|
||||
toggleDdl,
|
||||
focusSearch,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Upload, Download, RefreshCw } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -27,6 +28,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const { toast } = useToast();
|
||||
const connectionTreeRef = ref<InstanceType<typeof ConnectionTree>>();
|
||||
|
||||
async function refreshTree() {
|
||||
try {
|
||||
@@ -35,6 +37,12 @@ async function refreshTree() {
|
||||
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
return connectionTreeRef.value?.focusSearch() ?? false;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,7 +94,7 @@ async function refreshTree() {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<ConnectionTree />
|
||||
<ConnectionTree ref="connectionTreeRef" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" />
|
||||
|
||||
@@ -58,6 +58,8 @@ const columnInfoColumns = ref<ColumnInfo[]>([]);
|
||||
const columnInfoLoading = ref(false);
|
||||
const columnInfoError = ref<string | undefined>(undefined);
|
||||
const dataGridRef = ref<InstanceType<typeof DataGrid>>();
|
||||
const redisKeyBrowserRef = ref<InstanceType<typeof RedisKeyBrowser>>();
|
||||
const objectBrowserRef = ref<InstanceType<typeof ObjectBrowser>>();
|
||||
|
||||
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => {
|
||||
switch (props.activeConnection?.db_type) {
|
||||
@@ -160,6 +162,15 @@ function onHandleCloseColumnPanel() {
|
||||
columnInfoColumns.value = [];
|
||||
columnInfoError.value = undefined;
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
if (props.activeTab.mode === "redis") return redisKeyBrowserRef.value?.focusSearch() ?? false;
|
||||
if (props.activeTab.mode === "objects") return objectBrowserRef.value?.focusSearch() ?? false;
|
||||
if (props.activeTab.mode === "query" && props.activeOutputView !== "result") return false;
|
||||
return dataGridRef.value?.focusSearch() ?? false;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -433,7 +444,12 @@ function onHandleCloseColumnPanel() {
|
||||
<!-- Redis mode: key browser -->
|
||||
<template v-else-if="activeTab.mode === 'redis'">
|
||||
<div class="flex-1 min-h-0">
|
||||
<RedisKeyBrowser :key="activeTab.id" :connection-id="activeTab.connectionId" :db="Number(activeTab.database)" />
|
||||
<RedisKeyBrowser
|
||||
ref="redisKeyBrowserRef"
|
||||
:key="activeTab.id"
|
||||
:connection-id="activeTab.connectionId"
|
||||
:db="Number(activeTab.database)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -452,6 +468,7 @@ function onHandleCloseColumnPanel() {
|
||||
<!-- Objects mode: virtualized database object browser -->
|
||||
<template v-else-if="activeTab.mode === 'objects' && activeConnection">
|
||||
<ObjectBrowser
|
||||
ref="objectBrowserRef"
|
||||
:key="`${activeTab.id}-${activeTab.objectBrowser?.schema || ''}`"
|
||||
:connection="activeConnection"
|
||||
:database="activeTab.database"
|
||||
|
||||
@@ -37,6 +37,7 @@ import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import type { SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
|
||||
type ObjectRow = {
|
||||
id: string;
|
||||
@@ -66,6 +67,7 @@ const queryStore = useQueryStore();
|
||||
const schemas = ref<string[]>([]);
|
||||
const selectedSchema = ref<string | undefined>(props.schema);
|
||||
const rows = ref<ObjectRow[]>([]);
|
||||
const rootRef = ref<HTMLElement>();
|
||||
const search = ref("");
|
||||
const objectFilter = ref<ObjectFilter>("all");
|
||||
const loadingSchemas = ref(false);
|
||||
@@ -411,6 +413,26 @@ function filterLabel(filter: ObjectFilter) {
|
||||
return `${t(key)} ${filterCount(filter)}`;
|
||||
}
|
||||
|
||||
function getSearchInput(): HTMLInputElement | null {
|
||||
return rootRef.value?.querySelector<HTMLInputElement>("[data-object-search-input]") ?? null;
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = getSearchInput();
|
||||
if (!input) return false;
|
||||
input.focus();
|
||||
input.select();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onSearchKeydown(event: KeyboardEvent) {
|
||||
if (!isCancelSearchShortcut(event)) return;
|
||||
event.preventDefault();
|
||||
search.value = "";
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
|
||||
watch(
|
||||
() => [props.connection.id, props.database, props.schema] as const,
|
||||
() => {
|
||||
@@ -422,7 +444,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col bg-background">
|
||||
<div ref="rootRef" class="flex h-full min-h-0 flex-col bg-background">
|
||||
<div class="flex h-10 shrink-0 items-center gap-2 border-b px-3">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Table2 class="h-4 w-4 text-muted-foreground" />
|
||||
@@ -435,7 +457,13 @@ watch(
|
||||
</div>
|
||||
<div class="flex min-w-[240px] flex-1 items-center gap-2">
|
||||
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<Input v-model="search" class="h-7 text-xs" :placeholder="t('objects.search')" />
|
||||
<Input
|
||||
v-model="search"
|
||||
data-object-search-input
|
||||
class="h-7 text-xs"
|
||||
:placeholder="t('objects.search')"
|
||||
@keydown="onSearchKeydown"
|
||||
/>
|
||||
<div v-if="showObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
|
||||
<button
|
||||
v-for="filter in objectFilters"
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
type RedisKeyTreeNode,
|
||||
} from "@/lib/redisKeyTree";
|
||||
import { classifyRedisCommandSafety } from "@/lib/redisCommandSafety";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -45,6 +46,7 @@ const flatKeys = ref<RedisKeyInfo[]>([]);
|
||||
const treeKeys = ref<RedisKeyTreeNode[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const rootRef = ref<HTMLElement>();
|
||||
const searchPattern = ref("*");
|
||||
const selectedKeyRaw = ref<string | null>(null);
|
||||
const hasMore = ref(false);
|
||||
@@ -317,27 +319,53 @@ function onSearchInput() {
|
||||
searchTimer = setTimeout(loadKeys, 400);
|
||||
}
|
||||
|
||||
function getSearchInput(): HTMLInputElement | null {
|
||||
return rootRef.value?.querySelector<HTMLInputElement>("[data-redis-search-input]") ?? null;
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = getSearchInput();
|
||||
if (!input) return false;
|
||||
input.focus();
|
||||
input.select();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onSearchKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
void loadKeys();
|
||||
return;
|
||||
}
|
||||
if (!isCancelSearchShortcut(event)) return;
|
||||
event.preventDefault();
|
||||
searchPattern.value = "*";
|
||||
void loadKeys();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
});
|
||||
|
||||
onMounted(loadKeys);
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Splitpanes class="h-full" horizontal>
|
||||
<!-- Key table (top) -->
|
||||
<Pane :size="selectedKey ? 50 : 100" :min-size="25">
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div ref="rootRef" class="h-full flex flex-col overflow-hidden">
|
||||
<!-- Toolbar -->
|
||||
<div class="h-9 flex items-center gap-1 px-2 border-b shrink-0">
|
||||
<Search class="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
||||
<Input
|
||||
v-model="searchPattern"
|
||||
data-redis-search-input
|
||||
class="h-6 text-xs border-0 shadow-none focus-visible:ring-0"
|
||||
:placeholder="t('redis.pattern')"
|
||||
@input="onSearchInput"
|
||||
@keydown.enter="loadKeys"
|
||||
@keydown="onSearchKeydown"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="loadKeys">
|
||||
<Loader2 v-if="loading" class="h-3 w-3 animate-spin" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { matchSidebarLabel } from "@/lib/sidebarSearch";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
import {
|
||||
SIDEBAR_TREE_ROW_HEIGHT,
|
||||
SIDEBAR_TREE_PRERENDER_COUNT,
|
||||
@@ -30,6 +31,7 @@ const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
const searchQuery = ref("");
|
||||
const deferredSearchQuery = ref("");
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
const selectedTypes = ref<string[]>([]);
|
||||
const searchCollapsedIds = ref<Set<string>>(new Set());
|
||||
let searchTimer: number | undefined;
|
||||
@@ -184,6 +186,22 @@ function onSearchToggle(node: TreeNode) {
|
||||
else next.delete(node.id);
|
||||
searchCollapsedIds.value = next;
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = searchInputRef.value;
|
||||
if (!input) return false;
|
||||
input.focus();
|
||||
input.select();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onSearchKeydown(event: KeyboardEvent) {
|
||||
if (!isCancelSearchShortcut(event)) return;
|
||||
event.preventDefault();
|
||||
searchQuery.value = "";
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -193,12 +211,14 @@ function onSearchToggle(node: TreeNode) {
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
class="w-full h-6 pl-7 pr-6 text-xs rounded border border-border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
:placeholder="t('grid.search')"
|
||||
@keydown="onSearchKeydown"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
|
||||
Reference in New Issue
Block a user