From 70339959759adb50d30a51ae412f9bfa548f062b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 23 Aug 2026 10:58:23 -0400 Subject: [PATCH] search is a facet on the board, not a place you go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator (note 2930): tags exist so you can *"filter during a search"*. The server has always been able to do that — `GET /api/notes` composes `?q=` with `?label=` and the rest into one AND-ed query. The frontend never reached it. The header search box navigated to `/search`, and that view called a DIFFERENT endpoint — `GET /api/notes/search?q=`, full text only, no facets at all. So the one screen you landed on when you searched was the one screen where you could not narrow by tag. Tag filtering lived on the board's FilterBar, which is where you weren't searching. Two search boxes, two endpoints, and only the hidden one did what tags are for. Now the header box writes `?q=` into the board's URL beside whatever labels are already there, and stays on the lens you're in — searching while looking at Trash searches Trash. The box READS from the URL rather than holding its own copy, so it stays in step with the Filters panel's Clear and with a saved view opened from the sidebar. Deleted: `SearchView.vue`, its route, `GET /api/notes/search`, `repo.notes.search` and both adapter implementations, and the `notes_search` Tauri command whose only caller was the adapter entry. FilterBar loses its own "Search text…" input — it was the same facet, hidden behind a collapsed panel, duplicating a box that is always on screen. Filters now does what its name says: narrowing. The header does searching. `core::store::search` STAYS. Android calls it through the FFI (`search_notes`) and has its own search surface — which has the same no-tag-filter gap the web just lost, and deserves the same fix on its own terms rather than as a rider here. --- desktop/src-tauri/src/commands/local.rs | 6 ---- desktop/src-tauri/src/lib.rs | 1 - frontend/src/adapters/local.ts | 3 +- frontend/src/adapters/repo.ts | 1 - frontend/src/adapters/rest.ts | 1 - frontend/src/components/AppShell.vue | 47 ++++++++++++++++++------ frontend/src/components/FilterBar.vue | 17 +-------- frontend/src/router/index.ts | 1 - frontend/src/views/SearchView.vue | 48 ------------------------- src/thoughtsync/notes/__init__.py | 29 ++------------- tests/test_notes.py | 2 +- 11 files changed, 42 insertions(+), 114 deletions(-) delete mode 100644 frontend/src/views/SearchView.vue diff --git a/desktop/src-tauri/src/commands/local.rs b/desktop/src-tauri/src/commands/local.rs index 36f108a..e2e4a29 100644 --- a/desktop/src-tauri/src/commands/local.rs +++ b/desktop/src-tauri/src/commands/local.rs @@ -190,12 +190,6 @@ pub fn notes_titles(db: State<'_, Db>) -> Result, String> { store::titles(&conn).map_err(|e| e.to_string()) } -#[tauri::command] -pub fn notes_search(q: String, db: State<'_, Db>) -> Result, String> { - let conn = db.0.lock().map_err(|e| e.to_string())?; - store::search(&conn, &q).map_err(|e| e.to_string()) -} - #[tauri::command] pub fn labels_list(db: State<'_, Db>) -> Result, String> { let conn = db.0.lock().map_err(|e| e.to_string())?; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0f7ca81..51dd52f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -119,7 +119,6 @@ pub fn run() { commands::local::notes_restore_revision, commands::local::notes_reminders, commands::local::notes_titles, - commands::local::notes_search, commands::local::labels_list, commands::local::labels_create, commands::local::labels_rename, diff --git a/frontend/src/adapters/local.ts b/frontend/src/adapters/local.ts index ed414af..64142a9 100644 --- a/frontend/src/adapters/local.ts +++ b/frontend/src/adapters/local.ts @@ -6,7 +6,7 @@ // parameters (e.g. labelIds -> label_ids). A few operations have no offline meaning // yet (account auth, device linking, attachment upload, URL unfurl, file import) — // those reject with a clear message rather than silently failing; the board, editor, -// capture, search, filters, labels, checklists and reminders all work fully offline. +// capture, filters, labels, checklists and reminders all work fully offline. import { invoke } from "../desktop/bridge"; import type { Note, NoteRevision } from "../stores/notes"; @@ -71,7 +71,6 @@ export const local: Repo = { restoreRevision: (id, revId) => invoke("notes_restore_revision", { id, revId }), reminders: () => invoke("notes_reminders"), titles: () => invoke("notes_titles"), - search: (q) => invoke("notes_search", { q }), }, savedFilters: { diff --git a/frontend/src/adapters/repo.ts b/frontend/src/adapters/repo.ts index bdc3ca3..3573952 100644 --- a/frontend/src/adapters/repo.ts +++ b/frontend/src/adapters/repo.ts @@ -111,7 +111,6 @@ export interface NotesRepo { restoreRevision(id: string, revId: string): Promise; reminders(): Promise; titles(): Promise; - search(q: string): Promise; } export interface SavedFiltersRepo { diff --git a/frontend/src/adapters/rest.ts b/frontend/src/adapters/rest.ts index 4b161a5..12efa73 100644 --- a/frontend/src/adapters/rest.ts +++ b/frontend/src/adapters/rest.ts @@ -99,7 +99,6 @@ export const rest: Repo = { restoreRevision: (id, revId) => api.post(`/api/notes/${id}/revisions/${revId}/restore`), reminders: async () => (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes, titles: async () => (await api.get<{ titles: TitleEntry[] }>("/api/notes/titles")).titles, - search: async (q) => (await api.get<{ notes: Note[] }>(`/api/notes/search?q=${encodeURIComponent(q)}`)).notes, }, savedFilters: { diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index 0932fee..9db9c1b 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -1,6 +1,6 @@ - - diff --git a/src/thoughtsync/notes/__init__.py b/src/thoughtsync/notes/__init__.py index 719a49f..82f8490 100644 --- a/src/thoughtsync/notes/__init__.py +++ b/src/thoughtsync/notes/__init__.py @@ -141,8 +141,9 @@ async def list_notes(): stmt = stmt.where(Note.created_at < before_dt) if query_text: # Full-text match over the note's name + body (generated tsvector, - # migrations 0005/0026), ranked — so the facet bar's text box searches, - # not just filters. + # migrations 0005/0026), ranked. This is the ONLY text search now: the + # separate facet-less `/search` route was removed because landing on it + # was the one place you could not also narrow by tag (note 2930). tsquery = func.websearch_to_tsquery("english", query_text) search_col = literal_column("notes.search_vector") stmt = stmt.where(search_col.op("@@")(tsquery)).order_by( @@ -156,30 +157,6 @@ async def list_notes(): return jsonify({"notes": await _serialize_notes(db, notes)}) -@bp.get("/search") -@login_required -async def search_notes(): - q = (request.args.get("q") or "").strip() - if not q: - return jsonify({"notes": []}) - async with session_scope() as db: - tsquery = func.websearch_to_tsquery("english", q) - # search_vector is a generated column (migration 0005), not mapped on the ORM. - search_col = literal_column("notes.search_vector") - stmt = ( - select(Note) - .where( - visible_to_user("note", Note.owner_id, Note.id, g.user_id), - Note.deleted_at.is_(None), - search_col.op("@@")(tsquery), - ) - .order_by(func.ts_rank(search_col, tsquery).desc(), Note.updated_at.desc()) - .limit(100) - ) - notes = (await db.scalars(stmt)).all() - return jsonify({"notes": await _serialize_notes(db, notes)}) - - @bp.get("/reminders") @login_required async def list_reminders(): diff --git a/tests/test_notes.py b/tests/test_notes.py index 33fa6d7..30b9181 100644 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -37,7 +37,7 @@ def test_all_note_routes_registered(app): expected = { f"notes.{name}" for name in ( - "list_notes", "search_notes", "list_reminders", "complete_reminder", + "list_notes", "list_reminders", "complete_reminder", "snooze_reminder", "export_notes", "import_notes", "list_titles", "reorder_notes", "create_note", "get_note", "update_note", "list_revisions", "restore_revision",