From d08da1c7bf19d85fe3b6921cc2aab4e97c15bcdd Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 5 May 2026 10:32:10 +0200 Subject: [PATCH] feat: save mobile note frontmatter --- apps/mobile/src/mobileDemoVault.ts | 19 ++++++ .../src/mobileNoteFrontmatterSave.test.ts | 65 +++++++++++++++++++ apps/mobile/src/mobileNoteFrontmatterSave.ts | 41 ++++++++++++ docs/MOBILE_PROGRESS.md | 8 ++- 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/src/mobileNoteFrontmatterSave.test.ts create mode 100644 apps/mobile/src/mobileNoteFrontmatterSave.ts diff --git a/apps/mobile/src/mobileDemoVault.ts b/apps/mobile/src/mobileDemoVault.ts index f1bb91b7..86aa2ef1 100644 --- a/apps/mobile/src/mobileDemoVault.ts +++ b/apps/mobile/src/mobileDemoVault.ts @@ -1,6 +1,8 @@ import { demoNoteSources } from './demoData' import type { MobileEditorDraft } from './mobileEditorDraft' import { saveMobileEditorDraft } from './mobileEditorDraftSave' +import { saveMobileNoteFrontmatter } from './mobileNoteFrontmatterSave' +import type { WritableMobileNoteFrontmatter } from './mobileNoteFrontmatterWrite' import { createMobileNoteFile } from './mobileNoteCreate' import { createMobileVaultConfigFromMetadata, @@ -51,6 +53,23 @@ export async function deleteDemoVaultNote(noteId: string, vaultMetadata = defaul }).deleteNote(noteId) } +export function saveDemoVaultNoteFrontmatter({ + metadata, + noteId, + vaultMetadata = defaultMobileVaultMetadata, +}: { + metadata: WritableMobileNoteFrontmatter + noteId: string + vaultMetadata?: MobileVaultMetadata +}) { + return saveMobileNoteFrontmatter({ + metadata, + noteId, + storage: createNativeMobileVaultStorage(), + vault: createDemoVaultConfig(vaultMetadata), + }) +} + function demoVaultFiles(): MobileVaultFile[] { return demoNoteSources.map((source) => ({ path: source.filename, diff --git a/apps/mobile/src/mobileNoteFrontmatterSave.test.ts b/apps/mobile/src/mobileNoteFrontmatterSave.test.ts new file mode 100644 index 00000000..cbf74c08 --- /dev/null +++ b/apps/mobile/src/mobileNoteFrontmatterSave.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' +import { saveMobileNoteFrontmatter } from './mobileNoteFrontmatterSave' +import { createMobileVaultConfig } from './mobileVaultConfig' +import { createMemoryMobileVaultStorage } from './mobileVaultStorage' + +const vault = createVault() + +describe('mobile note frontmatter save', () => { + it('updates note frontmatter through vault storage', async () => { + const storage = createMemoryMobileVaultStorage([ + { + path: 'notes/workflow.md', + content: '---\ntitle: Workflow\nprivate: true\n---\n# Workflow', + }, + ]) + + await expect(saveMobileNoteFrontmatter({ + metadata: { + status: 'Draft', + tags: ['mobile'], + type: 'Project', + }, + noteId: 'notes/workflow', + storage, + vault, + })).resolves.toEqual({ + status: 'saved', + path: 'notes/workflow.md', + }) + + await expect(storage.readMarkdownFile(vault, 'notes/workflow.md')).resolves.toBe([ + '---', + 'title: Workflow', + 'private: true', + 'type: Project', + 'status: Draft', + 'tags: [mobile]', + '---', + '# Workflow', + ].join('\n')) + }) + + it('returns missing when the note file does not exist', async () => { + const storage = createMemoryMobileVaultStorage([]) + + await expect(saveMobileNoteFrontmatter({ + metadata: { type: 'Essay' }, + noteId: 'missing', + storage, + vault, + })).resolves.toEqual({ + status: 'missing', + path: 'missing.md', + }) + }) +}) + +function createVault() { + const result = createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' }) + if (!result.ok) { + throw new Error(result.error) + } + + return result.config +} diff --git a/apps/mobile/src/mobileNoteFrontmatterSave.ts b/apps/mobile/src/mobileNoteFrontmatterSave.ts new file mode 100644 index 00000000..2f8f297f --- /dev/null +++ b/apps/mobile/src/mobileNoteFrontmatterSave.ts @@ -0,0 +1,41 @@ +import type { MobileVaultConfig } from './mobileVaultConfig' +import type { MobileVaultStorageDriver } from './mobileVaultStorage' +import { + writeMobileNoteFrontmatter, + type WritableMobileNoteFrontmatter, +} from './mobileNoteFrontmatterWrite' + +export type MobileNoteFrontmatterSaveResult = + | { + status: 'saved' + path: string + } + | { + status: 'missing' + path: string + } + +export async function saveMobileNoteFrontmatter({ + metadata, + noteId, + storage, + vault, +}: { + metadata: WritableMobileNoteFrontmatter + noteId: string + storage: MobileVaultStorageDriver + vault: MobileVaultConfig +}): Promise { + const path = notePath(noteId) + const content = await storage.readMarkdownFile(vault, path) + if (content === null) { + return { status: 'missing', path } + } + + await storage.writeMarkdownFile(vault, path, writeMobileNoteFrontmatter({ content, metadata })) + return { status: 'saved', path } +} + +function notePath(noteId: string) { + return `${noteId}.md` +} diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md index a579df16..c9c042a6 100644 --- a/docs/MOBILE_PROGRESS.md +++ b/docs/MOBILE_PROGRESS.md @@ -77,12 +77,13 @@ This file is the resumable working log for Tolaria mobile. The strategy and road - Deferred archive as a first-class mobile note state until the mobile vault schema/frontmatter model is explicit; implementing archive now as file movement would create throwaway semantics that may conflict with desktop-compatible metadata. - Added mobile frontmatter metadata parsing for stored markdown notes, including type, icon, date, and inline tags, so app-local vault scans can project note metadata instead of hardcoding every stored note as a generic file. - Added mobile frontmatter serialization helpers that can create/update supported type/status/date/icon/tags fields while preserving unknown metadata lines. +- Added a mobile frontmatter save boundary that updates persisted note metadata through the vault storage driver and exposes the same path through the demo vault facade for future properties UI calls. ## Next Action Continue Phase 4 with editor durability: -1. Wire mobile property edits to the frontmatter read/write helpers for type/date/status/icon/tags. +1. Wire the mobile properties panel to the frontmatter save boundary for type/date/status/icon/tags. 2. Expand TenTap Markdown serialization coverage for common writing constructs and preserve unsupported blocks without corrupting files. 3. Add simulator interaction coverage for create/open/edit/autosave/delete using a development-client path or another route that avoids Expo Go's overlay controls. @@ -271,6 +272,11 @@ Continue Phase 4 with editor durability: - CodeScene after mobile frontmatter serialization helpers: `apps/mobile/src/mobileNoteFrontmatterWrite.ts` and `apps/mobile/src/mobileNoteFrontmatterWrite.test.ts` scored `10`. - `pnpm --filter @tolaria/mobile test` passed after mobile frontmatter serialization helpers: 24 files / 78 tests. - `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after mobile frontmatter serialization helpers. +- `pnpm --filter @tolaria/mobile test -- src/mobileNoteFrontmatterSave.test.ts src/mobileNoteFrontmatterWrite.test.ts` passed after mobile frontmatter save boundary: 25 files / 80 tests. +- `pnpm --filter @tolaria/mobile typecheck` passed after mobile frontmatter save boundary. +- CodeScene after mobile frontmatter save boundary: `apps/mobile/src/mobileNoteFrontmatterSave.ts`, `apps/mobile/src/mobileNoteFrontmatterSave.test.ts`, and `apps/mobile/src/mobileDemoVault.ts` scored `10`. +- `pnpm --filter @tolaria/mobile test` passed after mobile frontmatter save boundary: 25 files / 80 tests. +- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after mobile frontmatter save boundary. ## Risks / Watch Items