feat(objects): add search clear buttons

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-18 22:04:38 +08:00
1 parent bba6df6242
commit aeba7d81ad
3 files changed
+62 -5

No files matched your search

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { ArrowDown, ArrowUp, Database, GripVertical, LayoutGrid, List, Loader2, RefreshCw, Search } from "@lucide/vue";
import { ArrowDown, ArrowUp, Database, GripVertical, LayoutGrid, List, Loader2, RefreshCw, Search, X } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -32,7 +32,7 @@ const props = defineProps<{
const { t } = useI18n();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const searchInput = ref<HTMLInputElement>();
const searchInput = ref<InstanceType<typeof Input>>();
const search = ref("");
const rows = ref<DatabaseRow[]>([]);
const loading = ref(false);
@@ -206,10 +206,15 @@ async function refresh(): Promise<boolean> {
}
function focusSearch(): boolean {
searchInput.value?.focus();
searchInput.value?.$el?.focus();
return true;
}
function clearDatabaseSearch() {
search.value = "";
searchInput.value?.$el?.focus();
}
watch(sortKeyOptions, (options) => {
if (!options.includes(sortKey.value)) {
sortKey.value = "name";
@@ -239,7 +244,10 @@ defineExpose({ focusSearch, refresh });
</span>
<div class="relative min-w-0 flex-1">
<Search class="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input ref="searchInput" v-model="search" class="h-7 pl-8 text-xs" :placeholder="t('databaseBrowser.search')" />
<Input ref="searchInput" v-model="search" class="h-7 pl-8 pr-6 text-xs" :placeholder="t('databaseBrowser.search')" />
<button v-if="search" type="button" class="absolute right-1.5 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="t('common.clear')" @click="clearDatabaseSearch">
<X class="h-3 w-3" />
</button>
</div>
<div class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
<select
@@ -2602,7 +2602,12 @@ function focusSearch(): boolean {
function onSearchKeydown(event: KeyboardEvent) {
if (!isCancelSearchShortcut(event)) return;
event.preventDefault();
clearObjectSearch();
}
function clearObjectSearch() {
search.value = "";
getSearchInput()?.focus();
}
defineExpose({ focusSearch, refresh });
@@ -2847,7 +2852,10 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<div class="flex min-w-0 flex-1 items-center gap-2">
<div class="relative min-w-0 flex-1">
<Search class="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input v-model="search" data-object-search-input class="h-7 pl-8 text-xs" :placeholder="t('objects.search')" @keydown="onSearchKeydown" />
<Input v-model="search" data-object-search-input class="h-7 pl-8 pr-6 text-xs" :placeholder="t('objects.search')" @keydown="onSearchKeydown" />
<button v-if="search" type="button" class="absolute right-1.5 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="t('common.clear')" @click="clearObjectSearch">
<X class="h-3 w-3" />
</button>
</div>
<div v-if="showObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
<button
@@ -0,0 +1,41 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
const databaseBrowserSource = readFileSync(new URL("../DatabaseBrowser.vue", import.meta.url), "utf8");
function functionBody(source: string, name: string): string {
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
if (!signature) throw new Error(`Missing function ${name}`);
const bodyStart = signature.index + signature[0].length;
let depth = 1;
for (let index = bodyStart; index < source.length; index += 1) {
if (source[index] === "{") depth += 1;
else if (source[index] === "}") depth -= 1;
if (depth === 0) return source.slice(bodyStart, index);
}
throw new Error(`Unclosed function ${name}`);
}
describe("database and object browser search clear controls", () => {
it("renders a clear button with input padding on both search surfaces", () => {
expect(objectBrowserSource).toContain('class="h-7 pl-8 pr-6 text-xs"');
expect(objectBrowserSource).toContain(':aria-label="t(\'common.clear\')" @click="clearObjectSearch"');
expect(objectBrowserSource).toContain('<button v-if="search" type="button"');
expect(databaseBrowserSource).toContain('class="h-7 pl-8 pr-6 text-xs"');
expect(databaseBrowserSource).toContain(':aria-label="t(\'common.clear\')" @click="clearDatabaseSearch"');
expect(databaseBrowserSource).toContain('<button v-if="search" type="button"');
});
it("clears and refocuses each search input", () => {
expect(functionBody(objectBrowserSource, "clearObjectSearch")).toContain('search.value = "";');
expect(functionBody(objectBrowserSource, "clearObjectSearch")).toContain("getSearchInput()?.focus();");
expect(functionBody(databaseBrowserSource, "clearDatabaseSearch")).toContain('search.value = "";');
expect(functionBody(databaseBrowserSource, "clearDatabaseSearch")).toContain("searchInput.value?.$el?.focus();");
});
it("keeps the ObjectBrowser keyboard clear path on the shared handler", () => {
expect(functionBody(objectBrowserSource, "onSearchKeydown")).toContain("clearObjectSearch();");
});
});