2026-03-16 18:31:33 +01:00
|
|
|
import { useCallback } from 'react'
|
2026-02-17 12:10:21 +01:00
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
2026-03-16 18:31:33 +01:00
|
|
|
import { isTauri } from '../mock-tauri'
|
2026-02-17 12:10:21 +01:00
|
|
|
import type { VaultEntry } from '../types'
|
|
|
|
|
import type { FrontmatterValue } from '../components/Inspector'
|
2026-02-22 10:11:52 +01:00
|
|
|
import { useTabManagement } from './useTabManagement'
|
|
|
|
|
import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers'
|
2026-03-16 18:31:33 +01:00
|
|
|
import { updateMockContent, trackMockChange } from '../mock-tauri'
|
2026-03-11 19:12:05 +01:00
|
|
|
import { resolveEntry } from '../utils/wikilink'
|
2026-03-16 18:31:33 +01:00
|
|
|
import { useNoteCreation } from './useNoteCreation'
|
|
|
|
|
import {
|
|
|
|
|
useNoteRename,
|
|
|
|
|
performRename, loadNoteContent, renameToastMessage, reloadTabsAfterRename,
|
|
|
|
|
} from './useNoteRename'
|
|
|
|
|
|
|
|
|
|
// Re-export pure functions so existing consumers don't need to change imports.
|
|
|
|
|
export { buildNewEntry, slugify, generateUntitledName, entryMatchesTarget } from './useNoteCreation'
|
|
|
|
|
export { buildNoteContent, resolveNewNote, resolveNewType, resolveTemplate, DEFAULT_TEMPLATES } from './useNoteCreation'
|
|
|
|
|
export { todayDateString, buildDailyNoteContent, resolveDailyNote, findDailyNote } from './useNoteCreation'
|
|
|
|
|
export { needsRenameOnSave } from './useNoteRename'
|
|
|
|
|
export type { NewEntryParams } from './useNoteCreation'
|
2026-02-21 19:22:44 +01:00
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
export interface NoteActionsConfig {
|
2026-03-08 22:15:08 +01:00
|
|
|
addEntry: (entry: VaultEntry) => void
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
removeEntry: (path: string) => void
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
entries: VaultEntry[]
|
|
|
|
|
setToastMessage: (msg: string | null) => void
|
|
|
|
|
updateEntry: (path: string, patch: Partial<VaultEntry>) => void
|
2026-03-08 20:00:20 +01:00
|
|
|
vaultPath: string
|
2026-02-27 14:11:31 +01:00
|
|
|
addPendingSave?: (path: string) => void
|
|
|
|
|
removePendingSave?: (path: string) => void
|
2026-02-27 18:17:47 +01:00
|
|
|
trackUnsaved?: (path: string) => void
|
|
|
|
|
clearUnsaved?: (path: string) => void
|
|
|
|
|
unsavedPaths?: Set<string>
|
|
|
|
|
markContentPending?: (path: string, content: string) => void
|
2026-03-02 03:07:02 +01:00
|
|
|
onNewNotePersisted?: () => void
|
2026-03-08 22:15:08 +01:00
|
|
|
replaceEntry?: (oldPath: string, patch: Partial<VaultEntry> & { path: string }) => void
|
2026-03-08 21:37:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:44:44 +01:00
|
|
|
/** Check if a frontmatter key represents the note title. */
|
|
|
|
|
function isTitleKey(key: string): boolean {
|
|
|
|
|
return key.toLowerCase().replace(/\s+/g, '_') === 'title'
|
|
|
|
|
}
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const ENTRY_DELETE_MAP: Record<string, Partial<VaultEntry>> = {
|
2026-02-23 15:24:57 +01:00
|
|
|
type: { isA: null }, is_a: { isA: null }, status: { status: null }, color: { color: null },
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
icon: { icon: null }, owner: { owner: null }, cadence: { cadence: null },
|
|
|
|
|
aliases: { aliases: [] }, belongs_to: { belongsTo: [] }, related_to: { relatedTo: [] },
|
|
|
|
|
archived: { archived: false }, trashed: { trashed: false }, order: { order: null },
|
2026-03-07 00:20:51 +01:00
|
|
|
template: { template: null }, sort: { sort: null }, visible: { visible: null },
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */
|
|
|
|
|
export function frontmatterToEntryPatch(
|
|
|
|
|
op: 'update' | 'delete', key: string, value?: FrontmatterValue,
|
|
|
|
|
): Partial<VaultEntry> {
|
|
|
|
|
const k = key.toLowerCase().replace(/\s+/g, '_')
|
|
|
|
|
if (op === 'delete') return ENTRY_DELETE_MAP[k] ?? {}
|
|
|
|
|
const str = value != null ? String(value) : null
|
|
|
|
|
const arr = Array.isArray(value) ? value.map(String) : []
|
|
|
|
|
const updates: Record<string, Partial<VaultEntry>> = {
|
2026-02-23 15:24:57 +01:00
|
|
|
type: { isA: str }, is_a: { isA: str }, status: { status: str }, color: { color: str },
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
icon: { icon: str }, owner: { owner: str }, cadence: { cadence: str },
|
|
|
|
|
aliases: { aliases: arr }, belongs_to: { belongsTo: arr }, related_to: { relatedTo: arr },
|
|
|
|
|
archived: { archived: Boolean(value) }, trashed: { trashed: Boolean(value) },
|
|
|
|
|
order: { order: typeof value === 'number' ? value : null },
|
2026-03-02 08:37:06 +01:00
|
|
|
template: { template: str },
|
2026-03-03 11:22:04 +01:00
|
|
|
sort: { sort: str },
|
2026-03-05 15:26:21 +01:00
|
|
|
view: { view: str },
|
2026-03-07 00:20:51 +01:00
|
|
|
visible: { visible: value === false ? false : null },
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
}
|
|
|
|
|
return updates[k] ?? {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
async function invokeFrontmatter(command: string, args: Record<string, unknown>): Promise<string> {
|
|
|
|
|
return invoke<string>(command, args)
|
2026-02-27 14:11:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
function applyMockFrontmatterUpdate(path: string, key: string, value: FrontmatterValue): string {
|
|
|
|
|
const content = updateMockFrontmatter(path, key, value)
|
|
|
|
|
updateMockContent(path, content)
|
|
|
|
|
trackMockChange(path)
|
|
|
|
|
return content
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
function applyMockFrontmatterDelete(path: string, key: string): string {
|
|
|
|
|
const content = deleteMockFrontmatterProperty(path, key)
|
|
|
|
|
updateMockContent(path, content)
|
|
|
|
|
trackMockChange(path)
|
|
|
|
|
return content
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:55:45 +01:00
|
|
|
async function executeFrontmatterOp(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue): Promise<string> {
|
|
|
|
|
if (op === 'update') {
|
|
|
|
|
return isTauri() ? invokeFrontmatter('update_frontmatter', { path, key, value }) : applyMockFrontmatterUpdate(path, key, value!)
|
|
|
|
|
}
|
|
|
|
|
return isTauri() ? invokeFrontmatter('delete_frontmatter_property', { path, key }) : applyMockFrontmatterDelete(path, key)
|
|
|
|
|
}
|
|
|
|
|
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
/** Run a frontmatter update/delete and apply the result to state. */
|
|
|
|
|
async function runFrontmatterAndApply(
|
|
|
|
|
op: 'update' | 'delete', path: string, key: string, value: FrontmatterValue | undefined,
|
|
|
|
|
callbacks: { updateTab: (p: string, c: string) => void; updateEntry: (p: string, patch: Partial<VaultEntry>) => void; toast: (m: string | null) => void },
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
callbacks.updateTab(path, await executeFrontmatterOp(op, path, key, value))
|
|
|
|
|
const patch = frontmatterToEntryPatch(op, key, value)
|
|
|
|
|
if (Object.keys(patch).length > 0) callbacks.updateEntry(path, patch)
|
|
|
|
|
callbacks.toast(op === 'update' ? 'Property updated' : 'Property deleted')
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`Failed to ${op} frontmatter:`, err)
|
|
|
|
|
callbacks.toast(`Failed to ${op} property`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
interface TitleRenameDeps {
|
|
|
|
|
vaultPath: string
|
|
|
|
|
tabsRef: React.MutableRefObject<{ entry: VaultEntry; content: string }[]>
|
|
|
|
|
replaceEntry?: (oldPath: string, patch: Partial<VaultEntry> & { path: string }) => void
|
|
|
|
|
setTabs: React.Dispatch<React.SetStateAction<{ entry: VaultEntry; content: string }[]>>
|
|
|
|
|
activeTabPathRef: React.MutableRefObject<string | null>
|
|
|
|
|
handleSwitchTab: (path: string) => void
|
|
|
|
|
setToastMessage: (msg: string | null) => void
|
|
|
|
|
updateTabContent: (path: string, content: string) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** After a frontmatter title change, rename the file and update all tabs. */
|
|
|
|
|
async function renameAfterTitleChange(path: string, newTitle: string, deps: TitleRenameDeps): Promise<void> {
|
|
|
|
|
const oldTitle = deps.tabsRef.current.find(t => t.entry.path === path)?.entry.title
|
|
|
|
|
const result = await performRename(path, newTitle, deps.vaultPath, oldTitle)
|
|
|
|
|
if (result.new_path !== path) {
|
|
|
|
|
const newFilename = result.new_path.split('/').pop() ?? ''
|
|
|
|
|
deps.replaceEntry?.(path, { path: result.new_path, filename: newFilename, title: newTitle } as Partial<VaultEntry> & { path: string })
|
|
|
|
|
const newContent = await loadNoteContent(result.new_path)
|
|
|
|
|
deps.setTabs(prev => prev.map(t => t.entry.path === path
|
|
|
|
|
? { entry: { ...t.entry, path: result.new_path, filename: newFilename, title: newTitle }, content: newContent }
|
|
|
|
|
: t))
|
|
|
|
|
if (deps.activeTabPathRef.current === path) deps.handleSwitchTab(result.new_path)
|
|
|
|
|
const otherTabPaths = deps.tabsRef.current.filter(t => t.entry.path !== path && t.entry.path !== result.new_path).map(t => t.entry.path)
|
|
|
|
|
await reloadTabsAfterRename(otherTabPaths, deps.updateTabContent)
|
|
|
|
|
}
|
|
|
|
|
deps.setToastMessage(renameToastMessage(result.updated_files))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldRenameOnTitleUpdate(key: string, value: FrontmatterValue): value is string {
|
|
|
|
|
return isTitleKey(key) && typeof value === 'string' && value !== ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findWikilinkTarget(entries: VaultEntry[], target: string): VaultEntry | undefined {
|
|
|
|
|
return resolveEntry(entries, target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Navigate to a wikilink target, logging a warning if not found. */
|
|
|
|
|
function navigateWikilink(entries: VaultEntry[], target: string, selectNote: (e: VaultEntry) => void): void {
|
|
|
|
|
const found = findWikilinkTarget(entries, target)
|
|
|
|
|
if (found) selectNote(found)
|
|
|
|
|
else console.warn(`Navigation target not found: ${target}`)
|
|
|
|
|
}
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
export function useNoteActions(config: NoteActionsConfig) {
|
2026-03-16 18:31:33 +01:00
|
|
|
const { entries, setToastMessage, updateEntry } = config
|
2026-03-08 22:15:08 +01:00
|
|
|
const tabMgmt = useTabManagement()
|
2026-02-27 18:17:47 +01:00
|
|
|
const { setTabs, handleSelectNote, openTabWithContent, handleCloseTab, handleCloseTabRef, activeTabPathRef, handleSwitchTab } = tabMgmt
|
2026-02-17 12:10:21 +01:00
|
|
|
|
2026-02-22 10:11:52 +01:00
|
|
|
const updateTabContent = useCallback((path: string, newContent: string) => {
|
2026-02-22 10:55:45 +01:00
|
|
|
setTabs((prev) => prev.map((t) => t.entry.path === path ? { ...t, content: newContent } : t))
|
2026-03-08 22:15:08 +01:00
|
|
|
}, [setTabs])
|
2026-02-17 12:10:21 +01:00
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote, handleCloseTab, handleCloseTabRef })
|
|
|
|
|
const rename = useNoteRename(
|
|
|
|
|
{ entries, setToastMessage },
|
|
|
|
|
{ tabs: tabMgmt.tabs, setTabs, activeTabPathRef, handleSwitchTab, updateTabContent },
|
|
|
|
|
)
|
|
|
|
|
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
const handleNavigateWikilink = useCallback(
|
|
|
|
|
(target: string) => navigateWikilink(entries, target, handleSelectNote),
|
|
|
|
|
[entries, handleSelectNote],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const runFrontmatterOp = useCallback(
|
|
|
|
|
(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue) =>
|
2026-03-16 18:31:33 +01:00
|
|
|
runFrontmatterAndApply(op, path, key, value, { updateTab: updateTabContent, updateEntry, toast: setToastMessage }),
|
|
|
|
|
[updateTabContent, updateEntry, setToastMessage],
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
)
|
2026-02-17 12:10:21 +01:00
|
|
|
|
|
|
|
|
return {
|
2026-02-22 10:55:45 +01:00
|
|
|
...tabMgmt,
|
2026-03-16 18:31:33 +01:00
|
|
|
handleCloseTab: creation.handleCloseTabWithCleanup,
|
2026-02-17 12:10:21 +01:00
|
|
|
handleNavigateWikilink,
|
2026-03-16 18:31:33 +01:00
|
|
|
handleCreateNote: creation.handleCreateNote,
|
|
|
|
|
handleCreateNoteImmediate: creation.handleCreateNoteImmediate,
|
|
|
|
|
handleCreateNoteForRelationship: creation.handleCreateNoteForRelationship,
|
|
|
|
|
handleOpenDailyNote: creation.handleOpenDailyNote,
|
|
|
|
|
handleCreateType: creation.handleCreateType,
|
|
|
|
|
createTypeEntrySilent: creation.createTypeEntrySilent,
|
2026-03-08 21:37:27 +01:00
|
|
|
handleUpdateFrontmatter: useCallback(async (path: string, key: string, value: FrontmatterValue) => {
|
|
|
|
|
await runFrontmatterOp('update', path, key, value)
|
2026-03-16 18:31:33 +01:00
|
|
|
if (shouldRenameOnTitleUpdate(key, value)) {
|
2026-03-11 18:44:44 +01:00
|
|
|
try {
|
2026-03-16 18:31:33 +01:00
|
|
|
await renameAfterTitleChange(path, value, {
|
|
|
|
|
vaultPath: config.vaultPath, tabsRef: rename.tabsRef, replaceEntry: config.replaceEntry,
|
|
|
|
|
setTabs, activeTabPathRef, handleSwitchTab, setToastMessage, updateTabContent,
|
|
|
|
|
})
|
2026-03-11 18:44:44 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to rename note after title change:', err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 18:31:33 +01:00
|
|
|
}, [runFrontmatterOp, config.vaultPath, config.replaceEntry, rename.tabsRef, setTabs, activeTabPathRef, handleSwitchTab, setToastMessage, updateTabContent]),
|
2026-02-22 10:55:45 +01:00
|
|
|
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]),
|
2026-03-16 18:31:33 +01:00
|
|
|
handleRenameNote: rename.handleRenameNote,
|
2026-02-17 12:10:21 +01:00
|
|
|
}
|
|
|
|
|
}
|