import { useCallback, useMemo } from 'react' import type { VaultEntry } from '../types' import { isMissingFrontmatterTargetError, type FrontmatterOpOptions } from './frontmatterOps' import { trackEvent } from '../lib/telemetry' interface EntryActionsConfig { entries: VaultEntry[] updateEntry: (path: string, updates: Partial) => void handleUpdateFrontmatter: (path: string, key: string, value: string | number | boolean | string[], options?: FrontmatterOpOptions) => Promise handleDeleteProperty: (path: string, key: string, options?: FrontmatterOpOptions) => Promise setToastMessage: (msg: string | null) => void createTypeEntry: (typeName: string) => Promise onFrontmatterPersisted?: () => void /** Called before trash/archive to flush unsaved editor content to disk. */ onBeforeAction?: (path: string) => Promise } type ArchiveActionDeps = Pick type TypeActionDeps = Pick type EntryStateActionDeps = Pick type ReorderFavoritesDeps = Pick interface CustomizeTypeArgs { typeName: string icon: string color: string } interface ReorderTypeSectionsArgs { orderedTypes: { typeName: string; order: number }[] } interface UpdateTypeTemplateArgs { typeName: string template: string } interface RenameTypeSectionArgs { typeName: string label: string } function findTypeEntry(entries: VaultEntry[], typeName: string, typeEntryPath?: string): VaultEntry | undefined { if (typeEntryPath) { const entry = entries.find((candidate) => candidate.path === typeEntryPath) if (entry?.isA === 'Type') return entry } return entries.find((entry) => entry.isA === 'Type' && entry.title === typeName) } function logOptimisticRollback(label: string, error: unknown): void { if (isMissingFrontmatterTargetError(error)) { console.warn(label, error) return } console.error(label, error) } async function findOrCreateType( deps: Pick, typeName: string, typeEntryPath?: string, ): Promise { const existingType = findTypeEntry(deps.entries, typeName, typeEntryPath) if (existingType) return existingType if (typeEntryPath) return null try { return await deps.createTypeEntry(typeName) } catch { return null } } async function customizeTypeEntry(deps: TypeActionDeps, args: CustomizeTypeArgs): Promise { const typeEntry = await findOrCreateType(deps, args.typeName) if (!typeEntry) return await deps.handleUpdateFrontmatter(typeEntry.path, 'icon', args.icon) await deps.handleUpdateFrontmatter(typeEntry.path, 'color', args.color) deps.updateEntry(typeEntry.path, { icon: args.icon, color: args.color }) deps.onFrontmatterPersisted?.() } async function reorderTypeSections(deps: TypeActionDeps, args: ReorderTypeSectionsArgs): Promise { for (const { typeName, order } of args.orderedTypes) { const typeEntry = await findOrCreateType(deps, typeName) if (!typeEntry) return await deps.handleUpdateFrontmatter(typeEntry.path, 'order', order) deps.updateEntry(typeEntry.path, { order }) } deps.onFrontmatterPersisted?.() } async function updateTypeTemplate(deps: TypeActionDeps, args: UpdateTypeTemplateArgs): Promise { const typeEntry = await findOrCreateType(deps, args.typeName) if (!typeEntry) return await deps.handleUpdateFrontmatter(typeEntry.path, 'template', args.template) deps.updateEntry(typeEntry.path, { template: args.template || null }) deps.onFrontmatterPersisted?.() } async function renameTypeSection(deps: TypeActionDeps, args: RenameTypeSectionArgs): Promise { const typeEntry = await findOrCreateType(deps, args.typeName) if (!typeEntry) return const trimmed = args.label.trim() if (trimmed) { await deps.handleUpdateFrontmatter(typeEntry.path, 'sidebar label', trimmed) } else { await deps.handleDeleteProperty(typeEntry.path, 'sidebar label') } deps.updateEntry(typeEntry.path, { sidebarLabel: trimmed || null }) deps.onFrontmatterPersisted?.() } async function toggleTypeVisibility(deps: TypeActionDeps, typeName: string, typeEntryPath?: string): Promise { const typeEntry = await findOrCreateType(deps, typeName, typeEntryPath) if (!typeEntry) return if (typeEntry.visible === false) { await deps.handleDeleteProperty(typeEntry.path, 'visible') deps.updateEntry(typeEntry.path, { visible: null }) } else { await deps.handleUpdateFrontmatter(typeEntry.path, 'visible', false) deps.updateEntry(typeEntry.path, { visible: false }) } deps.onFrontmatterPersisted?.() } function useArchiveActions({ updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted, onBeforeAction, }: ArchiveActionDeps) { 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 }) trackEvent('note_archived') 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') logOptimisticRollback('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') logOptimisticRollback('Optimistic unarchive rollback:', err) } }, [handleDeleteProperty, updateEntry, setToastMessage, onFrontmatterPersisted]) return { handleArchiveNote, handleUnarchiveNote } } function useTypeActions(deps: TypeActionDeps) { const { entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, createTypeEntry, onFrontmatterPersisted, } = deps const typeActionDeps = useMemo(() => ({ entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, createTypeEntry, onFrontmatterPersisted, }), [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, createTypeEntry, onFrontmatterPersisted]) const handleCustomizeType = useCallback(async (typeName: string, icon: string, color: string) => { await customizeTypeEntry(typeActionDeps, { typeName, icon, color }) }, [typeActionDeps]) const handleReorderSections = useCallback(async (orderedTypes: { typeName: string; order: number }[]) => { await reorderTypeSections(typeActionDeps, { orderedTypes }) }, [typeActionDeps]) const handleUpdateTypeTemplate = useCallback(async (typeName: string, template: string) => { await updateTypeTemplate(typeActionDeps, { typeName, template }) }, [typeActionDeps]) const handleRenameSection = useCallback(async (typeName: string, label: string) => { await renameTypeSection(typeActionDeps, { typeName, label }) }, [typeActionDeps]) const handleToggleTypeVisibility = useCallback(async (typeName: string, typeEntryPath?: string) => { await toggleTypeVisibility(typeActionDeps, typeName, typeEntryPath) }, [typeActionDeps]) return { handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility } } function useFavoriteAction({ entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted, }: EntryStateActionDeps) { return useCallback(async (path: string) => { const entry = entries.find((candidate) => candidate.path === path) if (!entry) return if (entry.favorite) { trackEvent('note_unfavorited') 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 { trackEvent('note_favorited') const maxIndex = entries.filter((candidate) => candidate.favorite).reduce((max, candidate) => Math.max(max, candidate.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]) } function useOrganizedAction({ entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted, }: EntryStateActionDeps) { return useCallback(async (path: string) => { const entry = entries.find((candidate) => candidate.path === path) if (!entry) return false if (entry.organized) { trackEvent('note_unorganized') updateEntry(path, { organized: false }) try { await handleDeleteProperty(path, '_organized', { silent: true }) onFrontmatterPersisted?.() return true } catch { updateEntry(path, { organized: true }) setToastMessage('Failed to unorganize — rolled back') return false } } trackEvent('note_organized') updateEntry(path, { organized: true }) try { await handleUpdateFrontmatter(path, '_organized', true, { silent: true }) onFrontmatterPersisted?.() return true } catch { updateEntry(path, { organized: false }) setToastMessage('Failed to organize — rolled back') return false } }, [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted]) } function useReorderFavoritesAction({ updateEntry, handleUpdateFrontmatter, onFrontmatterPersisted }: ReorderFavoritesDeps) { return useCallback(async (orderedPaths: string[]) => { for (let i = 0; i < orderedPaths.length; i++) { const orderedPath = orderedPaths.at(i) if (!orderedPath) continue updateEntry(orderedPath, { favoriteIndex: i }) await handleUpdateFrontmatter(orderedPath, '_favorite_index', i, { silent: true }) } onFrontmatterPersisted?.() }, [updateEntry, handleUpdateFrontmatter, onFrontmatterPersisted]) } export function useEntryActions(config: EntryActionsConfig) { const archiveActions = useArchiveActions(config) const typeActions = useTypeActions(config) const handleToggleFavorite = useFavoriteAction(config) const handleToggleOrganized = useOrganizedAction(config) const handleReorderFavorites = useReorderFavoritesAction(config) return { ...archiveActions, ...typeActions, handleToggleFavorite, handleToggleOrganized, handleReorderFavorites, } }