M2 search: Postgres FTS backend + top search bar + results
- Migration 0005: generated tsvector column (title A + body B) + GIN index on notes; GET /api/notes/search?q= (websearch_to_tsquery, ts_rank, ACL-scoped, excludes trash), labels merged into results. - Persistent AppShell layout (parent route + <RouterView> children) so the new top search box keeps focus across board/search/label navigation. - SearchView (debounced live search from the shell → /search?q=, results masonry, no-match empty state); BoardView/SearchView render inside the shared shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
@@ -14,6 +14,8 @@ const config = useConfigStore();
|
||||
const labels = useLabelsStore();
|
||||
|
||||
const managing = ref(false);
|
||||
const searchText = ref(typeof route.query.q === "string" ? route.query.q : "");
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
if (!labels.loaded) void labels.load();
|
||||
@@ -21,6 +23,24 @@ onMounted(() => {
|
||||
|
||||
const currentLabelId = computed(() => (route.name === "label" ? String(route.params.id) : null));
|
||||
|
||||
function onSearch(value: string) {
|
||||
searchText.value = value;
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
const q = searchText.value.trim();
|
||||
if (q) router.push({ name: "search", query: { q } });
|
||||
else if (route.name === "search") router.push("/");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// Clear the search box when navigating to a non-search view.
|
||||
watch(
|
||||
() => route.name,
|
||||
(name) => {
|
||||
if (name !== "search") searchText.value = "";
|
||||
},
|
||||
);
|
||||
|
||||
async function signOut() {
|
||||
await session.logout();
|
||||
await router.replace("/login");
|
||||
@@ -32,15 +52,27 @@ async function signOut() {
|
||||
<header
|
||||
class="sticky top-0 z-20 border-b border-neutral-200 bg-neutral-50/90 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-3 px-4 py-3">
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-brand text-sm font-black text-neutral-900">
|
||||
TS
|
||||
</div>
|
||||
<span class="hidden font-semibold sm:inline">{{ config.siteName }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="hidden text-sm text-neutral-500 sm:inline dark:text-neutral-400">{{
|
||||
|
||||
<div class="flex flex-1 justify-center">
|
||||
<input
|
||||
:value="searchText"
|
||||
type="search"
|
||||
placeholder="Search notes…"
|
||||
aria-label="Search notes"
|
||||
class="w-full max-w-md rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-900"
|
||||
@input="onSearch(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<span class="hidden text-sm text-neutral-500 md:inline dark:text-neutral-400">{{
|
||||
session.user?.display_name
|
||||
}}</span>
|
||||
<RouterLink
|
||||
@@ -98,7 +130,7 @@ async function signOut() {
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="min-w-0 flex-1"><slot /></main>
|
||||
<main class="min-w-0 flex-1"><RouterView /></main>
|
||||
</div>
|
||||
|
||||
<LabelsModal v-if="managing" @close="managing = false" />
|
||||
|
||||
Reference in New Issue
Block a user