From 47c408dc5095eb4d2c5778fb9db77f9595f2b176 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 11 Mar 2026 18:44:44 +0100 Subject: [PATCH] feat: wire H1 sync and frontmatter title change to full rename flow handleTitleSync now saves pending content and calls rename_note (which renames the file and updates wikilinks) instead of only updating in-memory state. handleUpdateFrontmatter also triggers rename when the title: key is changed. Co-Authored-By: Claude Opus 4.6 --- src/App.tsx | 12 +++++------ src/hooks/useNoteActions.ts | 36 ++++++++++++++++++++++++++++----- src/mock-tauri/mock-handlers.ts | 11 ++++++---- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b37d0988..9d7e909e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -408,13 +408,11 @@ function App() { setToastMessage(`Type "${name}" created`) }, [notes]) - /** H1→title sync: update VaultEntry.title and tab entry in memory. */ - const handleTitleSync = useCallback((path: string, newTitle: string) => { - vault.updateEntry(path, { title: newTitle }) - notes.setTabs(prev => prev.map(t => - t.entry.path === path ? { ...t, entry: { ...t.entry, title: newTitle } } : t - )) - }, [vault, notes]) + /** H1→title sync: save pending content then rename file + update wikilinks. */ + const handleTitleSync = useCallback(async (path: string, newTitle: string) => { + await savePendingForPath(path) + await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles) + }, [notes, resolvedPath, vault, savePendingForPath]) const bulkActions = useBulkActions(entryActions, setToastMessage) diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index ba695638..ee9f33e8 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -61,15 +61,21 @@ function isTypeKey(key: string): boolean { return k === 'type' || k === 'is_a' } +/** Check if a frontmatter key represents the note title. */ +function isTitleKey(key: string): boolean { + return key.toLowerCase().replace(/\s+/g, '_') === 'title' +} + async function performRename( path: string, newTitle: string, vaultPath: string, + oldTitle?: string, ): Promise { if (isTauri()) { - return invoke('rename_note', { vaultPath, oldPath: path, newTitle }) + return invoke('rename_note', { vaultPath, oldPath: path, newTitle, oldTitle: oldTitle ?? null }) } - return mockInvoke('rename_note', { vault_path: vaultPath, old_path: path, new_title: newTitle }) + return mockInvoke('rename_note', { vault_path: vaultPath, old_path: path, new_title: newTitle, old_title: oldTitle ?? null }) } function buildRenamedEntry(entry: VaultEntry, newTitle: string, newPath: string): VaultEntry { @@ -443,9 +449,9 @@ export function useNoteActions(config: NoteActionsConfig) { onEntryRenamed: (oldPath: string, newEntry: Partial & { path: string }, newContent: string) => void, ) => { try { - const result = await performRename(path, newTitle, vaultPath) - const newContent = await loadNoteContent(result.new_path) const entry = entries.find((e) => e.path === path) + const result = await performRename(path, newTitle, vaultPath, entry?.title) + const newContent = await loadNoteContent(result.new_path) const newEntry = buildRenamedEntry(entry ?? {} as VaultEntry, newTitle, result.new_path) const otherTabPaths = tabsRef.current.filter(t => t.entry.path !== path).map(t => t.entry.path) setTabs((prev) => prev.map((t) => t.entry.path === path ? { entry: newEntry, content: newContent } : t)) @@ -470,6 +476,26 @@ export function useNoteActions(config: NoteActionsConfig) { createTypeEntrySilent, handleUpdateFrontmatter: useCallback(async (path: string, key: string, value: FrontmatterValue) => { await runFrontmatterOp('update', path, key, value) + if (isTitleKey(key) && typeof value === 'string' && value !== '') { + try { + const oldTitle = tabsRef.current.find(t => t.entry.path === path)?.entry.title + const result = await performRename(path, value, config.vaultPath, oldTitle) + if (result.new_path !== path) { + const newFilename = result.new_path.split('/').pop() ?? '' + config.replaceEntry?.(path, { path: result.new_path, filename: newFilename, title: value } as Partial & { path: string }) + const newContent = await loadNoteContent(result.new_path) + setTabs(prev => prev.map(t => t.entry.path === path + ? { entry: { ...t.entry, path: result.new_path, filename: newFilename, title: value }, content: newContent } + : t)) + if (activeTabPathRef.current === path) handleSwitchTab(result.new_path) + const otherTabPaths = tabsRef.current.filter(t => t.entry.path !== path && t.entry.path !== result.new_path).map(t => t.entry.path) + await reloadTabsAfterRename(otherTabPaths, updateTabContent) + } + setToastMessage(renameToastMessage(result.updated_files)) + } catch (err) { + console.error('Failed to rename note after title change:', err) + } + } if (isTypeKey(key) && typeof value === 'string' && value !== '') { try { const result = await performMoveToTypeFolder(config.vaultPath, path, value) @@ -494,7 +520,7 @@ export function useNoteActions(config: NoteActionsConfig) { console.error('Failed to move note to type folder:', err) } } - }, [runFrontmatterOp, config.vaultPath, config.replaceEntry, setTabs, activeTabPathRef, handleSwitchTab, setToastMessage]), + }, [runFrontmatterOp, config.vaultPath, config.replaceEntry, setTabs, activeTabPathRef, handleSwitchTab, setToastMessage, updateTabContent]), handleDeleteProperty: useCallback((path: string, key: string) => runFrontmatterOp('delete', path, key), [runFrontmatterOp]), handleAddProperty: useCallback((path: string, key: string, value: FrontmatterValue) => runFrontmatterOp('update', path, key, value), [runFrontmatterOp]), handleRenameNote, diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index b4841288..406ee7d9 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -114,7 +114,13 @@ const mockThemes: ThemeFile[] = [ let mockDeviceFlowPollCount = 0 -function handleRenameNote(args: { vault_path: string; old_path: string; new_title: string }) { +function handleRenameNote(args: { vault_path: string; old_path: string; new_title: string; old_title?: string | null }) { + const oldEntry = MOCK_ENTRIES.find(e => e.path === args.old_path) + const oldTitle = args.old_title ?? oldEntry?.title ?? '' + if (oldTitle === args.new_title) { + return { new_path: args.old_path, updated_files: 0 } + } + const oldContent = MOCK_CONTENT[args.old_path] ?? '' const slug = args.new_title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') const parentDir = args.old_path.replace(/\/[^/]+$/, '') @@ -123,9 +129,6 @@ function handleRenameNote(args: { vault_path: string; old_path: string; new_titl delete MOCK_CONTENT[args.old_path] MOCK_CONTENT[newPath] = newContent - - const oldEntry = MOCK_ENTRIES.find(e => e.path === args.old_path) - const oldTitle = oldEntry?.title ?? '' let updatedFiles = 0 if (oldTitle) { const pattern = new RegExp(`\\[\\[${oldTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\|[^\\]]*?)?\\]\\]`, 'g')