import { useCallback, useMemo } from 'react' import type { FolderNode, SidebarSelection, VaultEntry } from '../types' import type { FrontmatterOpOptions } from './frontmatterOps' import { extractVaultTypes } from '../utils/vaultTypes' type RetargetResult = 'updated' | 'noop' | 'error' export interface RetargetFolderOption { path: string label: string } interface NoteRetargetingInput { entries: VaultEntry[] folders: FolderNode[] selection: SidebarSelection setSelection: (selection: SidebarSelection) => void setToastMessage: (message: string | null) => void vaultPath: string updateFrontmatter: ( path: string, key: string, value: string, options?: FrontmatterOpOptions, ) => Promise moveNoteToFolder: ( path: string, folderPath: string, vaultPath: string, onEntryRenamed: ( oldPath: string, newEntry: Partial & { path: string }, newContent: string, ) => void, ) => Promise<{ new_path: string } | null> } function normalizeFolderPath(params: { folderPath: string }): string { return params.folderPath.trim().replace(/^\/+|\/+$/g, '') } function folderPathForEntry(params: { entry: VaultEntry; vaultPath: string }): string { const normalizedVaultPath = params.vaultPath.replace(/\/+$/, '') const relativePath = params.entry.path.startsWith(`${normalizedVaultPath}/`) ? params.entry.path.slice(normalizedVaultPath.length + 1) : params.entry.filename const lastSlashIndex = relativePath.lastIndexOf('/') return lastSlashIndex >= 0 ? relativePath.slice(0, lastSlashIndex) : '' } function flattenFolders(nodes: FolderNode[]): RetargetFolderOption[] { return nodes.flatMap((node) => [ { path: node.path, label: node.name }, ...flattenFolders(node.children), ]) } function entryByPath(params: { entries: VaultEntry[]; notePath: string }): VaultEntry | undefined { return params.entries.find((entry) => entry.path === params.notePath) } function canRetargetEntryToType(params: { entry: VaultEntry | undefined; type: string }): boolean { return !!params.entry && params.entry.isA !== params.type } function canRetargetEntryToFolder( params: { entry: VaultEntry | undefined folderPath: string vaultPath: string }, ): boolean { if (!params.entry) return false return folderPathForEntry({ entry: params.entry, vaultPath: params.vaultPath }) !== normalizeFolderPath({ folderPath: params.folderPath }) } function updateEntitySelection( selection: SidebarSelection, setSelection: (selection: SidebarSelection) => void, notePath: string, patch: Partial & { path: string }, ) { if (selection.kind !== 'entity' || selection.entry.path !== notePath) return setSelection({ kind: 'entity', entry: { ...selection.entry, ...patch, }, }) } async function changeEntryType({ entry, notePath, nextType, selection, setSelection, setToastMessage, updateFrontmatter, }: { entry: VaultEntry | undefined notePath: string nextType: string selection: SidebarSelection setSelection: (selection: SidebarSelection) => void setToastMessage: (message: string | null) => void updateFrontmatter: ( path: string, key: string, value: string, options?: FrontmatterOpOptions, ) => Promise }): Promise { const normalizedType = nextType.trim() if (!entry || !normalizedType) return 'error' if (entry.isA === normalizedType) return 'noop' try { await updateFrontmatter(notePath, 'type', normalizedType, { silent: true }) updateEntitySelection(selection, setSelection, notePath, { path: notePath, isA: normalizedType }) setToastMessage(`Type set to "${normalizedType}"`) return 'updated' } catch (error) { console.error('Failed to change note type:', error) setToastMessage(typeof error === 'string' ? error : 'Failed to change note type') return 'error' } } async function moveEntryToFolder({ entry, notePath, folderPath, vaultPath, selection, setSelection, moveNoteToFolder, }: { entry: VaultEntry | undefined notePath: string folderPath: string vaultPath: string selection: SidebarSelection setSelection: (selection: SidebarSelection) => void moveNoteToFolder: ( path: string, folderPath: string, vaultPath: string, onEntryRenamed: ( oldPath: string, newEntry: Partial & { path: string }, newContent: string, ) => void, ) => Promise<{ new_path: string } | null> }): Promise { const normalizedFolderPath = normalizeFolderPath({ folderPath }) if (!entry || !normalizedFolderPath) return 'error' if (folderPathForEntry({ entry, vaultPath }) === normalizedFolderPath) return 'noop' const result = await moveNoteToFolder( notePath, normalizedFolderPath, vaultPath, (oldPath, newEntry) => updateEntitySelection(selection, setSelection, oldPath, newEntry), ) if (!result) return 'error' return result.new_path === notePath ? 'noop' : 'updated' } export function useNoteRetargeting({ entries, folders, selection, setSelection, setToastMessage, vaultPath, updateFrontmatter, moveNoteToFolder, }: NoteRetargetingInput) { const availableTypes = useMemo( () => extractVaultTypes(entries).map((type) => type.canonicalType).sort((left, right) => left.localeCompare(right)), [entries], ) const availableFolders = useMemo(() => flattenFolders(folders), [folders]) const canDropNoteOnType = useCallback((notePath: string, type: string) => { return canRetargetEntryToType({ entry: entryByPath({ entries, notePath }), type, }) }, [entries]) const canDropNoteOnFolder = useCallback((notePath: string, folderPath: string) => { return canRetargetEntryToFolder({ entry: entryByPath({ entries, notePath }), folderPath, vaultPath, }) }, [entries, vaultPath]) const changeNoteType = useCallback(async ( notePath: string, nextType: string, ): Promise => { return changeEntryType({ entry: entryByPath({ entries, notePath }), notePath, nextType, selection, setSelection, setToastMessage, updateFrontmatter, }) }, [entries, selection, setSelection, setToastMessage, updateFrontmatter]) const moveIntoFolder = useCallback(async ( notePath: string, folderPath: string, ): Promise => { return moveEntryToFolder({ entry: entryByPath({ entries, notePath }), notePath, folderPath, vaultPath, selection, setSelection, moveNoteToFolder, }) }, [entries, moveNoteToFolder, selection, setSelection, vaultPath]) return { availableTypes, availableFolders, canDropNoteOnType, canDropNoteOnFolder, changeNoteType, moveIntoFolder, } }