feat(objects): add search clear buttons
This commit is contained in:
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();");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user