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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user