From 590d3ff2f6032c3e1c6bbe59bd956f3aeaeb687b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 23 Jul 2026 21:36:24 -0400 Subject: [PATCH] M9 S3 (frontend): recall views adopt useNoteList + AsyncState/EmptyState The Search / Timeline / Reminders views each hand-rolled the same items+loading+error scaffold, retry button, and editor-host glue. - useNoteList(fetcher, fallbackError) (new): the load-a-note-list scaffold (items/loading/error + a load() that never leaves a half-state). Views supply just the fetcher; the refs drive . - SearchView / TimelineView / RemindersView: adopt useNoteList + useNoteEditor + /; drop the local list/loading/error refs, the duplicated retry blocks, and the notes.items-shadowing navigate glue. - GraphView: editor host now via useNoteEditor (openNode/closeEditor/onNavigate collapse to navigate); loading/error via . Its two empty states keep inline markup/buttons, so they stay custom (not forced into EmptyState). - reminders store: fetchReminders() is the single owner of /api/notes/reminders; both the background poll (check) and RemindersView read through it. Frontend-only; CI vue-tsc is the type gate (no local typecheck, rule 10). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- frontend/src/composables/useNoteList.ts | 38 +++++++ frontend/src/stores/reminders.ts | 10 +- frontend/src/views/GraphView.vue | 46 +++----- frontend/src/views/RemindersView.vue | 143 ++++++++++-------------- frontend/src/views/SearchView.vue | 87 ++++---------- frontend/src/views/TimelineView.vue | 100 ++++++----------- 6 files changed, 173 insertions(+), 251 deletions(-) create mode 100644 frontend/src/composables/useNoteList.ts diff --git a/frontend/src/composables/useNoteList.ts b/frontend/src/composables/useNoteList.ts new file mode 100644 index 0000000..5093eed --- /dev/null +++ b/frontend/src/composables/useNoteList.ts @@ -0,0 +1,38 @@ +import { ref, type Ref } from "vue"; +import type { Note } from "../stores/notes"; + +// The load-a-list-of-notes scaffold every recall view (Search, Timeline, Reminders) +// hand-rolled: an items ref + loading/error flags + a load() that fills them and never +// leaves a half-state. The view supplies just the fetcher; wire the returned refs into +// and pass `items` to /cards. +// +// `fetcher` returns the notes to show (or [] — e.g. an empty search box). A thrown +// ApiError's `.error` becomes the message; otherwise `fallbackError` is shown. +export function useNoteList( + fetcher: () => Promise, + fallbackError = "Couldn't load your notes.", +): { + items: Ref; + loading: Ref; + error: Ref; + load: () => Promise; +} { + const items = ref([]); + const loading = ref(false); + const error = ref(""); + + async function load(): Promise { + loading.value = true; + error.value = ""; + try { + items.value = await fetcher(); + } catch (e) { + error.value = (e as { error?: string }).error ?? fallbackError; + items.value = []; + } finally { + loading.value = false; + } + } + + return { items, loading, error, load }; +} diff --git a/frontend/src/stores/reminders.ts b/frontend/src/stores/reminders.ts index 95f3e5b..087f59b 100644 --- a/frontend/src/stores/reminders.ts +++ b/frontend/src/stores/reminders.ts @@ -20,11 +20,17 @@ export const useReminderStore = defineStore("reminders", () => { let timer: ReturnType | undefined; let primed = false; + // Single owner of the reminders endpoint — the RemindersView reads its list through + // this too, so the URL + response shape live in one place. + async function fetchReminders(): Promise { + return (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes; + } + async function check(): Promise { const ui = useUiStore(); let notes: Note[]; try { - notes = (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes; + notes = await fetchReminders(); } catch { return; } @@ -66,5 +72,5 @@ export const useReminderStore = defineStore("reminders", () => { osEnabled.value = (await Notification.requestPermission()) === "granted"; } - return { osSupported, osEnabled, start, stop, enableOs }; + return { osSupported, osEnabled, start, stop, enableOs, fetchReminders }; }); diff --git a/frontend/src/views/GraphView.vue b/frontend/src/views/GraphView.vue index cc58899..00821cd 100644 --- a/frontend/src/views/GraphView.vue +++ b/frontend/src/views/GraphView.vue @@ -2,8 +2,9 @@ import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { useRouter } from "vue-router"; import { api } from "../api/client"; -import { useNotesStore, type Note } from "../stores/notes"; import { NOTE_NODE_FILL, type NoteColor } from "../notes/colors"; +import { useNoteEditor } from "../composables/useNoteEditor"; +import AsyncState from "../components/AsyncState.vue"; import NoteEditor from "../components/NoteEditor.vue"; type NodeKind = "note" | "label"; @@ -29,12 +30,12 @@ const WIDTH = 1000; const HEIGHT = 700; const router = useRouter(); -const notes = useNotesStore(); const allNodes = ref([]); const edges = ref([]); const loading = ref(true); const error = ref(""); -const editing = ref(null); +// Editor host: resolve a node id against the store, else fetch (shared controller). +const { editing, close: closeEditor, navigate } = useNoteEditor(); // Unlinked notes float in the space by default — the graph is a gentle overview, // not a links-only surface. Label hubs are on by default so tags cluster notes. const showAll = ref(true); @@ -263,7 +264,7 @@ function clickNode(n: GNode) { void router.push(`/label/${n.labelId}`); return; } - void openNode(n); + void navigate(n.id); } function onWheel(e: WheelEvent) { @@ -293,18 +294,6 @@ function toggleLabels() { reheat(); } -async function openNode(n: GNode) { - const found = notes.items.find((x) => x.id === n.id); - editing.value = found ?? (await notes.fetchOne(n.id)); -} -function closeEditor() { - editing.value = null; -} -async function onNavigate(id: string) { - const found = notes.items.find((x) => x.id === id); - editing.value = found ?? (await notes.fetchOne(id)); -} - onMounted(loadGraph); onBeforeUnmount(() => { cancelAnimationFrame(raf); @@ -336,21 +325,13 @@ onBeforeUnmount(() => { -
Loading graph…
- -
-

Couldn't load the graph

-

{{ error }}

- -
- -
+ +

No notes yet

Create notes and link them with @@ -430,9 +411,10 @@ onBeforeUnmount(() => {

+
diff --git a/frontend/src/views/RemindersView.vue b/frontend/src/views/RemindersView.vue index 04e847b..42f539c 100644 --- a/frontend/src/views/RemindersView.vue +++ b/frontend/src/views/RemindersView.vue @@ -1,42 +1,23 @@