feat(notes): wire backend note search

This commit is contained in:
saravanakumardb1 2026-03-10 16:44:27 -07:00
parent 7bec4e864d
commit 4748ed32a6
3 changed files with 30 additions and 28 deletions

View File

@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import SearchPage from "./page"; import SearchPage from "./page";
const listNoteSummariesMock = vi.fn(); const searchNoteSummariesMock = vi.fn();
vi.mock("next/link", () => ({ vi.mock("next/link", () => ({
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => ( default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
@ -13,12 +13,12 @@ vi.mock("next/link", () => ({
})); }));
vi.mock("@/lib/notes-client", () => ({ vi.mock("@/lib/notes-client", () => ({
listNoteSummaries: () => listNoteSummariesMock(), searchNoteSummaries: () => searchNoteSummariesMock(),
})); }));
describe("SearchPage", () => { describe("SearchPage", () => {
it("renders an accessible search field, saved searches, and note links", async () => { it("renders an accessible search field, saved searches, and note links", async () => {
listNoteSummariesMock.mockResolvedValue([ searchNoteSummariesMock.mockResolvedValue([
{ {
id: "note-prd-cutline", id: "note-prd-cutline",
workspaceId: "workspace-product", workspaceId: "workspace-product",

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { listNoteSummaries } from "@/lib/notes-client"; import { searchNoteSummaries } from "@/lib/notes-client";
import type { NoteSummary } from "@/lib/types"; import type { NoteSummary } from "@/lib/types";
export default function SearchPage() { export default function SearchPage() {
@ -12,27 +12,21 @@ export default function SearchPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
void (async () => { const timeout = window.setTimeout(() => {
try { void (async () => {
setNotes(await listNoteSummaries()); try {
} catch (err) { setNotes(await searchNoteSummaries(query));
setError(err instanceof Error ? err.message : "Unable to load notes"); setError(null);
} } catch (err) {
})(); setError(err instanceof Error ? err.message : "Unable to load notes");
}, []); }
})();
}, 150);
const filteredNotes = useMemo(() => { return () => {
const normalized = query.trim().toLowerCase(); window.clearTimeout(timeout);
if (!normalized) { };
return notes; }, [query]);
}
return notes.filter((note) =>
note.title.toLowerCase().includes(normalized) ||
note.excerpt.toLowerCase().includes(normalized) ||
note.tags.some((tag) => tag.toLowerCase().includes(normalized)),
);
}, [notes, query]);
const savedViews = [ const savedViews = [
{ {
@ -57,7 +51,7 @@ export default function SearchPage() {
<AppShell <AppShell
title="Search" title="Search"
description="Lexical search, tag filtering, and retrieval entry points. Semantic ranking and explainability remain follow-up work." description="Lexical search, tag filtering, and retrieval entry points. Semantic ranking and explainability remain follow-up work."
actions={<div className="badge">Dense retrieval shell</div>} actions={<div className="badge">Backend-backed note search</div>}
> >
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--ml-space-4)" }}> <section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--ml-space-4)" }}>
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}> <aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
@ -93,11 +87,11 @@ export default function SearchPage() {
<span className="badge">workspace:all</span> <span className="badge">workspace:all</span>
<span className="badge">status:active</span> <span className="badge">status:active</span>
<span className="badge">source:manual+agent</span> <span className="badge">source:manual+agent</span>
<span className="badge">matched:title+tags</span> <span className="badge">matched:title+body</span>
</div> </div>
{error ? <div style={{ color: "var(--ml-text-secondary)" }}>{error}</div> : null} {error ? <div style={{ color: "var(--ml-text-secondary)" }}>{error}</div> : null}
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{filteredNotes.map((note) => ( {notes.map((note) => (
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.5fr) repeat(3, minmax(100px, auto))", gap: "var(--ml-space-3)", alignItems: "start" }}> <div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.5fr) repeat(3, minmax(100px, auto))", gap: "var(--ml-space-3)", alignItems: "start" }}>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}> <div style={{ display: "grid", gap: "var(--ml-space-2)" }}>

View File

@ -240,6 +240,14 @@ export async function listNoteSummaries(): Promise<NoteSummary[]> {
return response.items.map(toNoteSummary); return response.items.map(toNoteSummary);
} }
export async function searchNoteSummaries(query: string): Promise<NoteSummary[]> {
const api = createNotesApiClient();
const search = query.trim();
const path = search ? `/notes?search=${encodeURIComponent(search)}` : "/notes";
const response = await api.fetch<NoteListResponse>(path);
return response.items.map(toNoteSummary);
}
export async function listNotesForWorkspace(workspaceId: string): Promise<NoteSummary[]> { export async function listNotesForWorkspace(workspaceId: string): Promise<NoteSummary[]> {
const api = createNotesApiClient(); const api = createNotesApiClient();
const response = await api.fetch<NoteListResponse>(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`); const response = await api.fetch<NoteListResponse>(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`);