diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts index 42174234..d694c83d 100644 --- a/src/hooks/frontmatterOps.ts +++ b/src/hooks/frontmatterOps.ts @@ -270,6 +270,7 @@ export interface FrontmatterOpOptions { export interface FrontmatterApplyCallbacks { updateTab: (path: VaultPath, content: MarkdownContent) => void updateEntry: (path: VaultPath, patch: Partial) => void + cacheContent?: (path: VaultPath, content: MarkdownContent) => void toast: (message: ToastMessage) => void getEntry?: (path: VaultPath) => VaultEntry | undefined onMissingNotePath?: (path: VaultPath, error: unknown) => void | Promise @@ -395,6 +396,7 @@ export async function runFrontmatterAndApply(request: FrontmatterRunRequest): Pr const { op, path, key, value, callbacks, options } = request try { const newContent = await executeFrontmatterOp(op, path, key, value) + callbacks.cacheContent?.(path, newContent) if (callbacks.shouldApply && !callbacks.shouldApply(path)) return undefined callbacks.updateTab(path, newContent) applyEntryPatch(path, callbacks, frontmatterToEntryPatch(op, key, value)) diff --git a/src/hooks/useNoteActions.hook.test.ts b/src/hooks/useNoteActions.hook.test.ts index 5d15a59d..4b1d9e1c 100644 --- a/src/hooks/useNoteActions.hook.test.ts +++ b/src/hooks/useNoteActions.hook.test.ts @@ -7,6 +7,7 @@ import { RAPID_CREATE_NOTE_SETTLE_MS } from './useNoteCreation' import { useNoteActions } from './useNoteActions' import type { NoteActionsConfig } from './useNoteActions' import { GITIGNORED_VISIBILITY_APPLIED_EVENT } from '../lib/gitignoredVisibilityEvents' +import { clearNoteContentCache, getCachedNoteContentEntry } from './noteContentCache' vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() })) vi.mock('../mock-tauri', () => ({ @@ -85,6 +86,7 @@ describe('useNoteActions hook', () => { vi.clearAllMocks() vi.mocked(invoke).mockReset() vi.mocked(isTauri).mockReturnValue(false) + clearNoteContentCache() vi.useRealTimers() }) @@ -283,6 +285,44 @@ describe('useNoteActions hook', () => { expect(setToastMessage).not.toHaveBeenCalled() }) + it('keeps property-only frontmatter writes in the note cache after a note switch wins the apply guard', async () => { + vi.mocked(isTauri).mockReturnValue(true) + const noteA = makeEntry({ path: '/vault/note-a.md', filename: 'note-a.md', title: 'Note A' }) + const noteB = makeEntry({ path: '/vault/note-b.md', filename: 'note-b.md', title: 'Note B' }) + const updatedContent = '---\nStatus: Done\n---\nBody' + let resolveFrontmatterWrite: ((content: string) => void) | null = null + vi.mocked(invoke).mockImplementation(() => new Promise((resolve) => { + resolveFrontmatterWrite = (content) => { resolve(content) } + })) + + const { result } = renderHook(() => useNoteActions(makeConfig([noteA, noteB]))) + act(() => { + result.current.handleSwitchTab(noteA.path) + }) + + let updatePromise: Promise = Promise.resolve() + await act(async () => { + updatePromise = result.current.handleUpdateFrontmatter( + noteA.path, + 'Status', + 'Done', + { requireActivePath: noteA.path }, + ) + await Promise.resolve() + }) + act(() => { + result.current.handleSwitchTab(noteB.path) + }) + await act(async () => { + resolveFrontmatterWrite?.(updatedContent) + await updatePromise + }) + + expect(updateEntry).not.toHaveBeenCalled() + expect(setToastMessage).not.toHaveBeenCalled() + expect(getCachedNoteContentEntry(noteA.path)?.value).toBe(updatedContent) + }) + it('handleCreateNoteImmediate creates note with timestamp-based title', async () => { const createdEntry = await createImmediateEntry() expect(createdEntry.title).toBe('Untitled Note 1700000000') diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index 55104834..13f138d9 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, type MutableRefObject } from 'react' import type { VaultEntry } from '../types' import type { FrontmatterValue } from '../components/Inspector' -import { useTabManagement } from './useTabManagement' +import { cacheNoteContent, useTabManagement } from './useTabManagement' import { GITIGNORED_VISIBILITY_APPLIED_EVENT, type GitignoredVisibilityAppliedEvent, @@ -474,6 +474,7 @@ function useFrontmatterRunner({ key, value, callbacks: { + cacheContent: cacheNoteContent, updateTab: updateTabContent, updateEntry, toast: setToastMessage,