feat: refresh mobile notes after save

This commit is contained in:
lucaronin
2026-05-04 18:54:01 +02:00
parent 518b61201b
commit 0efad2211a
6 changed files with 96 additions and 4 deletions

View File

@@ -25,6 +25,7 @@ import {
idleMobileEditorSaveState,
type MobileEditorSaveState,
} from './mobileEditorSaveState'
import { applySavedMobileEditorDraft } from './mobileSavedDraftProjection'
import { MobileEditorAdapter } from './MobileEditorAdapter'
import {
createCompactNavigationState,
@@ -57,6 +58,9 @@ export function MobileApp() {
onStateChange: (noteId, saveState) => {
setSaveStateByNoteId((state) => ({ ...state, [noteId]: saveState }))
},
onSavedDraft: (draft) => {
setAvailableNotes((notes) => applySavedMobileEditorDraft({ draft, notes }))
},
}),
[],
)

View File

@@ -30,6 +30,7 @@ describe('mobile autosave queue', () => {
it('ignores stale save results when a newer draft was queued', async () => {
const scheduler = createManualScheduler()
const savedMarkdown: string[] = []
const states: MobileEditorSaveState[] = []
let resolveFirstSave: () => void = () => {
throw new Error('First save was not scheduled')
@@ -37,6 +38,7 @@ describe('mobile autosave queue', () => {
const queue = createMobileAutosaveQueue({
delayMs: 300,
scheduler,
onSavedDraft: (draft) => savedMarkdown.push(draft.canonicalMarkdown),
onStateChange: (_noteId, state) => states.push(state),
saveDraft: (draft) =>
draftMarkdown(draft).includes('First')
@@ -54,6 +56,7 @@ describe('mobile autosave queue', () => {
await scheduler.flush()
expect(states.map((state) => state.state)).toEqual(['queued', 'saving', 'queued', 'saving', 'saved'])
expect(savedMarkdown).toEqual(['# Workflow\n\nSecond'])
})
it('can cancel pending draft saves', async () => {

View File

@@ -20,13 +20,17 @@ export type MobileAutosaveQueue = {
cancelAll: () => void
}
export type SavedMobileEditorDraft = Extract<MobileEditorDraft, { persistable: true }>
export function createMobileAutosaveQueue({
delayMs,
onSavedDraft,
onStateChange,
saveDraft,
scheduler = nativeScheduler,
}: {
delayMs: number
onSavedDraft?: (draft: SavedMobileEditorDraft) => void
onStateChange: (noteId: string, state: MobileEditorSaveState) => void
saveDraft: (draft: MobileEditorDraft) => Promise<MobileEditorDraftSaveResult>
scheduler?: MobileAutosaveScheduler
@@ -43,7 +47,7 @@ export function createMobileAutosaveQueue({
draft.noteId,
scheduler.set(() => {
timerByNoteId.delete(draft.noteId)
void saveLatestDraft({ draft, generation, generationByNoteId, onStateChange, saveDraft })
void saveLatestDraft({ draft, generation, generationByNoteId, onSavedDraft, onStateChange, saveDraft })
}, delayMs),
)
},
@@ -65,12 +69,14 @@ async function saveLatestDraft({
draft,
generation,
generationByNoteId,
onSavedDraft,
onStateChange,
saveDraft,
}: {
draft: MobileEditorDraft
generation: number
generationByNoteId: Map<string, number>
onSavedDraft?: (draft: SavedMobileEditorDraft) => void
onStateChange: (noteId: string, state: MobileEditorSaveState) => void
saveDraft: (draft: MobileEditorDraft) => Promise<MobileEditorDraftSaveResult>
}) {
@@ -79,6 +85,9 @@ async function saveLatestDraft({
const result = await saveDraft(draft)
if (isLatestGeneration({ draft, generation, generationByNoteId })) {
onStateChange(draft.noteId, saveResultState(result))
if (result.status === 'saved' && draft.persistable) {
onSavedDraft?.(draft)
}
}
} catch {
if (isLatestGeneration({ draft, generation, generationByNoteId })) {

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { notes } from './demoData'
import { applySavedMobileEditorDraft } from './mobileSavedDraftProjection'
describe('mobile saved draft projection', () => {
it('refreshes the saved note from canonical markdown', () => {
const updatedNotes = applySavedMobileEditorDraft({
notes,
draft: {
noteId: 'workflow',
sourceMarkdown: notes[0].content,
editorHtml: '<h1>New title</h1><p>Updated body</p>',
persistable: true,
canonicalMarkdown: '# New title\n\nUpdated body',
},
})
expect(updatedNotes[0]).toMatchObject({
id: 'workflow',
title: 'New title',
snippet: 'Updated body',
modified: 'Saved now',
words: 2,
})
expect(updatedNotes[1]).toBe(notes[1])
})
it('returns the original note list when the saved note is not loaded', () => {
const updatedNotes = applySavedMobileEditorDraft({
notes,
draft: {
noteId: 'missing',
sourceMarkdown: '',
editorHtml: '<p>Missing</p>',
persistable: true,
canonicalMarkdown: 'Missing',
},
})
expect(updatedNotes).toBe(notes)
})
})

View File

@@ -0,0 +1,28 @@
import type { MobileNote } from './demoData'
import type { SavedMobileEditorDraft } from './mobileAutosaveQueue'
import { projectMobileNote } from './mobileNoteProjection'
export function applySavedMobileEditorDraft({
draft,
notes,
}: {
draft: SavedMobileEditorDraft
notes: MobileNote[]
}) {
let updated = false
const nextNotes = notes.map((note) => {
if (note.id !== draft.noteId) {
return note
}
updated = true
return projectMobileNote({
...note,
content: draft.canonicalMarkdown,
filename: `${draft.noteId}.md`,
modified: 'Saved now',
})
})
return updated ? nextNotes : notes
}

View File

@@ -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: Debounce mobile editor autosave
- Active slice: Refresh saved mobile note projection
- Push policy: commit locally; do not push unless explicitly requested
- Validation target: iPad/iOS simulator first
@@ -61,14 +61,15 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Wired mobile editor draft changes to the app-local demo vault save path and added visible editor save states: Ready, Saving, Saved, Blocked, and Save failed.
- Split editor save-state styles into a separate style module to keep mobile style files at CodeScene `10`.
- Added a debounced mobile autosave queue that coalesces rapid TenTap draft changes, marks edited drafts as queued, and ignores stale save results when newer drafts supersede them.
- Refreshed the in-memory mobile note projection after the latest successful editor save so the note list, editor source, properties words, and snippets update from canonical Markdown.
## Next Action
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. Refresh the in-memory note projection after a successful editor save so the note list snippet/title stay in sync with saved Markdown.
3. Add the first local note create path against app-local vault storage, then wire it into the compact phone and iPad shells.
2. Add the first local note create path against app-local vault storage, then wire it into the compact phone and iPad shells.
3. Add a focused simulator interaction path for editor typing/autosave once Expo Go's overlay no longer blocks clean screenshots.
## Verification Log
@@ -197,6 +198,11 @@ Continue Phase 2 with the next mobile shell slice:
- `pnpm --filter @tolaria/mobile test` passed after debounced autosave: 15 files / 49 tests.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after debounced autosave.
- `pnpm --filter @tolaria/mobile exec expo start --ios --clear --port 8089` launched on `iPhone 15` after debounced autosave; screenshots captured at `/tmp/tolaria-mobile-autosave-iphone.png` and `/tmp/tolaria-mobile-autosave-iphone-loaded.png`. The app rendered behind Expo Go's first-run Tools modal with no red runtime error overlay.
- `pnpm --filter @tolaria/mobile test -- src/mobileAutosaveQueue.test.ts src/mobileSavedDraftProjection.test.ts` passed after saved note projection refresh: 16 files / 51 tests.
- `pnpm --filter @tolaria/mobile typecheck` passed after saved note projection refresh.
- CodeScene after saved note projection refresh: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileAutosaveQueue.ts`, `apps/mobile/src/mobileAutosaveQueue.test.ts`, `apps/mobile/src/mobileSavedDraftProjection.ts`, and `apps/mobile/src/mobileSavedDraftProjection.test.ts` scored `10`.
- `pnpm --filter @tolaria/mobile test` passed after saved note projection refresh: 16 files / 51 tests.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after saved note projection refresh.
## Risks / Watch Items