M9 S3 (frontend): recall views adopt useNoteList + AsyncState/EmptyState
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 34s

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 <AsyncState>.
- SearchView / TimelineView / RemindersView: adopt useNoteList + useNoteEditor
  + <AsyncState>/<EmptyState>; 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 <AsyncState>. 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-23 21:36:24 -04:00
co-authored by Claude Opus 4.8
parent f1033da75e
commit 590d3ff2f6
6 changed files with 173 additions and 251 deletions
+14 -32
View File
@@ -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<GNode[]>([]);
const edges = ref<GEdge[]>([]);
const loading = ref(true);
const error = ref("");
const editing = ref<Note | null>(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(() => {
</div>
</div>
<div v-if="loading" class="py-24 text-center text-sm text-neutral-400">Loading graph…</div>
<div v-else-if="error" class="py-24 text-center">
<h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">Couldn't load the graph</h2>
<p class="mt-1 text-sm text-neutral-400">{{ error }}</p>
<button
type="button"
class="mt-3 rounded-md border border-neutral-300 px-3 py-1.5 text-sm hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:hover:bg-neutral-800"
@click="loadGraph"
>
Retry
</button>
</div>
<div v-else-if="allNodes.length === 0" class="py-24 text-center">
<AsyncState
:loading="loading"
:error="error || undefined"
error-title="Couldn't load the graph"
@retry="loadGraph"
>
<div v-if="allNodes.length === 0" class="py-24 text-center">
<h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">No notes yet</h2>
<p class="mt-1 text-sm text-neutral-400">
Create notes and link them with
@@ -430,9 +411,10 @@ onBeforeUnmount(() => {
</g>
</svg>
</div>
</AsyncState>
<template v-if="editing">
<NoteEditor :note="editing" @close="closeEditor" @navigate="onNavigate" />
<NoteEditor :note="editing" @close="closeEditor" @navigate="navigate" />
</template>
</div>
</template>