refactor: extract hooks from App.tsx to improve code health (#62)
Extract vault management (useVaultSwitcher), git history loading (useGitHistory), dialog state (useDialogs), and keyboard/command setup (useAppCommands) into dedicated hooks. App.tsx code health improves from 8.95 to 10.0 — all extracted hooks also score 10.0. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
82
src/hooks/useAppCommands.ts
Normal file
82
src/hooks/useAppCommands.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useAppKeyboard } from './useAppKeyboard'
|
||||
import { useCommandRegistry } from './useCommandRegistry'
|
||||
import type { CommandAction } from './useCommandRegistry'
|
||||
import { useKeyboardNavigation } from './useKeyboardNavigation'
|
||||
import type { SidebarSelection, VaultEntry } from '../types'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
|
||||
interface Tab { entry: VaultEntry; content: string }
|
||||
|
||||
interface AppCommandsConfig {
|
||||
activeTabPath: string | null
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
|
||||
tabs: Tab[]
|
||||
entries: VaultEntry[]
|
||||
allContent: Record<string, string>
|
||||
modifiedCount: number
|
||||
selection: SidebarSelection
|
||||
onQuickOpen: () => void
|
||||
onCommandPalette: () => void
|
||||
onCreateNote: () => void
|
||||
onSave: () => void
|
||||
onOpenSettings: () => void
|
||||
onTrashNote: (path: string) => void
|
||||
onArchiveNote: (path: string) => void
|
||||
onUnarchiveNote: (path: string) => void
|
||||
onCommitPush: () => void
|
||||
onSetViewMode: (mode: ViewMode) => void
|
||||
onToggleInspector: () => void
|
||||
onSelect: (sel: SidebarSelection) => void
|
||||
onCloseTab: (path: string) => void
|
||||
onSwitchTab: (path: string) => void
|
||||
onReplaceActiveTab: (entry: VaultEntry) => void
|
||||
onSelectNote: (entry: VaultEntry) => void
|
||||
}
|
||||
|
||||
/** Sets up keyboard shortcuts, command registry, and keyboard navigation. */
|
||||
export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
useAppKeyboard({
|
||||
onQuickOpen: config.onQuickOpen,
|
||||
onCommandPalette: config.onCommandPalette,
|
||||
onCreateNote: config.onCreateNote,
|
||||
onSave: config.onSave,
|
||||
onOpenSettings: config.onOpenSettings,
|
||||
onTrashNote: config.onTrashNote,
|
||||
onArchiveNote: config.onArchiveNote,
|
||||
onSetViewMode: config.onSetViewMode,
|
||||
activeTabPathRef: config.activeTabPathRef,
|
||||
handleCloseTabRef: config.handleCloseTabRef,
|
||||
})
|
||||
|
||||
const commands = useCommandRegistry({
|
||||
activeTabPath: config.activeTabPath,
|
||||
entries: config.entries,
|
||||
modifiedCount: config.modifiedCount,
|
||||
onQuickOpen: config.onQuickOpen,
|
||||
onCreateNote: config.onCreateNote,
|
||||
onSave: config.onSave,
|
||||
onOpenSettings: config.onOpenSettings,
|
||||
onTrashNote: config.onTrashNote,
|
||||
onArchiveNote: config.onArchiveNote,
|
||||
onUnarchiveNote: config.onUnarchiveNote,
|
||||
onCommitPush: config.onCommitPush,
|
||||
onSetViewMode: config.onSetViewMode,
|
||||
onToggleInspector: config.onToggleInspector,
|
||||
onSelect: config.onSelect,
|
||||
onCloseTab: config.onCloseTab,
|
||||
})
|
||||
|
||||
useKeyboardNavigation({
|
||||
tabs: config.tabs,
|
||||
activeTabPath: config.activeTabPath,
|
||||
entries: config.entries,
|
||||
selection: config.selection,
|
||||
allContent: config.allContent,
|
||||
onSwitchTab: config.onSwitchTab,
|
||||
onReplaceActiveTab: config.onReplaceActiveTab,
|
||||
onSelectNote: config.onSelectNote,
|
||||
})
|
||||
|
||||
return commands
|
||||
}
|
||||
@@ -3,25 +3,29 @@ import { useState, useCallback } from 'react'
|
||||
export function useDialogs() {
|
||||
const [showCreateTypeDialog, setShowCreateTypeDialog] = useState(false)
|
||||
const [showQuickOpen, setShowQuickOpen] = useState(false)
|
||||
const [showCommitDialog, setShowCommitDialog] = useState(false)
|
||||
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
||||
const [showAIChat, setShowAIChat] = useState(false)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showGitHubVault, setShowGitHubVault] = useState(false)
|
||||
|
||||
const openCreateType = useCallback(() => setShowCreateTypeDialog(true), [])
|
||||
const closeCreateType = useCallback(() => setShowCreateTypeDialog(false), [])
|
||||
const openQuickOpen = useCallback(() => setShowQuickOpen(true), [])
|
||||
const closeQuickOpen = useCallback(() => setShowQuickOpen(false), [])
|
||||
const openCommitDialog = useCallback(() => setShowCommitDialog(true), [])
|
||||
const closeCommitDialog = useCallback(() => setShowCommitDialog(false), [])
|
||||
const openCommandPalette = useCallback(() => setShowCommandPalette(true), [])
|
||||
const closeCommandPalette = useCallback(() => setShowCommandPalette(false), [])
|
||||
const openSettings = useCallback(() => setShowSettings(true), [])
|
||||
const closeSettings = useCallback(() => setShowSettings(false), [])
|
||||
const openGitHubVault = useCallback(() => setShowGitHubVault(true), [])
|
||||
const closeGitHubVault = useCallback(() => setShowGitHubVault(false), [])
|
||||
const toggleAIChat = useCallback(() => setShowAIChat((c) => !c), [])
|
||||
|
||||
return {
|
||||
showCreateTypeDialog, openCreateType, closeCreateType,
|
||||
showQuickOpen, openQuickOpen, closeQuickOpen,
|
||||
showCommitDialog, openCommitDialog, closeCommitDialog,
|
||||
showCommandPalette, openCommandPalette, closeCommandPalette,
|
||||
showAIChat, toggleAIChat,
|
||||
showSettings, openSettings, closeSettings,
|
||||
showGitHubVault, openGitHubVault, closeGitHubVault,
|
||||
}
|
||||
}
|
||||
|
||||
13
src/hooks/useGitHistory.ts
Normal file
13
src/hooks/useGitHistory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { GitCommit } from '../types'
|
||||
|
||||
export function useGitHistory(activeTabPath: string | null, loadGitHistory: (path: string) => Promise<GitCommit[]>) {
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabPath) return
|
||||
loadGitHistory(activeTabPath).then(setGitHistory)
|
||||
}, [activeTabPath, loadGitHistory])
|
||||
|
||||
return activeTabPath ? gitHistory : []
|
||||
}
|
||||
62
src/hooks/useVaultSwitcher.ts
Normal file
62
src/hooks/useVaultSwitcher.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import type { VaultOption } from '../components/StatusBar'
|
||||
|
||||
export const DEFAULT_VAULTS: VaultOption[] = isTauri()
|
||||
? [
|
||||
{ label: 'Demo v2', path: '/Users/luca/Workspace/laputa-app/demo-vault-v2' },
|
||||
{ label: 'Laputa', path: '/Users/luca/Laputa' },
|
||||
]
|
||||
: [
|
||||
{ label: 'Demo v2', path: '/Users/luca/Workspace/laputa-app/demo-vault-v2' },
|
||||
]
|
||||
|
||||
interface UseVaultSwitcherOptions {
|
||||
onSwitch: () => void
|
||||
onToast: (msg: string) => void
|
||||
}
|
||||
|
||||
/** Manages vault path, extra vaults, switching, cloning, and local folder opening. */
|
||||
export function useVaultSwitcher({ onSwitch, onToast }: UseVaultSwitcherOptions) {
|
||||
const [vaultPath, setVaultPath] = useState(DEFAULT_VAULTS[0].path)
|
||||
const [extraVaults, setExtraVaults] = useState<VaultOption[]>([])
|
||||
const allVaults = useMemo(() => [...DEFAULT_VAULTS, ...extraVaults], [extraVaults])
|
||||
|
||||
// Refs ensure stable callbacks that always invoke the latest closures,
|
||||
// breaking the circular dependency between useVaultSwitcher and downstream hooks.
|
||||
const onSwitchRef = useRef(onSwitch)
|
||||
const onToastRef = useRef(onToast)
|
||||
useEffect(() => { onSwitchRef.current = onSwitch; onToastRef.current = onToast })
|
||||
|
||||
const addVault = useCallback((path: string, label: string) => {
|
||||
setExtraVaults(prev => prev.some(v => v.path === path) ? prev : [...prev, { label, path }])
|
||||
}, [])
|
||||
|
||||
const switchVault = useCallback((path: string) => {
|
||||
setVaultPath(path)
|
||||
onSwitchRef.current()
|
||||
}, [])
|
||||
|
||||
const handleVaultCloned = useCallback((path: string, label: string) => {
|
||||
addVault(path, label)
|
||||
switchVault(path)
|
||||
onToastRef.current(`Vault "${label}" cloned and opened`)
|
||||
}, [addVault, switchVault])
|
||||
|
||||
const handleOpenLocalFolder = useCallback(async () => {
|
||||
try {
|
||||
const path = await pickFolder('Open vault folder')
|
||||
if (!path) return
|
||||
const label = path.split('/').pop() || 'Local Vault'
|
||||
addVault(path, label)
|
||||
switchVault(path)
|
||||
onToastRef.current(`Vault "${label}" opened`)
|
||||
} catch (err) {
|
||||
console.error('Failed to open local folder:', err)
|
||||
onToastRef.current(`Failed to open folder: ${err}`)
|
||||
}
|
||||
}, [addVault, switchVault])
|
||||
|
||||
return { vaultPath, allVaults, switchVault, handleVaultCloned, handleOpenLocalFolder }
|
||||
}
|
||||
Reference in New Issue
Block a user