From be53b4b26ece8438673c2bd5978ba2076a262ffb Mon Sep 17 00:00:00 2001 From: Frederic Cabassut Date: Tue, 24 Mar 2026 15:18:19 +0100 Subject: [PATCH] fix: sync attachment changes to store and skip spurious auto-save Two issues fixed: 1. Attachment operations (upload, feature, remove) only updated the local editor state, not the notes store. Before auto-save, this was masked because saveAndClose always called updateNote, whose PATCH response refreshed the store. With auto-save skipping redundant saves, the store was never updated with attachment changes. Now attachment handlers update the store directly. 2. TiptapEditor fires onUpdate on init with round-tripped markdown that can differ from the original. Added an autoSaveReady guard to absorb this initial normalization as the baseline, preventing spurious saves when reopening existing notes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/components/NoteEditor.svelte | 31 +++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/lib/components/NoteEditor.svelte b/src/lib/components/NoteEditor.svelte index ba81099..1f79ed5 100644 --- a/src/lib/components/NoteEditor.svelte +++ b/src/lib/components/NoteEditor.svelte @@ -71,6 +71,9 @@ let isSaving = false; let savingPromise: Promise | null = null; let autoSaveTimer: ReturnType | undefined; + // TiptapEditor fires onUpdate on init with round-tripped markdown that may differ + // from the original. Track whether auto-save should be active yet. + let autoSaveReady = isNew; function hasUnsavedChanges(): boolean { return ( @@ -126,6 +129,13 @@ const _col = color; const _cm = checklistMode; + if (!autoSaveReady) { + // Absorb TiptapEditor's initial content normalization as the baseline + lastSavedContent = _c; + autoSaveReady = true; + return; + } + if (_t === lastSavedTitle && _c === lastSavedContent && _col === lastSavedColor && _cm === lastSavedChecklistMode) return; if (!_t.trim() && !_c.trim()) return; @@ -272,7 +282,13 @@ }); function handleAttachmentUpload(attachment: Attachment) { - attachmentsList = [...attachmentsList, attachment]; + const updated = [...attachmentsList, attachment]; + attachmentsList = updated; + if (noteId) { + notes.update((list) => + list.map((n) => (n.id === noteId ? { ...n, attachments: updated } : n)) + ); + } } async function handleToggleFeatured(attachmentId: string, featured: boolean) { @@ -284,9 +300,14 @@ body: JSON.stringify({ featured }) }); if (res.ok) { - attachmentsList = attachmentsList.map((a) => + const updated = attachmentsList.map((a) => a.id === attachmentId ? { ...a, featured } : a ); + attachmentsList = updated; + // Keep the notes store in sync so the card reflects featured state + notes.update((list) => + list.map((n) => (n.id === noteId ? { ...n, attachments: updated } : n)) + ); } } catch (err) { console.error('Failed to toggle featured:', err); @@ -299,7 +320,11 @@ await fetch(`/api/notes/${noteId}/attachments?attachmentId=${attachmentId}`, { method: 'DELETE' }); - attachmentsList = attachmentsList.filter((a) => a.id !== attachmentId); + const updated = attachmentsList.filter((a) => a.id !== attachmentId); + attachmentsList = updated; + notes.update((list) => + list.map((n) => (n.id === noteId ? { ...n, attachments: updated } : n)) + ); } catch (err) { console.error('Failed to remove attachment:', err); }