From 71ccae1c70ff7db19b5f390f6aea7cd5bd32089f Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 16 Mar 2026 23:34:45 +0100 Subject: [PATCH] refactor: extract frontmatterOps, update imports for useNoteCreation/useNoteRename split useNoteActions.ts reduced from 213 to 125 lines by extracting frontmatter helpers into frontmatterOps.ts and removing re-exports. Consumers now import directly from the extracted modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.tsx | 3 +- src/components/TitleField.tsx | 2 +- src/hooks/frontmatterOps.ts | 77 ++++++++++++++++++++++++++ src/hooks/useNoteActions.test.ts | 8 +-- src/hooks/useNoteActions.ts | 92 +------------------------------- 5 files changed, 86 insertions(+), 96 deletions(-) create mode 100644 src/hooks/frontmatterOps.ts diff --git a/src/App.tsx b/src/App.tsx index 5259db79..61da33f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,8 @@ import { WelcomeScreen } from './components/WelcomeScreen' import { useMcpStatus } from './hooks/useMcpStatus' import { useVaultLoader } from './hooks/useVaultLoader' import { useSettings } from './hooks/useSettings' -import { useNoteActions, needsRenameOnSave } from './hooks/useNoteActions' +import { useNoteActions } from './hooks/useNoteActions' +import { needsRenameOnSave } from './hooks/useNoteRename' import { useCommitFlow } from './hooks/useCommitFlow' import { useViewMode } from './hooks/useViewMode' import { useEntryActions } from './hooks/useEntryActions' diff --git a/src/components/TitleField.tsx b/src/components/TitleField.tsx index 336d55cc..3692639a 100644 --- a/src/components/TitleField.tsx +++ b/src/components/TitleField.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, useEffect } from 'react' -import { slugify } from '../hooks/useNoteActions' +import { slugify } from '../hooks/useNoteCreation' interface TitleFieldProps { title: string diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts new file mode 100644 index 00000000..2c79e242 --- /dev/null +++ b/src/hooks/frontmatterOps.ts @@ -0,0 +1,77 @@ +import { invoke } from '@tauri-apps/api/core' +import { isTauri } from '../mock-tauri' +import type { VaultEntry } from '../types' +import type { FrontmatterValue } from '../components/Inspector' +import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers' +import { updateMockContent, trackMockChange } from '../mock-tauri' + +const ENTRY_DELETE_MAP: Record> = { + type: { isA: null }, is_a: { isA: null }, status: { status: null }, color: { color: null }, + 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 }, + template: { template: null }, sort: { sort: null }, visible: { visible: null }, +} + +/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */ +export function frontmatterToEntryPatch( + op: 'update' | 'delete', key: string, value?: FrontmatterValue, +): Partial { + 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> = { + type: { isA: str }, is_a: { isA: str }, status: { status: str }, color: { color: str }, + 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 }, + template: { template: str }, + sort: { sort: str }, + view: { view: str }, + visible: { visible: value === false ? false : null }, + } + return updates[k] ?? {} +} + +async function invokeFrontmatter(command: string, args: Record): Promise { + return invoke(command, args) +} + +function applyMockFrontmatterUpdate(path: string, key: string, value: FrontmatterValue): string { + const content = updateMockFrontmatter(path, key, value) + updateMockContent(path, content) + trackMockChange(path) + return content +} + +function applyMockFrontmatterDelete(path: string, key: string): string { + const content = deleteMockFrontmatterProperty(path, key) + updateMockContent(path, content) + trackMockChange(path) + return content +} + +async function executeFrontmatterOp(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue): Promise { + 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) +} + +/** Run a frontmatter update/delete and apply the result to state. */ +export 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) => void; toast: (m: string | null) => void }, +): Promise { + 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`) + } +} diff --git a/src/hooks/useNoteActions.test.ts b/src/hooks/useNoteActions.test.ts index 6ca87038..a70dda12 100644 --- a/src/hooks/useNoteActions.test.ts +++ b/src/hooks/useNoteActions.test.ts @@ -5,22 +5,22 @@ import { isTauri, mockInvoke } from '../mock-tauri' import type { VaultEntry } from '../types' import { slugify, - needsRenameOnSave, buildNewEntry, generateUntitledName, entryMatchesTarget, buildNoteContent, resolveNewNote, resolveNewType, - frontmatterToEntryPatch, todayDateString, buildDailyNoteContent, resolveDailyNote, findDailyNote, - useNoteActions, DEFAULT_TEMPLATES, resolveTemplate, -} from './useNoteActions' +} from './useNoteCreation' +import { needsRenameOnSave } from './useNoteRename' +import { frontmatterToEntryPatch } from './frontmatterOps' +import { useNoteActions } from './useNoteActions' import type { NoteActionsConfig } from './useNoteActions' // Mock dependencies diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index 81a4430c..e1791da9 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -1,24 +1,14 @@ import { useCallback } from 'react' -import { invoke } from '@tauri-apps/api/core' -import { isTauri } from '../mock-tauri' import type { VaultEntry } from '../types' import type { FrontmatterValue } from '../components/Inspector' import { useTabManagement } from './useTabManagement' -import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers' -import { updateMockContent, trackMockChange } from '../mock-tauri' import { resolveEntry } from '../utils/wikilink' 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' +import { runFrontmatterAndApply } from './frontmatterOps' export interface NoteActionsConfig { addEntry: (entry: VaultEntry) => void @@ -37,82 +27,10 @@ export interface NoteActionsConfig { replaceEntry?: (oldPath: string, patch: Partial & { path: string }) => void } -/** Check if a frontmatter key represents the note title. */ function isTitleKey(key: string): boolean { return key.toLowerCase().replace(/\s+/g, '_') === 'title' } -const ENTRY_DELETE_MAP: Record> = { - type: { isA: null }, is_a: { isA: null }, status: { status: null }, color: { color: null }, - 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 }, - template: { template: null }, sort: { sort: null }, visible: { visible: null }, -} - -/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */ -export function frontmatterToEntryPatch( - op: 'update' | 'delete', key: string, value?: FrontmatterValue, -): Partial { - 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> = { - type: { isA: str }, is_a: { isA: str }, status: { status: str }, color: { color: str }, - 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 }, - template: { template: str }, - sort: { sort: str }, - view: { view: str }, - visible: { visible: value === false ? false : null }, - } - return updates[k] ?? {} -} - -async function invokeFrontmatter(command: string, args: Record): Promise { - return invoke(command, args) -} - -function applyMockFrontmatterUpdate(path: string, key: string, value: FrontmatterValue): string { - const content = updateMockFrontmatter(path, key, value) - updateMockContent(path, content) - trackMockChange(path) - return content -} - -function applyMockFrontmatterDelete(path: string, key: string): string { - const content = deleteMockFrontmatterProperty(path, key) - updateMockContent(path, content) - trackMockChange(path) - return content -} - -async function executeFrontmatterOp(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue): Promise { - 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) -} - -/** 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) => void; toast: (m: string | null) => void }, -): Promise { - 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`) - } -} - interface TitleRenameDeps { vaultPath: string tabsRef: React.MutableRefObject<{ entry: VaultEntry; content: string }[]> @@ -124,7 +42,6 @@ interface TitleRenameDeps { 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 { const oldTitle = deps.tabsRef.current.find(t => t.entry.path === path)?.entry.title const result = await performRename(path, newTitle, deps.vaultPath, oldTitle) @@ -146,13 +63,8 @@ function shouldRenameOnTitleUpdate(key: string, value: FrontmatterValue): value 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) + const found = resolveEntry(entries, target) if (found) selectNote(found) else console.warn(`Navigation target not found: ${target}`) }