M6 1902b: facet bar + saved views UI (frontend)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 37s

The dead-simple facet bar over the board + saved views in the sidebar,
completing task 1902. Filter state lives in the URL query, so a filtered
board is a shareable lens and a saved view is just a link ("one space,
many lenses").

- notes/facets.ts: facetsFromQuery / facetsToQuery / facetCount helpers.
- FilterBar.vue (board only): a "Filters (N)" toggle expanding to text
  search + color swatches + label chips + has-reminder / has-attachment /
  Lists / Notes toggles + a created-date range; Clear + "Save view".
  Each control writes the URL query (router.replace).
- notes store: load(view, label, facets) builds the query; NoteFacets type
  + activeFacets; import reload preserves active facets.
- savedFilters store + sidebar "Views" section (each a query-link, delete
  on hover); loaded on mount.
- BoardView derives facets from the query, reloads on facet change (ignores
  ?open=), and shows a "no notes match these filters" empty state.
- Backend: saved-filter param whitelist keys on `label` (matches the
  repeatable ?label= query) so saved views keep their labels. New filter
  icon.

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:
2026-07-23 08:45:56 -04:00
co-authored by Claude Opus 4.8
parent 5fdc124c77
commit 5c045aed63
9 changed files with 382 additions and 11 deletions
+30 -4
View File
@@ -7,6 +7,19 @@ import type { NoteColor } from "../notes/colors";
export type NoteView = "active" | "archived" | "trash";
export type NoteKind = "text" | "list";
// Combinable facet filters for the board (mirrors the GET /api/notes query + a saved
// view's stored params). All optional; empty = the plain, unfiltered board.
export interface NoteFacets {
q?: string;
color?: string;
kind?: NoteKind;
label?: string[];
has_reminder?: boolean;
has_attachment?: boolean;
created_after?: string;
created_before?: string;
}
export interface NoteLabel {
id: string;
name: string;
@@ -77,6 +90,7 @@ export const useNotesStore = defineStore("notes", () => {
const loading = ref(false);
const view = ref<NoteView>("active");
const activeLabel = ref<string | null>(null);
const activeFacets = ref<NoteFacets>({});
function sortItems(): void {
items.value.sort((a, b) => {
@@ -106,13 +120,24 @@ export const useNotesStore = defineStore("notes", () => {
}
}
async function load(v: NoteView, labelId: string | null = null): Promise<void> {
async function load(v: NoteView, labelId: string | null = null, facets: NoteFacets = {}): Promise<void> {
view.value = v;
activeLabel.value = labelId;
activeFacets.value = facets;
loading.value = true;
try {
const query = labelId ? `/api/notes?filter=${v}&label=${labelId}` : `/api/notes?filter=${v}`;
const res = await api.get<{ notes: Note[] }>(query);
const params = new URLSearchParams();
params.set("filter", v);
if (labelId) params.append("label", labelId);
for (const id of facets.label ?? []) if (id) params.append("label", id);
if (facets.q) params.set("q", facets.q);
if (facets.color) params.set("color", facets.color);
if (facets.kind) params.set("kind", facets.kind);
if (facets.has_reminder) params.set("has_reminder", "true");
if (facets.has_attachment) params.set("has_attachment", "true");
if (facets.created_after) params.set("created_after", facets.created_after);
if (facets.created_before) params.set("created_before", facets.created_before);
const res = await api.get<{ notes: Note[] }>(`/api/notes?${params.toString()}`);
items.value = res.notes;
sortItems();
} finally {
@@ -206,7 +231,7 @@ export const useNotesStore = defineStore("notes", () => {
throw { error: message, status: resp.status };
}
// Refresh the current lens so imported notes appear (labels reloaded by caller).
await load(view.value, activeLabel.value);
await load(view.value, activeLabel.value, activeFacets.value);
return data as { source: string; imported: number; skipped: number };
}
@@ -267,6 +292,7 @@ export const useNotesStore = defineStore("notes", () => {
loading,
view,
activeLabel,
activeFacets,
load,
create,
setPinned,