From 38805b0eaf4e3534cc3d9344c54fca126dec820b Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 4 May 2026 11:05:42 +0200 Subject: [PATCH] feat: add mobile editor draft save --- apps/mobile/src/mobileEditorDraftSave.test.ts | 48 +++++++++++++++++++ apps/mobile/src/mobileEditorDraftSave.ts | 36 ++++++++++++++ docs/MOBILE_PROGRESS.md | 10 +++- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 apps/mobile/src/mobileEditorDraftSave.test.ts create mode 100644 apps/mobile/src/mobileEditorDraftSave.ts diff --git a/apps/mobile/src/mobileEditorDraftSave.test.ts b/apps/mobile/src/mobileEditorDraftSave.test.ts new file mode 100644 index 00000000..2f9f4c56 --- /dev/null +++ b/apps/mobile/src/mobileEditorDraftSave.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { createMobileEditorDraft } from './mobileEditorDraft' +import { saveMobileEditorDraft } from './mobileEditorDraftSave' +import { createMobileVaultConfig } from './mobileVaultConfig' +import { createMemoryMobileVaultStorage } from './mobileVaultStorage' + +const vault = createVault() + +describe('mobile editor draft save', () => { + it('writes persistable editor drafts as canonical Markdown', async () => { + const storage = createMemoryMobileVaultStorage([]) + const draft = createMobileEditorDraft({ + note: { id: 'notes/workflow', title: 'Workflow', content: '# Workflow' }, + editorHtml: '

Workflow

Edited

', + }) + + await expect(saveMobileEditorDraft({ draft, storage, vault })).resolves.toEqual({ + status: 'saved', + path: 'notes/workflow.md', + }) + + await expect(storage.readMarkdownFile(vault, 'notes/workflow.md')).resolves.toBe('# Workflow\n\nEdited') + }) + + it('does not write blocked editor drafts', async () => { + const storage = createMemoryMobileVaultStorage([]) + const draft = createMobileEditorDraft({ + note: { id: 'workflow', title: 'Workflow', content: '# Workflow' }, + editorHtml: '
Unsupported
', + }) + + await expect(saveMobileEditorDraft({ draft, storage, vault })).resolves.toEqual({ + status: 'blocked', + reason: 'unsupportedEditorHtml', + }) + + await expect(storage.listMarkdownFiles(vault)).resolves.toEqual([]) + }) +}) + +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/mobileEditorDraftSave.ts b/apps/mobile/src/mobileEditorDraftSave.ts new file mode 100644 index 00000000..1fe97708 --- /dev/null +++ b/apps/mobile/src/mobileEditorDraftSave.ts @@ -0,0 +1,36 @@ +import type { MobileEditorDraft } from './mobileEditorDraft' +import type { MobileVaultConfig } from './mobileVaultConfig' +import type { MobileVaultStorageDriver } from './mobileVaultStorage' + +export type MobileEditorDraftSaveResult = + | { + status: 'saved' + path: string + } + | { + status: 'blocked' + reason: 'unsupportedEditorHtml' + } + +export async function saveMobileEditorDraft({ + draft, + storage, + vault, +}: { + draft: MobileEditorDraft + storage: MobileVaultStorageDriver + vault: MobileVaultConfig +}): Promise { + if (!draft.persistable) { + return { status: 'blocked', reason: draft.blockedReason } + } + + const path = draftPath(draft) + await storage.writeMarkdownFile(vault, path, draft.canonicalMarkdown) + + return { status: 'saved', path } +} + +function draftPath(draft: MobileEditorDraft) { + return `${draft.noteId}.md` +} diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md index e7bd52c9..3bcea1ef 100644 --- a/docs/MOBILE_PROGRESS.md +++ b/docs/MOBILE_PROGRESS.md @@ -8,7 +8,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road - Branch: `codex/mobile` - Active phase: Phase 2 - Mobile Shell -- Active slice: Expand TenTap Markdown serialization +- Active slice: Add mobile editor draft save boundary - Push policy: commit locally; do not push unless explicitly requested - Validation target: iPad/iOS simulator first @@ -57,6 +57,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road - Wired the mobile shell to load its starter notes through the native Expo FileSystem storage driver and stored repository path, with fixture notes retained as the fallback while storage initializes. - Expanded supported TenTap HTML serialization to include H1-H6 headings, ordered lists, task-list markers, inline strong/emphasis/code, and links while continuing to block unsupported block HTML. - Split the mobile editor HTML serializer into its own CodeScene-10 module so the draft boundary stays small. +- Added a mobile editor draft save boundary that writes persistable canonical Markdown drafts to vault storage and refuses blocked drafts. ## Next Action @@ -64,7 +65,7 @@ Continue Phase 2 with the next mobile shell slice: 1. Dismiss or suppress Expo Go's first-run tools modal during simulator QA so screenshots capture the app without the overlay. 2. Expand the serializer for additional TenTap output needed by real notes: links, emphasis, code, headings, ordered lists, and task lists. -3. Add a save command boundary that writes persistable TenTap Markdown drafts back to mobile vault storage. +3. Wire `MobileEditorAdapter` draft callbacks to the save boundary with explicit dirty/saved/blocked UI state. ## Verification Log @@ -176,6 +177,11 @@ Continue Phase 2 with the next mobile shell slice: - `pnpm --filter @tolaria/mobile typecheck` passed after expanded TenTap serialization. - CodeScene after expanded TenTap serialization: `apps/mobile/src/mobileEditorDraft.ts`, `apps/mobile/src/mobileEditorDraft.test.ts`, and `apps/mobile/src/mobileEditorHtmlMarkdown.ts` scored `10`. - `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after expanded TenTap serialization. +- `pnpm --filter @tolaria/mobile test -- src/mobileEditorDraftSave.test.ts` passed after editor draft save boundary: 13 files / 44 tests. +- `pnpm --filter @tolaria/mobile test` passed after editor draft save boundary: 13 files / 44 tests. +- `pnpm --filter @tolaria/mobile typecheck` passed after editor draft save boundary. +- CodeScene after editor draft save boundary: `apps/mobile/src/mobileEditorDraftSave.ts` and `apps/mobile/src/mobileEditorDraftSave.test.ts` scored `10`. +- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after editor draft save boundary. ## Risks / Watch Items