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
+2 -2
View File
@@ -13,12 +13,12 @@ def test_clean_params_whitelists_facet_keys():
raw = {
"q": "hi",
"color": "yellow",
"labels": ["a"],
"label": ["a"],
"has_reminder": True,
"junk": 1,
"__proto__": 2,
}
assert clean_params(raw) == {"q": "hi", "color": "yellow", "labels": ["a"], "has_reminder": True}
assert clean_params(raw) == {"q": "hi", "color": "yellow", "label": ["a"], "has_reminder": True}
assert clean_params("nope") == {}
assert clean_params(None) == {}