feat: refresh mobile notes after save
This commit is contained in:
@@ -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 }))
|
||||
},
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 })) {
|
||||
|
||||
42
apps/mobile/src/mobileSavedDraftProjection.test.ts
Normal file
42
apps/mobile/src/mobileSavedDraftProjection.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
28
apps/mobile/src/mobileSavedDraftProjection.ts
Normal file
28
apps/mobile/src/mobileSavedDraftProjection.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user