M6 1902b: facet bar + saved views UI (frontend)
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:
@@ -0,0 +1,56 @@
|
||||
// Convert between the board's URL query and a NoteFacets object. The URL query IS the
|
||||
// filter state (so a lens is shareable/bookmarkable and a saved view is just a link).
|
||||
import type { LocationQuery, LocationQueryRaw } from "vue-router";
|
||||
import type { NoteFacets } from "../stores/notes";
|
||||
|
||||
function one(v: LocationQuery[string]): string | undefined {
|
||||
return (Array.isArray(v) ? v[0] : v) ?? undefined;
|
||||
}
|
||||
|
||||
export function facetsFromQuery(q: LocationQuery): NoteFacets {
|
||||
const rawLabel = q.label;
|
||||
const labels = (Array.isArray(rawLabel) ? rawLabel : [rawLabel]).filter(
|
||||
(x): x is string => typeof x === "string" && x.length > 0,
|
||||
);
|
||||
const f: NoteFacets = {};
|
||||
const text = one(q.q);
|
||||
if (text) f.q = text;
|
||||
const color = one(q.color);
|
||||
if (color) f.color = color;
|
||||
const kind = one(q.kind);
|
||||
if (kind === "text" || kind === "list") f.kind = kind;
|
||||
if (labels.length) f.label = labels;
|
||||
if (one(q.has_reminder) === "true") f.has_reminder = true;
|
||||
if (one(q.has_attachment) === "true") f.has_attachment = true;
|
||||
const after = one(q.created_after);
|
||||
if (after) f.created_after = after;
|
||||
const before = one(q.created_before);
|
||||
if (before) f.created_before = before;
|
||||
return f;
|
||||
}
|
||||
|
||||
export function facetsToQuery(f: NoteFacets): LocationQueryRaw {
|
||||
const q: LocationQueryRaw = {};
|
||||
if (f.q) q.q = f.q;
|
||||
if (f.color) q.color = f.color;
|
||||
if (f.kind) q.kind = f.kind;
|
||||
if (f.label?.length) q.label = f.label;
|
||||
if (f.has_reminder) q.has_reminder = "true";
|
||||
if (f.has_attachment) q.has_attachment = "true";
|
||||
if (f.created_after) q.created_after = f.created_after;
|
||||
if (f.created_before) q.created_before = f.created_before;
|
||||
return q;
|
||||
}
|
||||
|
||||
// How many facets are active (labels counted individually) — drives the "Filters (N)" badge.
|
||||
export function facetCount(f: NoteFacets): number {
|
||||
let n = 0;
|
||||
if (f.q) n++;
|
||||
if (f.color) n++;
|
||||
if (f.kind) n++;
|
||||
n += f.label?.length ?? 0;
|
||||
if (f.has_reminder) n++;
|
||||
if (f.has_attachment) n++;
|
||||
if (f.created_after || f.created_before) n++;
|
||||
return n;
|
||||
}
|
||||
Reference in New Issue
Block a user