Repository navigation
Open
Labels
Description
Summary
Replace the localStorage draft flow for notes with Google Docs/Notion-style autosave: whatever the user types is persisted to the server, debounced. No more client-side drafts.
This spec was settled through a grilling session; the decisions below are final.
Behavior
Lazy create
/notes/newtouches no server until the first keystroke.- On first edit:
POST /rest/notes, capture the returned id, and switch the view to edit semantics. All further edits go out asPATCH /rest/notes/{id}. - A blank title is normalized server-side to
"Untitled"; a blank body is stored as''.
Autosave
- Scope: title + body only. URL and tags remain Save-button-only (tags create entities, URL has format validation).
- Debounce: 2s trailing with a 10s max-wait cap while typing continuously.
- Flush immediately on editor blur, component unmount, and Save click.
- Saves are serialized client-side: one in-flight save max, monotonic sequence numbers, stale responses discarded, pending edits coalesced.
Concurrency safety (future-proofing for multiple tabs/users)
NoteRequestgains an optionallastKnownUpdatefield (null = skip check, preserving contract compatibility).- PATCH compares it to the row's
last_update; mismatch →409 CONFLICT. No new column or migration:last_updateis the concurrency token. - On 409 the client silently GETs the latest note; if the user has pending edits it reapplies local title/body on top and retries with the fresh token, otherwise it adopts the server state.
- Known limitation (accepted, out of scope): two tabs both on
/notes/newtyping simultaneously create two separate "Untitled" notes; lazy create has no shared id. Solving this needs cross-tab coordination (BroadcastChannel/localStorage lock) and is explicitly deferred.
Backend validation changes
- Relax the shared validator (
NoteService.isNoteRequestValid) for both POST and PATCH: blank title and blank description are allowed; size caps stay. - Title null/blank → stored as
"Untitled". Description blank → stored as''. - No Flyway migration:
notes.descriptionstaysNOT NULL(empty string satisfies it). Title is already nullable. - The hardcoded
archived = falseinpatchNotestays as-is (archived notes cannot be edited anyway).
Status label
- Tiny muted text directly under the Markdown editor (above the Save/Cancel row).
- States: hidden when pristine →
Saving…→ persistentSaved HH:MMuntil the next edit. No spinner. - Errors are never shown in the label; the failed save is retried on the next attempt.
Buttons
- Save (both modes, kept): flush pending autosave (await in-flight save first; never double-write), then:
- note exists under the hood →
PATCH - user clicked New note and hit Save without typing →
POSTof a blank Untitled note - never both requests. Then navigate Home with
refreshHomeas today.
- note exists under the hood →
- Cancel: shown only in create mode, hidden in edit mode.
- Create mode, pristine (no keystroke): navigate Home, touch nothing, no cache invalidation.
- Create mode, dirty: await any in-flight save / pending create, then
DELETE /rest/notes/{id}?force=true, navigate Home with refetch. - Edit mode: no Cancel; autosaved edits simply persist (Notion-style). No snapshot/revert machinery.
Force delete
DELETE /rest/notes/{id}currently throwsNoteNotArchivedExceptionfor non-archived notes (NoteService.deleteNote).- Add optional query param
?force=truethat skips the archived check (archived check remains the default; existing trash flow untouched). One endpoint, backward compatible.
Draft machinery removal
- Remove the localStorage draft system for notes in
NoteAdd(saveDraft,applyDraft,clearDraft, draft keysdraft:note:new/draft:note:edit:{id}, the "Draft restored" banner,hasUserEdited). The server is now the draft.
Home cache
- Refetch Home iff the session mutated the server: Save, dirty-Cancel (note deleted), or edit mode with autosaved changes on exit. Pristine Cancel keeps today's cache reuse (Keep Home data when add forms are canceled #86).
Test plan
- Backend unit: relaxed validation (blank title/description accepted on POST and PATCH), Untitled normalization, 409 on stale
lastKnownUpdate,?force=truedelete bypassing the archive check (and default still enforced). - Backend integration (
*IntTest): patch conflict flow (stale token → 409 → refetch → retry succeeds); force-delete of a fresh note. - Frontend: debounce/coalescing (2s trailing, 10s cap), lazy-create-on-first-keystroke and id capture, label state transitions, Save flush semantics (PATCH vs POST, never both), dirty-Cancel awaits save then force-deletes, pristine-Cancel leaves cache untouched, draft removal (no localStorage writes).
Notes
addNote()currently discards the POST response; it must return the created note so the id can be captured.- Manual validation lives in
NoteService.isNoteRequestValid(no Bean Validation annotations onNoteRequest). - Effective DB body limit is 2000 chars (old
chk_max_lengthcheck) while app validation allows 50000; out of scope here, but worth a separate look.
Activity