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,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { api } from "../api/client";
|
||||
import type { NoteFacets } from "./notes";
|
||||
|
||||
// A named, saved facet combination (a 'view'/lens). `params` mirrors NoteFacets.
|
||||
export interface SavedFilter {
|
||||
id: string;
|
||||
name: string;
|
||||
params: NoteFacets;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export const useSavedFiltersStore = defineStore("savedFilters", () => {
|
||||
const items = ref<SavedFilter[]>([]);
|
||||
const loaded = ref(false);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
try {
|
||||
items.value = (await api.get<{ filters: SavedFilter[] }>("/api/saved-filters")).filters;
|
||||
} catch {
|
||||
// leave whatever we have
|
||||
} finally {
|
||||
loaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function create(name: string, params: NoteFacets): Promise<SavedFilter> {
|
||||
const sf = await api.post<SavedFilter>("/api/saved-filters", { name, params });
|
||||
items.value.push(sf);
|
||||
return sf;
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await api.del(`/api/saved-filters/${id}`);
|
||||
items.value = items.value.filter((f) => f.id !== id);
|
||||
}
|
||||
|
||||
async function rename(id: string, name: string): Promise<void> {
|
||||
const sf = await api.patch<SavedFilter>(`/api/saved-filters/${id}`, { name });
|
||||
const idx = items.value.findIndex((f) => f.id === id);
|
||||
if (idx >= 0) items.value[idx] = sf;
|
||||
}
|
||||
|
||||
return { items, loaded, load, create, remove, rename };
|
||||
});
|
||||
Reference in New Issue
Block a user