From 41ad09b05f1864f796497547d0ae161def9aab46 Mon Sep 17 00:00:00 2001 From: Aaron Gabriel Date: Sat, 18 Apr 2026 15:43:55 -0600 Subject: [PATCH] Add /new create flow and two-intent delete on note view/editor - /new route: path, optional summary, tag chip editor, CodeMirror content editor; POSTs to /api/notes and navigates to /notes/ - Delete button on NoteView + NoteEditor: modal gate requires typing the note path before the destructive button enables - Minimal toast store + Toaster for success/error feedback after create/delete - TagEditor extracted so /new and /edit share the same chip input - VaultClient gains createNote + deleteNote; useCreateNote/useDeleteNote invalidate notes/tags/vaultInfo and remove the deleted note's cache entry - Home and /notes empty-state "Create one" now links to /new Co-Authored-By: Claude Opus 4.7 --- src/app/App.tsx | 4 + src/app/routes/NoteEditor.tsx | 61 +------ src/app/routes/NoteNew.test.tsx | 196 ++++++++++++++++++++++ src/app/routes/NoteNew.tsx | 197 +++++++++++++++++++++++ src/app/routes/NoteView.tsx | 10 +- src/app/routes/Notes.tsx | 33 ++-- src/components/DeleteNoteButton.test.tsx | 165 +++++++++++++++++++ src/components/DeleteNoteButton.tsx | 137 ++++++++++++++++ src/components/TagEditor.tsx | 54 +++++++ src/components/Toaster.tsx | 49 ++++++ src/lib/toast/store.ts | 31 ++++ src/lib/vault/client.test.ts | 76 +++++++++ src/lib/vault/client.ts | 21 +++ src/lib/vault/queries.ts | 41 ++++- 14 files changed, 1000 insertions(+), 75 deletions(-) create mode 100644 src/app/routes/NoteNew.test.tsx create mode 100644 src/app/routes/NoteNew.tsx create mode 100644 src/components/DeleteNoteButton.test.tsx create mode 100644 src/components/DeleteNoteButton.tsx create mode 100644 src/components/TagEditor.tsx create mode 100644 src/components/Toaster.tsx create mode 100644 src/lib/toast/store.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index e0dae9c..60e2f89 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,9 +1,11 @@ import { Header } from "@/components/Header"; +import { Toaster } from "@/components/Toaster"; import { QueryProvider } from "@/providers/QueryProvider"; import { BrowserRouter, Route, Routes } from "react-router"; import { AddVault } from "./routes/AddVault"; import { Home } from "./routes/Home"; import { NoteEditor } from "./routes/NoteEditor"; +import { NoteNew } from "./routes/NoteNew"; import { NoteView } from "./routes/NoteView"; import { Notes } from "./routes/Notes"; import { OAuthCallback } from "./routes/OAuthCallback"; @@ -14,11 +16,13 @@ export function App() {
+
} /> } /> + } /> } /> } /> } /> diff --git a/src/app/routes/NoteEditor.tsx b/src/app/routes/NoteEditor.tsx index 863205f..f89ecc7 100644 --- a/src/app/routes/NoteEditor.tsx +++ b/src/app/routes/NoteEditor.tsx @@ -1,5 +1,7 @@ import { CodeMirrorEditor } from "@/components/CodeMirrorEditor"; +import { DeleteNoteButton } from "@/components/DeleteNoteButton"; import { MarkdownView, buildWikilinkResolver } from "@/components/MarkdownView"; +import { TagEditor, normalizeTag } from "@/components/TagEditor"; import { relativeTime } from "@/lib/time"; import { useNote, useUpdateNote, useVaultStore } from "@/lib/vault"; import { type UpdateNotePayload, VaultAuthError, VaultConflictError } from "@/lib/vault/client"; @@ -136,7 +138,7 @@ function EditorSurface({ note }: { note: Note }) { const pathChanged = draft.path !== baseline.path; const addTag = (raw: string) => { - const t = raw.trim().replace(/^#/, ""); + const t = normalizeTag(raw); if (!t) return; if (draft.tags.includes(t)) return; setDraft((d) => ({ ...d, tags: [...d.tags, t] })); @@ -169,6 +171,8 @@ function EditorSurface({ note }: { note: Note }) { )}
+ + - ))} - onInputChange(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - onAdd(input); - } else if (e.key === "Backspace" && input === "" && tags.length > 0) { - onRemove(tags[tags.length - 1]!); - } - }} - onBlur={() => { - if (input.trim()) onAdd(input); - }} - placeholder="add tag…" - className="min-w-24 flex-1 rounded-md border border-transparent bg-transparent px-1 py-0.5 text-xs text-fg focus:border-border focus:outline-none" - aria-label="Add tag" - /> -
- ); -} - function ConflictBanner({ conflict, onReload, diff --git a/src/app/routes/NoteNew.test.tsx b/src/app/routes/NoteNew.test.tsx new file mode 100644 index 0000000..5eee759 --- /dev/null +++ b/src/app/routes/NoteNew.test.tsx @@ -0,0 +1,196 @@ +import { NoteNew } from "@/app/routes/NoteNew"; +import { useToastStore } from "@/lib/toast/store"; +import { useVaultStore } from "@/lib/vault/store"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { MemoryRouter, Route, Routes } from "react-router"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/components/CodeMirrorEditor", () => ({ + CodeMirrorEditor: ({ + value, + onChange, + }: { value: string; onChange(next: string): void; onSave?(): void; onCancel?(): void }) => ( +