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:
@@ -3,6 +3,8 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useNotesStore, type Note, type NoteView } from "../stores/notes";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import { facetCount, facetsFromQuery, facetsToQuery } from "../notes/facets";
|
||||
import FilterBar from "../components/FilterBar.vue";
|
||||
import NoteCard from "../components/NoteCard.vue";
|
||||
import NoteEditor from "../components/NoteEditor.vue";
|
||||
|
||||
@@ -47,6 +49,11 @@ function viewForRoute(name: unknown): NoteView {
|
||||
const currentView = computed<NoteView>(() => viewForRoute(route.name));
|
||||
const currentLabel = computed<string | null>(() => (route.name === "label" ? String(route.params.id) : null));
|
||||
|
||||
// Facet filters live in the URL query on the board route (a filtered board = a lens).
|
||||
const facets = computed(() => facetsFromQuery(route.query));
|
||||
const facetKey = computed(() => JSON.stringify(facetsToQuery(facets.value))); // stable; ignores ?open=
|
||||
const filtered = computed(() => route.name === "board" && facetCount(facets.value) > 0);
|
||||
|
||||
// Quick-add + pinned/others split only on the main board (not archive/trash/label).
|
||||
const isMainBoard = computed(() => route.name === "board");
|
||||
const pinnedNotes = computed(() => notes.items.filter((n) => n.pinned));
|
||||
@@ -107,9 +114,10 @@ watch(
|
||||
if (focusedIndex.value >= len) focusedIndex.value = len - 1;
|
||||
},
|
||||
);
|
||||
watch([currentView, currentLabel], () => (focusedIndex.value = -1));
|
||||
watch([currentView, currentLabel, facetKey], () => (focusedIndex.value = -1));
|
||||
|
||||
const emptyState = computed(() => {
|
||||
if (filtered.value) return { title: "No notes match these filters", subtitle: "Try clearing or loosening a facet." };
|
||||
if (currentView.value === "trash") return { title: "Trash is empty", subtitle: "Notes you delete land here first." };
|
||||
if (currentView.value === "archived")
|
||||
return { title: "Nothing archived", subtitle: "Archived notes are tucked away here." };
|
||||
@@ -120,7 +128,7 @@ const emptyState = computed(() => {
|
||||
async function reload() {
|
||||
loadError.value = "";
|
||||
try {
|
||||
await notes.load(currentView.value, currentLabel.value);
|
||||
await notes.load(currentView.value, currentLabel.value, facets.value);
|
||||
} catch (e) {
|
||||
loadError.value = (e as { error?: string }).error ?? "Couldn't load your notes.";
|
||||
}
|
||||
@@ -131,7 +139,7 @@ onMounted(() => {
|
||||
window.addEventListener("keydown", onBoardKey);
|
||||
});
|
||||
onBeforeUnmount(() => window.removeEventListener("keydown", onBoardKey));
|
||||
watch([currentView, currentLabel], reload);
|
||||
watch([currentView, currentLabel, facetKey], reload);
|
||||
|
||||
function openEditor(note: Note) {
|
||||
editing.value = note;
|
||||
@@ -174,7 +182,8 @@ async function onDrop(target: Note) {
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
||||
<NoteEditor v-if="isMainBoard" ref="composer" inline autofocus class="mb-8" />
|
||||
<NoteEditor v-if="isMainBoard" ref="composer" inline autofocus class="mb-6" />
|
||||
<FilterBar v-if="isMainBoard" />
|
||||
|
||||
<div v-if="notes.loading" class="py-24 text-center text-sm text-neutral-400">Loading…</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user