From 18ca4d4db4a5e824e3133dc55e1b6e375a0be3d4 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 23 Jul 2026 19:40:37 -0400 Subject: [PATCH] =?UTF-8?q?S1:=20frontend=20infra=20=E2=80=94=20AsyncState?= =?UTF-8?q?/EmptyState=20+=20useNoteEditor,=20adopted=20in=20BoardView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M9 section S1, commit 4 (frontend). Introduce the shared UI primitives the 7 data views + 5 editor hosts were hand-rolling: - components/AsyncState.vue: the loading / error(+Retry) wrapper (emits `retry`). - components/EmptyState.vue: the centered title/subtitle "nothing here" block. - composables/useNoteEditor.ts: the editing/open/close/navigate glue every editor host duplicated, as one controller (onClose hook + local-list resolution for [[wiki-link]] navigation). BoardView adopts all three: its three hand-rolled loading/error/empty blocks collapse into + , and its editor glue into useNoteEditor. The other views + editor hosts adopt these in the Organize (S3) and Auth (S5) sections. Behavior-preserving. Frontend has no local typecheck (rule 10); CI's vue-tsc is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- frontend/src/components/AsyncState.vue | 27 ++++++++++ frontend/src/components/EmptyState.vue | 13 +++++ frontend/src/composables/useNoteEditor.ts | 33 ++++++++++++ frontend/src/views/BoardView.vue | 61 ++++++++--------------- 4 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 frontend/src/components/AsyncState.vue create mode 100644 frontend/src/components/EmptyState.vue create mode 100644 frontend/src/composables/useNoteEditor.ts diff --git a/frontend/src/components/AsyncState.vue b/frontend/src/components/AsyncState.vue new file mode 100644 index 0000000..864d5af --- /dev/null +++ b/frontend/src/components/AsyncState.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/components/EmptyState.vue b/frontend/src/components/EmptyState.vue new file mode 100644 index 0000000..9c5f8ac --- /dev/null +++ b/frontend/src/components/EmptyState.vue @@ -0,0 +1,13 @@ + + + diff --git a/frontend/src/composables/useNoteEditor.ts b/frontend/src/composables/useNoteEditor.ts new file mode 100644 index 0000000..30f6879 --- /dev/null +++ b/frontend/src/composables/useNoteEditor.ts @@ -0,0 +1,33 @@ +import { ref } from "vue"; +import { useNotesStore, type Note } from "../stores/notes"; + +// Shared controller for hosting the NoteEditor modal. Every view that opens the +// editor used to re-declare the same editing/open/close/navigate glue; this is the +// single implementation. +// +// - `onClose` lets a host clean up its own state (e.g. a board's compose flag). +// - `list` is the host's local note array, tried first when resolving a navigated +// [[wiki-link]] target before falling back to the store, then a fetch — so views +// that keep their own list (reminders, timeline, search, graph) still resolve +// locally without duplicating the lookup. +export function useNoteEditor(options: { onClose?: () => void; list?: () => Note[] } = {}) { + const notes = useNotesStore(); + const editing = ref(null); + + function open(note: Note): void { + editing.value = note; + } + + function close(): void { + editing.value = null; + options.onClose?.(); + } + + async function navigate(id: string): Promise { + const local = options.list?.() ?? notes.items; + const found = local.find((n) => n.id === id) ?? notes.items.find((n) => n.id === id) ?? (await notes.fetchOne(id)); + if (found) editing.value = found; + } + + return { editing, open, close, navigate }; +} diff --git a/frontend/src/views/BoardView.vue b/frontend/src/views/BoardView.vue index 6128467..18b1029 100644 --- a/frontend/src/views/BoardView.vue +++ b/frontend/src/views/BoardView.vue @@ -4,6 +4,9 @@ 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 { useNoteEditor } from "../composables/useNoteEditor"; +import AsyncState from "../components/AsyncState.vue"; +import EmptyState from "../components/EmptyState.vue"; import FilterBar from "../components/FilterBar.vue"; import NoteCard from "../components/NoteCard.vue"; import NoteEditor from "../components/NoteEditor.vue"; @@ -13,9 +16,14 @@ const route = useRoute(); const ui = useUiStore(); const router = useRouter(); -const editing = ref(null); const composing = ref(false); // compose modal open (a new, empty note) const loadError = ref(""); +// Shared editor controller (open/close/navigate glue lives in the composable). +const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } = useNoteEditor({ + onClose: () => { + composing.value = false; + }, +}); // Compose and edit are ONE surface: the "Take a note…" bar and the global `c` // shortcut both open the same modal editor with an empty note (editing = null). @@ -34,7 +42,7 @@ watch( ); async function openFromQuery(id: string) { const found = notes.items.find((n) => n.id === id) ?? (await notes.fetchOne(id)); - if (found) editing.value = found; + if (found) openEditor(found); const q = { ...route.query }; delete q.open; void router.replace({ query: q }); @@ -164,24 +172,6 @@ onBeforeUnmount(() => { }); watch([currentView, currentLabel, facetKey], reload); -function openEditor(note: Note) { - editing.value = note; -} -function closeEditor() { - editing.value = null; - composing.value = false; -} - -async function onNavigate(id: string) { - const found = notes.items.find((n) => n.id === id); - if (found) { - editing.value = found; - return; - } - const fetched = await notes.fetchOne(id); - if (fetched) editing.value = fetched; -} - const draggingId = ref(null); function onDragStart(note: Note) { draggingId.value = note.id; @@ -218,26 +208,14 @@ async function onDrop(target: Note) { -
Loading…
- -
-

Couldn't load your notes

-

{{ loadError }}

- -
- -
-

{{ emptyState.title }}

-

{{ emptyState.subtitle }}

-
- -