Write operations now use underscore-prefixed canonical keys (_archived, _trashed, _trashed_at). Read operations accept both old keys (Archived, archived, Trashed, trashed, Trashed at, trashed_at) and new keys via serde aliases, ensuring backward compatibility without migration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
8.6 KiB
TypeScript
178 lines
8.6 KiB
TypeScript
import { useCallback } from 'react'
|
|
import type { VaultEntry } from '../types'
|
|
import type { FrontmatterOpOptions } from './frontmatterOps'
|
|
|
|
interface EntryActionsConfig {
|
|
entries: VaultEntry[]
|
|
updateEntry: (path: string, updates: Partial<VaultEntry>) => void
|
|
handleUpdateFrontmatter: (path: string, key: string, value: string | number | boolean | string[], options?: FrontmatterOpOptions) => Promise<void>
|
|
handleDeleteProperty: (path: string, key: string, options?: FrontmatterOpOptions) => Promise<void>
|
|
setToastMessage: (msg: string | null) => void
|
|
createTypeEntry: (typeName: string) => Promise<VaultEntry>
|
|
onFrontmatterPersisted?: () => void
|
|
/** Called before trash/archive to flush unsaved editor content to disk. */
|
|
onBeforeAction?: (path: string) => Promise<void>
|
|
}
|
|
|
|
function findTypeEntry(entries: VaultEntry[], typeName: string): VaultEntry | undefined {
|
|
return entries.find((e) => e.isA === 'Type' && e.title === typeName)
|
|
}
|
|
|
|
async function findOrCreateType(
|
|
entries: VaultEntry[], typeName: string, create: (name: string) => Promise<VaultEntry>,
|
|
): Promise<VaultEntry> {
|
|
return findTypeEntry(entries, typeName) ?? await create(typeName)
|
|
}
|
|
|
|
export function useEntryActions({
|
|
entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, createTypeEntry, onFrontmatterPersisted, onBeforeAction,
|
|
}: EntryActionsConfig) {
|
|
const handleTrashNote = useCallback(async (path: string) => {
|
|
await onBeforeAction?.(path)
|
|
// Optimistic: update UI immediately, write to disk async with rollback on failure
|
|
const trashedAt = Date.now() / 1000
|
|
updateEntry(path, { trashed: true, trashedAt })
|
|
setToastMessage('Note moved to trash')
|
|
const now = new Date().toISOString().slice(0, 10)
|
|
try {
|
|
await handleUpdateFrontmatter(path, '_trashed', true, { silent: true })
|
|
await handleUpdateFrontmatter(path, '_trashed_at', now, { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch (err) {
|
|
updateEntry(path, { trashed: false, trashedAt: null })
|
|
setToastMessage('Failed to trash note — rolled back')
|
|
console.error('Optimistic trash rollback:', err)
|
|
}
|
|
}, [onBeforeAction, handleUpdateFrontmatter, updateEntry, setToastMessage, onFrontmatterPersisted])
|
|
|
|
const handleRestoreNote = useCallback(async (path: string) => {
|
|
// Optimistic: update UI immediately
|
|
updateEntry(path, { trashed: false, trashedAt: null })
|
|
setToastMessage('Note restored from trash')
|
|
try {
|
|
await handleDeleteProperty(path, '_trashed', { silent: true })
|
|
await handleDeleteProperty(path, '_trashed_at', { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch (err) {
|
|
updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 })
|
|
setToastMessage('Failed to restore note — rolled back')
|
|
console.error('Optimistic restore rollback:', err)
|
|
}
|
|
}, [handleDeleteProperty, updateEntry, setToastMessage, onFrontmatterPersisted])
|
|
|
|
const handleArchiveNote = useCallback(async (path: string) => {
|
|
await onBeforeAction?.(path)
|
|
// Optimistic: update UI immediately, write to disk async with rollback on failure
|
|
updateEntry(path, { archived: true })
|
|
setToastMessage('Note archived')
|
|
try {
|
|
await handleUpdateFrontmatter(path, '_archived', true, { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch (err) {
|
|
updateEntry(path, { archived: false })
|
|
setToastMessage('Failed to archive note — rolled back')
|
|
console.error('Optimistic archive rollback:', err)
|
|
}
|
|
}, [onBeforeAction, handleUpdateFrontmatter, updateEntry, setToastMessage, onFrontmatterPersisted])
|
|
|
|
const handleUnarchiveNote = useCallback(async (path: string) => {
|
|
// Optimistic: update UI immediately
|
|
updateEntry(path, { archived: false })
|
|
setToastMessage('Note unarchived')
|
|
try {
|
|
await handleDeleteProperty(path, '_archived', { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch (err) {
|
|
updateEntry(path, { archived: true })
|
|
setToastMessage('Failed to unarchive note — rolled back')
|
|
console.error('Optimistic unarchive rollback:', err)
|
|
}
|
|
}, [handleDeleteProperty, updateEntry, setToastMessage, onFrontmatterPersisted])
|
|
|
|
const handleCustomizeType = useCallback(async (typeName: string, icon: string, color: string) => {
|
|
const typeEntry = await findOrCreateType(entries, typeName, createTypeEntry)
|
|
await handleUpdateFrontmatter(typeEntry.path, 'icon', icon)
|
|
await handleUpdateFrontmatter(typeEntry.path, 'color', color)
|
|
updateEntry(typeEntry.path, { icon, color })
|
|
onFrontmatterPersisted?.()
|
|
}, [entries, handleUpdateFrontmatter, updateEntry, createTypeEntry, onFrontmatterPersisted])
|
|
|
|
const handleReorderSections = useCallback(async (orderedTypes: { typeName: string; order: number }[]) => {
|
|
for (const { typeName, order } of orderedTypes) {
|
|
const typeEntry = await findOrCreateType(entries, typeName, createTypeEntry)
|
|
await handleUpdateFrontmatter(typeEntry.path, 'order', order)
|
|
updateEntry(typeEntry.path, { order })
|
|
}
|
|
onFrontmatterPersisted?.()
|
|
}, [entries, handleUpdateFrontmatter, updateEntry, createTypeEntry, onFrontmatterPersisted])
|
|
|
|
const handleUpdateTypeTemplate = useCallback(async (typeName: string, template: string) => {
|
|
const typeEntry = await findOrCreateType(entries, typeName, createTypeEntry)
|
|
await handleUpdateFrontmatter(typeEntry.path, 'template', template)
|
|
updateEntry(typeEntry.path, { template: template || null })
|
|
onFrontmatterPersisted?.()
|
|
}, [entries, handleUpdateFrontmatter, updateEntry, createTypeEntry, onFrontmatterPersisted])
|
|
|
|
const handleRenameSection = useCallback(async (typeName: string, label: string) => {
|
|
const typeEntry = await findOrCreateType(entries, typeName, createTypeEntry)
|
|
const trimmed = label.trim()
|
|
if (trimmed) {
|
|
await handleUpdateFrontmatter(typeEntry.path, 'sidebar label', trimmed)
|
|
} else {
|
|
await handleDeleteProperty(typeEntry.path, 'sidebar label')
|
|
}
|
|
updateEntry(typeEntry.path, { sidebarLabel: trimmed || null })
|
|
onFrontmatterPersisted?.()
|
|
}, [entries, handleUpdateFrontmatter, handleDeleteProperty, updateEntry, createTypeEntry, onFrontmatterPersisted])
|
|
|
|
const handleToggleFavorite = useCallback(async (path: string) => {
|
|
const entry = entries.find((e) => e.path === path)
|
|
if (!entry) return
|
|
if (entry.favorite) {
|
|
updateEntry(path, { favorite: false, favoriteIndex: null })
|
|
try {
|
|
await handleDeleteProperty(path, '_favorite', { silent: true })
|
|
await handleDeleteProperty(path, '_favorite_index', { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch {
|
|
updateEntry(path, { favorite: true, favoriteIndex: entry.favoriteIndex })
|
|
setToastMessage('Failed to unfavorite — rolled back')
|
|
}
|
|
} else {
|
|
const maxIndex = entries.filter((e) => e.favorite).reduce((max, e) => Math.max(max, e.favoriteIndex ?? 0), 0)
|
|
const newIndex = maxIndex + 1
|
|
updateEntry(path, { favorite: true, favoriteIndex: newIndex })
|
|
try {
|
|
await handleUpdateFrontmatter(path, '_favorite', true, { silent: true })
|
|
await handleUpdateFrontmatter(path, '_favorite_index', newIndex, { silent: true })
|
|
onFrontmatterPersisted?.()
|
|
} catch {
|
|
updateEntry(path, { favorite: false, favoriteIndex: null })
|
|
setToastMessage('Failed to favorite — rolled back')
|
|
}
|
|
}
|
|
}, [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted])
|
|
|
|
const handleReorderFavorites = useCallback(async (orderedPaths: string[]) => {
|
|
for (let i = 0; i < orderedPaths.length; i++) {
|
|
updateEntry(orderedPaths[i], { favoriteIndex: i })
|
|
await handleUpdateFrontmatter(orderedPaths[i], '_favorite_index', i, { silent: true })
|
|
}
|
|
onFrontmatterPersisted?.()
|
|
}, [updateEntry, handleUpdateFrontmatter, onFrontmatterPersisted])
|
|
|
|
const handleToggleTypeVisibility = useCallback(async (typeName: string) => {
|
|
const typeEntry = await findOrCreateType(entries, typeName, createTypeEntry)
|
|
if (typeEntry.visible === false) {
|
|
await handleDeleteProperty(typeEntry.path, 'visible')
|
|
updateEntry(typeEntry.path, { visible: null })
|
|
} else {
|
|
await handleUpdateFrontmatter(typeEntry.path, 'visible', false)
|
|
updateEntry(typeEntry.path, { visible: false })
|
|
}
|
|
onFrontmatterPersisted?.()
|
|
}, [entries, handleUpdateFrontmatter, handleDeleteProperty, updateEntry, createTypeEntry, onFrontmatterPersisted])
|
|
|
|
return { handleTrashNote, handleRestoreNote, handleArchiveNote, handleUnarchiveNote, handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility, handleToggleFavorite, handleReorderFavorites }
|
|
}
|