feat: TypeScript calls sync_note_title on note open

- handleSelectNote syncs title frontmatter before loading content
- handleSelectNoteWithSync reloads entry after open to update display title
- Added sync_note_title to mock handlers
- Fixed rapid-switching test to flush sync microtask

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-17 16:52:23 +01:00
parent 9112a24fcb
commit 77b6961cb9
4 changed files with 36 additions and 5 deletions

View File

@@ -1,4 +1,6 @@
import { useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri } from '../mock-tauri'
import type { VaultEntry } from '../types'
import type { FrontmatterValue } from '../components/Inspector'
import { useTabManagement } from './useTabManagement'
@@ -78,15 +80,27 @@ export function useNoteActions(config: NoteActionsConfig) {
setTabs((prev) => prev.map((t) => t.entry.path === path ? { ...t, content: newContent } : t))
}, [setTabs])
const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote, handleCloseTab, handleCloseTabRef })
// After opening a note, reload its VaultEntry so title reflects any sync.
const handleSelectNoteWithSync = useCallback(async (entry: VaultEntry) => {
await handleSelectNote(entry)
// Reload entry from disk to pick up title changes from sync_note_title
if (isTauri()) {
try {
const fresh = await invoke<VaultEntry>('reload_vault_entry', { path: entry.path })
if (fresh.title !== entry.title) updateEntry(entry.path, { title: fresh.title })
} catch { /* non-fatal: entry display may be stale */ }
}
}, [handleSelectNote, updateEntry])
const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote: handleSelectNoteWithSync, handleCloseTab, handleCloseTabRef })
const rename = useNoteRename(
{ entries, setToastMessage },
{ tabs: tabMgmt.tabs, setTabs, activeTabPathRef, handleSwitchTab, updateTabContent },
)
const handleNavigateWikilink = useCallback(
(target: string) => navigateWikilink(entries, target, handleSelectNote),
[entries, handleSelectNote],
(target: string) => navigateWikilink(entries, target, handleSelectNoteWithSync),
[entries, handleSelectNoteWithSync],
)
const runFrontmatterOp = useCallback(
@@ -97,6 +111,7 @@ export function useNoteActions(config: NoteActionsConfig) {
return {
...tabMgmt,
handleSelectNote: handleSelectNoteWithSync,
handleCloseTab: creation.handleCloseTabWithCleanup,
handleNavigateWikilink,
handleCreateNote: creation.handleCreateNote,

View File

@@ -559,14 +559,17 @@ describe('useTabManagement', () => {
// Start loading A (don't await — simulates rapid click)
let selectADone = false
act(() => {
await act(async () => {
result.current.handleSelectNote(makeEntry({ path: '/vault/a.md', title: 'A' })).then(() => { selectADone = true })
// Flush microtask from sync_note_title (no-op in mock mode) so loadAndSetTab starts
await Promise.resolve()
})
// Start loading B while A is still loading
let selectBDone = false
act(() => {
await act(async () => {
result.current.handleSelectNote(makeEntry({ path: '/vault/b.md', title: 'B' })).then(() => { selectBDone = true })
await Promise.resolve()
})
// B resolves first

View File

@@ -69,6 +69,16 @@ async function loadNoteContent(path: string): Promise<string> {
: mockInvoke<string>('get_note_content', { path })
}
/** Sync title frontmatter with filename on note open.
* Returns true if the file was modified. */
async function syncNoteTitle(path: string): Promise<boolean> {
try {
return isTauri()
? await invoke<boolean>('sync_note_title', { path })
: false // mock: no-op
} catch { return false }
}
function addTabIfAbsent(prev: Tab[], entry: VaultEntry, content: string): Tab[] {
if (prev.some((t) => t.entry.path === entry.path)) return prev
return [...prev, { entry, content }]
@@ -147,6 +157,8 @@ export function useTabManagement() {
const handleSelectNote = useCallback(async (entry: VaultEntry) => {
if (isTabOpen(tabsRef.current, entry.path)) { setActiveTabPath(entry.path); return }
const seq = ++navSeqRef.current
// Sync title frontmatter with filename before loading content
await syncNoteTitle(entry.path)
await loadAndSetTab(entry, (prev, content) => addTabIfAbsent(prev, entry, content), setTabs)
if (navSeqRef.current === seq) setActiveTabPath(entry.path)
}, [])

View File

@@ -156,6 +156,7 @@ export const mockHandlers: Record<string, (args: any) => any> = {
list_vault: () => MOCK_ENTRIES,
reload_vault: () => MOCK_ENTRIES,
reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, trashed: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {} },
sync_note_title: () => false,
get_note_content: (args: { path: string }) => MOCK_CONTENT[args.path] ?? '',
get_all_content: () => MOCK_CONTENT,
get_file_history: (args: { path: string }) => mockFileHistory(args.path),