Refactor NavigationMenuGlobalSearch: add input focus handling, enhance entity type selectors with checklists, and improve responsive layout.

This commit is contained in:
Jeppe Bundgaard
2026-03-12 21:46:53 +01:00
parent a9972c5203
commit 70bba3f77c
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { BButton, BCheckbox, BField, BInput, BModal, BSelect, BSidebar, BTag } from 'buefy';
import SessionUser from '@/components/session/token/SessionUser.vue';
@@ -70,6 +70,7 @@ const router = useRouter();
const isModalOpen = ref(false);
const method = ref<SearchMethod>('POST');
const query = ref('');
const queryInputRef = ref<unknown>(null);
const includeTypes = ref<EntityType[]>([]);
const excludeTypes = ref<EntityType[]>([]);
const includeAssociations = ref(true);
@@ -140,6 +141,30 @@ watch(grouped, (next) => {
}
});
const tryFocusAndSelectQuery = () => {
const buefyInput = queryInputRef.value as { $el?: HTMLElement; $refs?: { input?: HTMLInputElement } } | null;
const input = buefyInput?.$refs?.input
?? buefyInput?.$el?.querySelector('input')
?? null;
if (!input) return;
input.focus();
input.select();
};
const focusQueryInput = () => {
nextTick(() => {
tryFocusAndSelectQuery();
window.setTimeout(() => tryFocusAndSelectQuery(), 90);
});
};
watch(isModalOpen, (open) => {
if (!open) return;
activeSetting.value = null;
settingsOpen.value = false;
focusQueryInput();
});
const parseErr = (e: unknown) => {
const parsed = SessionUser.functions.parseErrorMessage(e);
if (typeof parsed === 'string') return parsed;
@@ -421,7 +446,7 @@ const asJson = (v: unknown) => {
<template v-if="!activeSetting">
<b-field grouped group-multiline>
<b-input v-model="query" placeholder="Search everything..." icon="search" icon-pack="fas" expanded @keyup.enter="runSearch(0)" />
<b-input ref="queryInputRef" v-model="query" placeholder="Search everything..." icon="search" icon-pack="fas" expanded @keyup.enter="runSearch(0)" />
<p class="control"><b-button type="is-link" icon-left="search" icon-pack="fas" :loading="loading" @click="runSearch(0)">Search</b-button></p>
</b-field>
@@ -516,7 +541,7 @@ const asJson = (v: unknown) => {
</div>
<div v-if="activeSetting === 'pagination'" class="sidebar-settings-section no-divider">
<b-field label="Search method" label-position="on-border" class="mb-2">
<b-field v-if="!canCacheManage" label="Search method" label-position="on-border" class="mb-2">
<b-select v-model="method" expanded>
<option value="POST">POST (JSON body)</option>
<option value="GET">GET (query params)</option>
@@ -530,17 +555,25 @@ const asJson = (v: unknown) => {
</div>
<div v-if="activeSetting === 'object-types'" class="sidebar-settings-section no-divider">
<b-field label="Include entity types" label-position="on-border">
<b-select v-model="includeTypes" expanded multiple>
<option v-for="t in includeTypeOptions" :key="`inc-${t}`" :value="t">{{ humanize(t) }}</option>
</b-select>
</b-field>
<div class="object-type-grid">
<div class="object-type-column">
<p class="menu-label mb-2">Include entity types</p>
<div class="object-type-checklist">
<b-field v-for="t in includeTypeOptions" :key="`inc-${t}`" class="mb-1">
<b-checkbox v-model="includeTypes" :native-value="t" size="is-medium">{{ humanize(t) }}</b-checkbox>
</b-field>
</div>
</div>
<b-field label="Exclude entity types" label-position="on-border">
<b-select v-model="excludeTypes" expanded multiple>
<option v-for="t in excludeTypeOptions" :key="`exc-${t}`" :value="t">{{ humanize(t) }}</option>
</b-select>
</b-field>
<div class="object-type-column">
<p class="menu-label mb-2">Exclude entity types</p>
<div class="object-type-checklist">
<b-field v-for="t in excludeTypeOptions" :key="`exc-${t}`" class="mb-1">
<b-checkbox v-model="excludeTypes" :native-value="t" size="is-medium">{{ humanize(t) }}</b-checkbox>
</b-field>
</div>
</div>
</div>
<div class="is-flex is-flex-wrap-wrap mt-2">
<b-button size="is-small" type="is-light" class="mr-2 mb-2" @click="includeTypes = [...ENTITY_TYPES]; excludeTypes = []">Include all</b-button>
@@ -574,11 +607,14 @@ const asJson = (v: unknown) => {
<p class="control"><b-button type="is-info" icon-left="redo" icon-pack="fas" :loading="cacheLoading" @click="rebuildCache">Queue rebuild</b-button></p>
</b-field>
<b-field v-if="cacheScope === 'types'" label="Rebuild types" label-position="on-border" class="mt-2">
<b-select v-model="cacheTypes" expanded multiple :disabled="cacheLoading">
<option v-for="t in ENTITY_TYPES" :key="`cache-${t}`" :value="t">{{ humanize(t) }}</option>
</b-select>
</b-field>
<div v-if="cacheScope === 'types'" class="mt-2">
<p class="menu-label mb-2">Rebuild types</p>
<div class="object-type-checklist">
<b-field v-for="t in ENTITY_TYPES" :key="`cache-${t}`" class="mb-1">
<b-checkbox v-model="cacheTypes" :native-value="t" size="is-medium" :disabled="cacheLoading">{{ humanize(t) }}</b-checkbox>
</b-field>
</div>
</div>
<p v-if="cacheMsg" class="help is-success mt-1">{{ cacheMsg }}</p>
<p v-if="cacheErr" class="help is-danger mt-1">{{ cacheErr }}</p>
@@ -639,6 +675,16 @@ const asJson = (v: unknown) => {
.sidebar-settings-section { border-top: 1px solid rgba(0, 0, 0, 0.08); margin-top: 8px; padding-top: 10px; }
.settings-right-panel { max-height: 68vh; overflow: auto; }
.no-divider { border-top: none; margin-top: 0; padding-top: 0; }
.object-type-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.object-type-column { min-width: 0; }
.object-type-checklist {
max-height: 320px;
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
padding: 8px;
background: #fff;
}
.search-modal-main-pane {
flex: 1;
padding: 16px;
@@ -661,4 +707,7 @@ const asJson = (v: unknown) => {
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 980px) {
.object-type-grid { grid-template-columns: 1fr; }
}
</style>