Auto-save entries and lists on navigate-away and app close

- Add saveCurrentDocument() that persists entries (via saveEntryFromStore)
  and lists (via updateListByStoreId) when switching documents or sections
- Register flush callback in session store so +layout.svelte can trigger
  a save before vault rebuild on window close
- Remove explicit Save button from MarkdownEditor (no longer needed)
- Simplify MarkdownEditor props (removed activeSection, onOpenDocument,
  onDeleteDocument since save is now handled by the parent)
- Todos already save instantly; fragments keep their form-based workflow

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
Jacob Schmidt 2026-02-26 17:40:32 -06:00
parent c7933aeeec
commit 58f9f46cb9
5 changed files with 41 additions and 42 deletions

View File

@ -36,13 +36,10 @@
/>
{:else}
<MarkdownEditor
{activeSection}
{openDocumentId}
{openDocumentName}
{openDocumentContent}
{onDocumentContentChange}
{onOpenDocument}
{onDeleteDocument}
/>
{/if}
</main>

View File

@ -1,23 +1,15 @@
<script lang="ts">
import {
saveEntryFromStore,
type EntryItem,
} from "$lib/stores/entries";
import { renderMarkdown, extractEditorTitle } from "$lib/utils/markdown";
export let activeSection = "entries";
export let openDocumentId = "";
export let openDocumentName = "";
export let openDocumentContent = "";
export let onDocumentContentChange: (content: string) => void = () => {};
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
export let onDeleteDocument: (id: string) => void = () => {};
let markdownText = openDocumentContent;
let lastOpenDocumentId = openDocumentId;
let previewOnly = true;
let editorInput: HTMLTextAreaElement | null = null;
let entrySaveBusy = false;
function updateDraft(value: string) {
markdownText = value;
@ -69,28 +61,6 @@
applyWrap("[", "](https://example.com)");
}
async function saveEntryDocument() {
if (activeSection !== "entries") return;
try {
entrySaveBusy = true;
const previousId = openDocumentId;
const saved: EntryItem | null = await saveEntryFromStore(previousId, markdownText, "Overwrite");
if (!saved) return;
if (saved.id !== previousId) {
onDeleteDocument(previousId);
}
onOpenDocument(saved);
onDocumentContentChange(saved.initialContent);
} catch (error) {
console.error("[editor] entries:save:error", error);
} finally {
entrySaveBusy = false;
}
}
$: if (openDocumentId !== lastOpenDocumentId) {
markdownText = openDocumentContent;
lastOpenDocumentId = openDocumentId;
@ -102,11 +72,6 @@
<header class="editor-header">
<h1>{editorTitle}</h1>
<div class="editor-actions">
{#if activeSection === "entries"}
<button type="button" on:click={saveEntryDocument} disabled={entrySaveBusy}>
{entrySaveBusy ? "Saving..." : "Save"}
</button>
{/if}
<button type="button" class:primary={!previewOnly} on:click={() => (previewOnly = false)}>Write</button>
<button type="button" class:primary={previewOnly} on:click={() => (previewOnly = true)}>Preview</button>
</div>

View File

@ -22,3 +22,13 @@ export function clearVaultSession(): void {
_password.set(null);
_unlocked.set(false);
}
let _flushCallback: (() => Promise<void>) | null = null;
export function setFlushCallback(fn: () => Promise<void>): void {
_flushCallback = fn;
}
export async function flushBeforeClose(): Promise<void> {
if (_flushCallback) await _flushCallback();
}

View File

@ -2,7 +2,7 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
import { getSessionPassword, clearVaultSession } from "$lib/stores/session";
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
import { persistAndClearVault } from "$lib/backend/auth";
let closeInProgress = false;
@ -14,6 +14,8 @@
event.preventDefault();
closeInProgress = true;
try { await flushBeforeClose(); } catch {}
const password = getSessionPassword();
if (password) {
try {

View File

@ -2,10 +2,10 @@
import { goto } from "$app/navigation";
import { unlockVaultWorkspace } from "$lib/backend/auth";
import AppModal from "$lib/components/AppModal.svelte";
import { entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId } from "$lib/stores/entries";
import { entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId, saveEntryFromStore } from "$lib/stores/entries";
import { hydrateFragments } from "$lib/stores/fragments";
import { hydrateLists } from "$lib/stores/lists";
import { isVaultReady, setVaultSession } from "$lib/stores/session";
import { hydrateLists, updateListByStoreId } from "$lib/stores/lists";
import { isVaultReady, setFlushCallback, setVaultSession } from "$lib/stores/session";
import { hydrateTodos } from "$lib/stores/todos";
import Navbar from "$lib/components/Navbar.svelte";
import SidePanel from "$lib/components/SidePanel.svelte";
@ -195,6 +195,28 @@
}
}
async function saveCurrentDocument() {
if (!activeDocumentId) return;
const content = openDocuments[activeDocumentId];
if (!content?.trim()) return;
try {
if (selectedSection === "entries") {
const saved = await saveEntryFromStore(activeDocumentId, content, "Overwrite");
if (saved && saved.id !== activeDocumentId) {
const { [activeDocumentId]: _, ...rest } = openDocuments;
openDocuments = { ...rest, [saved.id]: saved.initialContent };
activeDocumentId = saved.id;
activeDocumentLabel = saved.label;
}
} else if (selectedSection === "lists" && activeDocumentId.startsWith("lists/") && !activeDocumentId.startsWith("lists/draft-")) {
await updateListByStoreId(activeDocumentId, undefined, content);
}
} catch {
// best-effort save
}
}
function handleSelect(id: string) {
if (id === "account" || id === "settings") {
goto(`/${id}`);
@ -219,6 +241,7 @@
return;
}
saveCurrentDocument();
selectedSection = id;
panelOpen = true;
activeDocumentId = "";
@ -226,6 +249,7 @@
}
async function handleOpenDocument(doc: OpenDocument) {
await saveCurrentDocument();
let resolvedDoc = doc;
if (selectedSection === "entries" && doc.id.startsWith("entries/file/") && !doc.initialContent) {
try {
@ -255,6 +279,7 @@
}
onMount(() => {
setFlushCallback(saveCurrentDocument);
bootstrapFragmentsWithUnlock();
});
</script>