2026-03-16 18:31:33 +01:00
|
|
|
import { useCallback } from 'react'
|
2026-02-17 12:10:21 +01:00
|
|
|
import type { VaultEntry } from '../types'
|
|
|
|
|
import type { FrontmatterValue } from '../components/Inspector'
|
2026-04-07 20:43:13 +02:00
|
|
|
import { useTabManagement } from './useTabManagement'
|
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,
|
2026-04-13 17:44:41 +02:00
|
|
|
performRename, loadNoteContent, renameToastMessage, reloadTabsAfterRename, reloadVaultAfterRename,
|
2026-03-16 18:31:33 +01:00
|
|
|
} from './useNoteRename'
|
2026-03-18 13:32:07 +01:00
|
|
|
import { runFrontmatterAndApply, type FrontmatterOpOptions } from './frontmatterOps'
|
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[]
|
2026-04-18 09:19:29 +02:00
|
|
|
flushBeforeNoteSwitch?: (path: string) => Promise<void>
|
2026-04-21 18:50:50 +02:00
|
|
|
flushBeforeFrontmatterChange?: (path: string) => Promise<void>
|
2026-04-14 16:49:18 +02:00
|
|
|
flushBeforePathRename?: (path: string) => Promise<void>
|
2026-04-13 17:44:41 +02:00
|
|
|
reloadVault?: () => Promise<unknown>
|
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
|
|
|
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-04-14 15:40:46 +02:00
|
|
|
onPathRenamed?: (oldPath: string, newPath: string) => void
|
2026-03-19 00:57:34 +01:00
|
|
|
/** Called after frontmatter is written to disk — used for live-reloading theme CSS vars. */
|
|
|
|
|
onFrontmatterContentChanged?: (path: string, content: string) => void
|
2026-04-09 13:29:33 +02:00
|
|
|
/** Called after a frontmatter mutation is fully persisted, including follow-up renames. */
|
|
|
|
|
onFrontmatterPersisted?: () => void
|
2026-03-08 21:37:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:44:44 +01:00
|
|
|
function isTitleKey(key: string): boolean {
|
|
|
|
|
return key.toLowerCase().replace(/\s+/g, '_') === 'title'
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
interface TitleRenameDeps {
|
|
|
|
|
vaultPath: string
|
|
|
|
|
tabsRef: React.MutableRefObject<{ entry: VaultEntry; content: string }[]>
|
2026-04-13 17:44:41 +02:00
|
|
|
reloadVault?: () => Promise<unknown>
|
2026-03-16 18:31:33 +01:00
|
|
|
replaceEntry?: (oldPath: string, patch: Partial<VaultEntry> & { path: string }) => void
|
2026-04-14 15:40:46 +02:00
|
|
|
onPathRenamed?: (oldPath: string, newPath: string) => void
|
2026-03-16 18:31:33 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:14:03 +02:00
|
|
|
interface FrontmatterCallbackParams {
|
|
|
|
|
config: NoteActionsConfig
|
|
|
|
|
path: string
|
|
|
|
|
newContent: string | undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyFrontmatterCallbacks({ config, path, newContent }: FrontmatterCallbackParams): boolean {
|
2026-04-09 13:29:33 +02:00
|
|
|
if (!newContent) return false
|
|
|
|
|
config.onFrontmatterContentChanged?.(path, newContent)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:14:03 +02:00
|
|
|
interface RenameAfterTitleChangeParams {
|
|
|
|
|
path: string
|
|
|
|
|
newTitle: string
|
|
|
|
|
deps: TitleRenameDeps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renameAfterTitleChange({ path, newTitle, deps }: RenameAfterTitleChangeParams): Promise<void> {
|
2026-03-16 18:31:33 +01:00
|
|
|
const oldTitle = deps.tabsRef.current.find(t => t.entry.path === path)?.entry.title
|
2026-04-13 17:44:41 +02:00
|
|
|
const result = await performRename({ path, newTitle, vaultPath: deps.vaultPath, oldTitle })
|
2026-03-16 18:31:33 +01:00
|
|
|
if (result.new_path !== path) {
|
|
|
|
|
const newFilename = result.new_path.split('/').pop() ?? ''
|
2026-04-14 15:40:46 +02:00
|
|
|
deps.onPathRenamed?.(path, result.new_path)
|
2026-03-16 18:31:33 +01:00
|
|
|
deps.replaceEntry?.(path, { path: result.new_path, filename: newFilename, title: newTitle } as Partial<VaultEntry> & { path: string })
|
2026-04-13 17:44:41 +02:00
|
|
|
const newContent = await loadNoteContent({ path: result.new_path })
|
2026-03-16 18:31:33 +01:00
|
|
|
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)
|
2026-04-13 17:44:41 +02:00
|
|
|
await reloadTabsAfterRename({ tabPaths: otherTabPaths, updateTabContent: deps.updateTabContent })
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
2026-04-13 17:44:41 +02:00
|
|
|
await reloadVaultAfterRename(deps.reloadVault)
|
2026-03-16 18:31:33 +01:00
|
|
|
deps.setToastMessage(renameToastMessage(result.updated_files))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldRenameOnTitleUpdate(key: string, value: FrontmatterValue): value is string {
|
|
|
|
|
return isTitleKey(key) && typeof value === 'string' && value !== ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:14:03 +02:00
|
|
|
interface NavigateWikilinkParams {
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
target: string
|
|
|
|
|
selectNote: (entry: VaultEntry) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navigateWikilink({ entries, target, selectNote }: NavigateWikilinkParams): void {
|
2026-03-16 23:34:45 +01:00
|
|
|
const found = resolveEntry(entries, target)
|
2026-03-16 18:31:33 +01:00
|
|
|
if (found) selectNote(found)
|
|
|
|
|
else console.warn(`Navigation target not found: ${target}`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:14:03 +02:00
|
|
|
interface MaybeRenameAfterFrontmatterUpdateParams {
|
|
|
|
|
path: string
|
|
|
|
|
key: string
|
|
|
|
|
value: FrontmatterValue
|
|
|
|
|
deps: TitleRenameDeps
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:49:18 +02:00
|
|
|
async function flushBeforeTitleRename(
|
|
|
|
|
path: string,
|
|
|
|
|
key: string,
|
|
|
|
|
value: FrontmatterValue,
|
|
|
|
|
flushBeforePathRename?: (path: string) => Promise<void>,
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
if (!shouldRenameOnTitleUpdate(key, value) || !flushBeforePathRename) return true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await flushBeforePathRename(path)
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 18:50:50 +02:00
|
|
|
async function flushBeforeFrontmatterMutation(
|
|
|
|
|
path: string,
|
|
|
|
|
flushBeforeFrontmatterChange?: (path: string) => Promise<void>,
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
if (!flushBeforeFrontmatterChange) return true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await flushBeforeFrontmatterChange(path)
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:14:03 +02:00
|
|
|
async function maybeRenameAfterFrontmatterUpdate({
|
|
|
|
|
path,
|
|
|
|
|
key,
|
|
|
|
|
value,
|
|
|
|
|
deps,
|
|
|
|
|
}: MaybeRenameAfterFrontmatterUpdateParams): Promise<void> {
|
2026-04-09 13:29:33 +02:00
|
|
|
if (!shouldRenameOnTitleUpdate(key, value)) return
|
|
|
|
|
try {
|
2026-04-12 23:14:03 +02:00
|
|
|
await renameAfterTitleChange({ path, newTitle: value, deps })
|
2026-04-09 13:29:33 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to rename note after title change:', err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:49:18 +02:00
|
|
|
interface UpdateFrontmatterAndMaybeRenameParams {
|
|
|
|
|
config: NoteActionsConfig
|
|
|
|
|
deps: TitleRenameDeps
|
|
|
|
|
key: string
|
|
|
|
|
options?: FrontmatterOpOptions
|
|
|
|
|
path: string
|
|
|
|
|
runFrontmatterOp: (
|
|
|
|
|
op: 'update' | 'delete',
|
|
|
|
|
path: string,
|
|
|
|
|
key: string,
|
|
|
|
|
value?: FrontmatterValue,
|
|
|
|
|
options?: FrontmatterOpOptions,
|
|
|
|
|
) => Promise<string | undefined>
|
|
|
|
|
value: FrontmatterValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateFrontmatterAndMaybeRename({
|
|
|
|
|
config,
|
|
|
|
|
deps,
|
|
|
|
|
key,
|
|
|
|
|
options,
|
|
|
|
|
path,
|
|
|
|
|
runFrontmatterOp,
|
|
|
|
|
value,
|
|
|
|
|
}: UpdateFrontmatterAndMaybeRenameParams): Promise<void> {
|
2026-04-21 18:50:50 +02:00
|
|
|
const canFlush = await flushBeforeFrontmatterMutation(path, config.flushBeforeFrontmatterChange)
|
|
|
|
|
if (!canFlush) return
|
|
|
|
|
|
2026-04-14 16:49:18 +02:00
|
|
|
const canRename = await flushBeforeTitleRename(path, key, value, config.flushBeforePathRename)
|
|
|
|
|
if (!canRename) return
|
|
|
|
|
|
|
|
|
|
const newContent = await runFrontmatterOp('update', path, key, value, options)
|
|
|
|
|
if (!applyFrontmatterCallbacks({ config, path, newContent })) return
|
|
|
|
|
|
|
|
|
|
await maybeRenameAfterFrontmatterUpdate({ path, key, value, deps })
|
|
|
|
|
config.onFrontmatterPersisted?.()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
function buildTabManagementOptions(
|
|
|
|
|
flushBeforeNoteSwitch?: NoteActionsConfig['flushBeforeNoteSwitch'],
|
|
|
|
|
) {
|
|
|
|
|
return flushBeforeNoteSwitch
|
|
|
|
|
? { beforeNavigate: (fromPath: string) => flushBeforeNoteSwitch(fromPath) }
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useFrontmatterActionHandlers({
|
|
|
|
|
config,
|
|
|
|
|
renameTabsRef,
|
|
|
|
|
setTabs,
|
|
|
|
|
activeTabPathRef,
|
|
|
|
|
handleSwitchTab,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
updateTabContent,
|
|
|
|
|
runFrontmatterOp,
|
|
|
|
|
}: {
|
|
|
|
|
config: NoteActionsConfig
|
|
|
|
|
renameTabsRef: TitleRenameDeps['tabsRef']
|
|
|
|
|
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, newContent: string) => void
|
|
|
|
|
runFrontmatterOp: (
|
|
|
|
|
op: 'update' | 'delete',
|
|
|
|
|
path: string,
|
|
|
|
|
key: string,
|
|
|
|
|
value?: FrontmatterValue,
|
|
|
|
|
options?: FrontmatterOpOptions,
|
|
|
|
|
) => Promise<string | undefined>
|
|
|
|
|
}) {
|
|
|
|
|
const handleUpdateFrontmatter = useCallback(async (
|
|
|
|
|
path: string,
|
|
|
|
|
key: string,
|
|
|
|
|
value: FrontmatterValue,
|
|
|
|
|
options?: FrontmatterOpOptions,
|
|
|
|
|
) => {
|
|
|
|
|
await updateFrontmatterAndMaybeRename({
|
|
|
|
|
config,
|
|
|
|
|
deps: {
|
|
|
|
|
vaultPath: config.vaultPath,
|
|
|
|
|
tabsRef: renameTabsRef,
|
|
|
|
|
reloadVault: config.reloadVault,
|
|
|
|
|
replaceEntry: config.replaceEntry,
|
|
|
|
|
onPathRenamed: config.onPathRenamed,
|
|
|
|
|
setTabs,
|
|
|
|
|
activeTabPathRef,
|
|
|
|
|
handleSwitchTab,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
updateTabContent,
|
|
|
|
|
},
|
|
|
|
|
path,
|
|
|
|
|
key,
|
|
|
|
|
value,
|
|
|
|
|
options,
|
|
|
|
|
runFrontmatterOp,
|
|
|
|
|
})
|
|
|
|
|
}, [activeTabPathRef, config, handleSwitchTab, renameTabsRef, runFrontmatterOp, setTabs, setToastMessage, updateTabContent])
|
|
|
|
|
|
|
|
|
|
const handleDeleteProperty = useCallback(async (path: string, key: string, options?: FrontmatterOpOptions) => {
|
2026-04-21 18:50:50 +02:00
|
|
|
const canFlush = await flushBeforeFrontmatterMutation(path, config.flushBeforeFrontmatterChange)
|
|
|
|
|
if (!canFlush) return
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
const newContent = await runFrontmatterOp('delete', path, key, undefined, options)
|
|
|
|
|
if (!applyFrontmatterCallbacks({ config, path, newContent })) return
|
|
|
|
|
config.onFrontmatterPersisted?.()
|
|
|
|
|
}, [config, runFrontmatterOp])
|
|
|
|
|
|
|
|
|
|
const handleAddProperty = useCallback(async (path: string, key: string, value: FrontmatterValue) => {
|
2026-04-21 18:50:50 +02:00
|
|
|
const canFlush = await flushBeforeFrontmatterMutation(path, config.flushBeforeFrontmatterChange)
|
|
|
|
|
if (!canFlush) return
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
const newContent = await runFrontmatterOp('update', path, key, value)
|
|
|
|
|
if (!applyFrontmatterCallbacks({ config, path, newContent })) return
|
|
|
|
|
config.onFrontmatterPersisted?.()
|
|
|
|
|
}, [config, runFrontmatterOp])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
handleUpdateFrontmatter,
|
|
|
|
|
handleDeleteProperty,
|
|
|
|
|
handleAddProperty,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-04-18 09:19:29 +02:00
|
|
|
const tabMgmt = useTabManagement(buildTabManagementOptions(config.flushBeforeNoteSwitch))
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
const { setTabs, handleSelectNote, openTabWithContent, 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-04-12 23:14:03 +02:00
|
|
|
const creation = useNoteCreation(config, { openTabWithContent })
|
2026-03-16 18:31:33 +01:00
|
|
|
const rename = useNoteRename(
|
2026-04-13 17:44:41 +02:00
|
|
|
{ entries, setToastMessage, reloadVault: config.reloadVault },
|
2026-03-16 18:31:33 +01:00
|
|
|
{ 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(
|
2026-04-12 23:14:03 +02:00
|
|
|
(target: string) => navigateWikilink({ entries, target, selectNote: handleSelectNote }),
|
2026-04-07 20:43:13 +02:00
|
|
|
[entries, handleSelectNote],
|
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 runFrontmatterOp = useCallback(
|
2026-03-18 13:32:07 +01:00
|
|
|
(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue, options?: FrontmatterOpOptions) =>
|
2026-03-20 03:26:52 +01:00
|
|
|
runFrontmatterAndApply(op, path, key, value, { updateTab: updateTabContent, updateEntry, toast: setToastMessage, getEntry: (p) => entries.find((e) => e.path === p) }, options),
|
|
|
|
|
[updateTabContent, updateEntry, setToastMessage, entries],
|
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-04-18 09:19:29 +02:00
|
|
|
const frontmatterActions = useFrontmatterActionHandlers({
|
|
|
|
|
config,
|
|
|
|
|
renameTabsRef: rename.tabsRef,
|
|
|
|
|
setTabs,
|
|
|
|
|
activeTabPathRef,
|
|
|
|
|
handleSwitchTab,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
updateTabContent,
|
|
|
|
|
runFrontmatterOp,
|
|
|
|
|
})
|
2026-02-17 12:10:21 +01:00
|
|
|
|
|
|
|
|
return {
|
2026-02-22 10:55:45 +01:00
|
|
|
...tabMgmt,
|
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,
|
|
|
|
|
handleCreateType: creation.handleCreateType,
|
|
|
|
|
createTypeEntrySilent: creation.createTypeEntrySilent,
|
2026-04-18 09:19:29 +02:00
|
|
|
handleUpdateFrontmatter: frontmatterActions.handleUpdateFrontmatter,
|
|
|
|
|
handleDeleteProperty: frontmatterActions.handleDeleteProperty,
|
|
|
|
|
handleAddProperty: frontmatterActions.handleAddProperty,
|
2026-03-16 18:31:33 +01:00
|
|
|
handleRenameNote: rename.handleRenameNote,
|
2026-04-10 17:59:21 +02:00
|
|
|
handleRenameFilename: rename.handleRenameFilename,
|
2026-02-17 12:10:21 +01:00
|
|
|
}
|
|
|
|
|
}
|