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:
196
src/App.tsx
196
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { NoteList } from './components/NoteList'
|
||||
import { Editor } from './components/Editor'
|
||||
@@ -16,19 +16,16 @@ import { useSettings } from './hooks/useSettings'
|
||||
import { useNoteActions } from './hooks/useNoteActions'
|
||||
import { useEditorSave } from './hooks/useEditorSave'
|
||||
import { useCommitFlow } from './hooks/useCommitFlow'
|
||||
import { useAppKeyboard } from './hooks/useAppKeyboard'
|
||||
import { useViewMode } from './hooks/useViewMode'
|
||||
import { useEntryActions } from './hooks/useEntryActions'
|
||||
import { useCommandRegistry } from './hooks/useCommandRegistry'
|
||||
import { isTauri } from './mock-tauri'
|
||||
import { pickFolder } from './utils/vault-dialog'
|
||||
|
||||
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'
|
||||
import { useAppCommands } from './hooks/useAppCommands'
|
||||
import { useDialogs } from './hooks/useDialogs'
|
||||
import { useVaultSwitcher } from './hooks/useVaultSwitcher'
|
||||
import { useGitHistory } from './hooks/useGitHistory'
|
||||
import { useUpdater } from './hooks/useUpdater'
|
||||
import { UpdateBanner } from './components/UpdateBanner'
|
||||
import { setApiKey } from './utils/ai-chat'
|
||||
import type { SidebarSelection, GitCommit } from './types'
|
||||
import type { VaultOption } from './components/StatusBar'
|
||||
import type { SidebarSelection } from './types'
|
||||
import './App.css'
|
||||
|
||||
// Type declaration for mock content storage
|
||||
@@ -40,15 +37,6 @@ declare global {
|
||||
|
||||
const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }
|
||||
|
||||
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' },
|
||||
]
|
||||
|
||||
function useLayoutPanels() {
|
||||
const [sidebarWidth, setSidebarWidth] = useState(250)
|
||||
const [noteListWidth, setNoteListWidth] = useState(300)
|
||||
@@ -63,26 +51,21 @@ function useLayoutPanels() {
|
||||
function App() {
|
||||
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
|
||||
const layout = useLayoutPanels()
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
const [showCreateTypeDialog, setShowCreateTypeDialog] = useState(false)
|
||||
const [showQuickOpen, setShowQuickOpen] = useState(false)
|
||||
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||
const [vaultPath, setVaultPath] = useState(DEFAULT_VAULTS[0].path)
|
||||
const [showAIChat, setShowAIChat] = useState(false)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showGitHubVault, setShowGitHubVault] = useState(false)
|
||||
const [extraVaults, setExtraVaults] = useState<VaultOption[]>([])
|
||||
const dialogs = useDialogs()
|
||||
|
||||
const allVaults = useMemo(() => [...DEFAULT_VAULTS, ...extraVaults], [extraVaults])
|
||||
// onSwitch closure captures `notes` declared below — safe because it's only
|
||||
// called on user interaction, never during render (refs inside the hook
|
||||
// guarantee the latest closure is always used).
|
||||
const vaultSwitcher = useVaultSwitcher({
|
||||
onSwitch: () => { setSelection(DEFAULT_SELECTION); notes.closeAllTabs() },
|
||||
onToast: (msg) => setToastMessage(msg),
|
||||
})
|
||||
|
||||
const vault = useVaultLoader(vaultPath)
|
||||
const vault = useVaultLoader(vaultSwitcher.vaultPath)
|
||||
const { settings, saveSettings } = useSettings()
|
||||
|
||||
// Sync Anthropic key from settings to localStorage for AIChatPanel
|
||||
useEffect(() => {
|
||||
setApiKey(settings.anthropic_key ?? '')
|
||||
}, [settings.anthropic_key])
|
||||
useEffect(() => { setApiKey(settings.anthropic_key ?? '') }, [settings.anthropic_key])
|
||||
|
||||
const notes = useNoteActions({ addEntry: vault.addEntry, updateContent: vault.updateContent, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry })
|
||||
|
||||
@@ -93,69 +76,15 @@ function App() {
|
||||
onAfterSave: vault.loadModifiedFiles,
|
||||
})
|
||||
|
||||
const commitFlow = useCommitFlow({
|
||||
savePending,
|
||||
loadModifiedFiles: vault.loadModifiedFiles,
|
||||
commitAndPush: vault.commitAndPush,
|
||||
setToastMessage,
|
||||
})
|
||||
const commitFlow = useCommitFlow({ savePending, loadModifiedFiles: vault.loadModifiedFiles, commitAndPush: vault.commitAndPush, setToastMessage })
|
||||
|
||||
const entryActions = useEntryActions({
|
||||
entries: vault.entries,
|
||||
updateEntry: vault.updateEntry,
|
||||
entries: vault.entries, updateEntry: vault.updateEntry,
|
||||
handleUpdateFrontmatter: notes.handleUpdateFrontmatter,
|
||||
handleDeleteProperty: notes.handleDeleteProperty,
|
||||
setToastMessage,
|
||||
handleDeleteProperty: notes.handleDeleteProperty, setToastMessage,
|
||||
})
|
||||
|
||||
const handleCreateNoteImmediate = notes.handleCreateNoteImmediate
|
||||
|
||||
const handleSwitchVault = useCallback((path: string) => {
|
||||
setVaultPath(path)
|
||||
setSelection(DEFAULT_SELECTION)
|
||||
setGitHistory([])
|
||||
notes.closeAllTabs()
|
||||
}, [notes])
|
||||
|
||||
const handleVaultCloned = useCallback((path: string, label: string) => {
|
||||
setExtraVaults(prev => {
|
||||
if (prev.some(v => v.path === path)) return prev
|
||||
return [...prev, { label, path }]
|
||||
})
|
||||
handleSwitchVault(path)
|
||||
setToastMessage(`Vault "${label}" cloned and opened`)
|
||||
}, [handleSwitchVault])
|
||||
|
||||
const addAndSwitchVault = useCallback((path: string, label: string) => {
|
||||
setExtraVaults(prev => {
|
||||
if (prev.some(v => v.path === path)) return prev
|
||||
return [...prev, { label, path }]
|
||||
})
|
||||
handleSwitchVault(path)
|
||||
}, [handleSwitchVault])
|
||||
|
||||
const handleOpenLocalFolder = useCallback(async () => {
|
||||
try {
|
||||
const path = await pickFolder('Open vault folder')
|
||||
if (!path) return
|
||||
const label = path.split('/').pop() || 'Local Vault'
|
||||
addAndSwitchVault(path, label)
|
||||
setToastMessage(`Vault "${label}" opened`)
|
||||
} catch (err) {
|
||||
console.error('Failed to open local folder:', err)
|
||||
setToastMessage(`Failed to open folder: ${err}`)
|
||||
}
|
||||
}, [addAndSwitchVault])
|
||||
|
||||
useEffect(() => {
|
||||
if (!notes.activeTabPath) { setGitHistory([]); return }
|
||||
vault.loadGitHistory(notes.activeTabPath).then(setGitHistory)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- vault object is unstable; loadGitHistory is the actual dep
|
||||
}, [notes.activeTabPath, vault.loadGitHistory])
|
||||
|
||||
const openCreateTypeDialog = useCallback(() => {
|
||||
setShowCreateTypeDialog(true)
|
||||
}, [])
|
||||
const gitHistory = useGitHistory(notes.activeTabPath, vault.loadGitHistory)
|
||||
|
||||
const handleCreateType = useCallback((name: string) => {
|
||||
notes.handleCreateType(name)
|
||||
@@ -164,55 +93,30 @@ function App() {
|
||||
|
||||
const handleRenameTab = useCallback(async (path: string, newTitle: string) => {
|
||||
await savePendingForPath(path)
|
||||
await notes.handleRenameNote(path, newTitle, vaultPath, vault.replaceEntry).then(vault.loadModifiedFiles)
|
||||
}, [notes, vaultPath, vault, savePendingForPath])
|
||||
await notes.handleRenameNote(path, newTitle, vaultSwitcher.vaultPath, vault.replaceEntry).then(vault.loadModifiedFiles)
|
||||
}, [notes, vaultSwitcher.vaultPath, vault, savePendingForPath])
|
||||
|
||||
const { setViewMode } = useViewMode()
|
||||
|
||||
useAppKeyboard({
|
||||
onQuickOpen: () => setShowQuickOpen(true),
|
||||
onCommandPalette: () => setShowCommandPalette(true),
|
||||
onCreateNote: handleCreateNoteImmediate,
|
||||
onSave: handleSave,
|
||||
onOpenSettings: () => setShowSettings(true),
|
||||
onTrashNote: entryActions.handleTrashNote,
|
||||
onArchiveNote: entryActions.handleArchiveNote,
|
||||
onSetViewMode: setViewMode,
|
||||
activeTabPathRef: notes.activeTabPathRef,
|
||||
handleCloseTabRef: notes.handleCloseTabRef,
|
||||
})
|
||||
|
||||
const commands = useCommandRegistry({
|
||||
activeTabPath: notes.activeTabPath,
|
||||
entries: vault.entries,
|
||||
modifiedCount: vault.modifiedFiles.length,
|
||||
onQuickOpen: () => setShowQuickOpen(true),
|
||||
onCreateNote: handleCreateNoteImmediate,
|
||||
onSave: handleSave,
|
||||
onOpenSettings: () => setShowSettings(true),
|
||||
onTrashNote: entryActions.handleTrashNote,
|
||||
onArchiveNote: entryActions.handleArchiveNote,
|
||||
const commands = useAppCommands({
|
||||
activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef,
|
||||
handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs,
|
||||
entries: vault.entries, allContent: vault.allContent,
|
||||
modifiedCount: vault.modifiedFiles.length, selection,
|
||||
onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette,
|
||||
onCreateNote: notes.handleCreateNoteImmediate, onSave: handleSave,
|
||||
onOpenSettings: dialogs.openSettings,
|
||||
onTrashNote: entryActions.handleTrashNote, onArchiveNote: entryActions.handleArchiveNote,
|
||||
onUnarchiveNote: entryActions.handleUnarchiveNote,
|
||||
onCommitPush: commitFlow.openCommitDialog,
|
||||
onSetViewMode: setViewMode,
|
||||
onCommitPush: commitFlow.openCommitDialog, onSetViewMode: setViewMode,
|
||||
onToggleInspector: () => layout.setInspectorCollapsed(c => !c),
|
||||
onSelect: setSelection,
|
||||
onCloseTab: notes.handleCloseTab,
|
||||
onSelect: setSelection, onCloseTab: notes.handleCloseTab,
|
||||
onSwitchTab: notes.handleSwitchTab, onReplaceActiveTab: notes.handleReplaceActiveTab,
|
||||
onSelectNote: notes.handleSelectNote,
|
||||
})
|
||||
|
||||
const { status: updateStatus, actions: updateActions } = useUpdater()
|
||||
|
||||
useKeyboardNavigation({
|
||||
tabs: notes.tabs,
|
||||
activeTabPath: notes.activeTabPath,
|
||||
entries: vault.entries,
|
||||
selection,
|
||||
allContent: vault.allContent,
|
||||
onSwitchTab: notes.handleSwitchTab,
|
||||
onReplaceActiveTab: notes.handleReplaceActiveTab,
|
||||
onSelectNote: notes.handleSelectNote,
|
||||
})
|
||||
|
||||
const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null
|
||||
|
||||
return (
|
||||
@@ -220,11 +124,11 @@ function App() {
|
||||
<UpdateBanner status={updateStatus} actions={updateActions} />
|
||||
<div className="app">
|
||||
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={handleCreateNoteImmediate} onCreateNewType={openCreateTypeDialog} onCustomizeType={entryActions.handleCustomizeType} onReorderSections={entryActions.handleReorderSections} modifiedCount={vault.modifiedFiles.length} onCommitPush={commitFlow.openCommitDialog} />
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onReorderSections={entryActions.handleReorderSections} modifiedCount={vault.modifiedFiles.length} onCommitPush={commitFlow.openCommitDialog} />
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleSidebarResize} />
|
||||
<div className="app__note-list" style={{ width: layout.noteListWidth }}>
|
||||
<NoteList entries={vault.entries} selection={selection} selectedNote={activeTab?.entry ?? null} allContent={vault.allContent} modifiedFiles={vault.modifiedFiles} getNoteStatus={vault.getNoteStatus} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={handleCreateNoteImmediate} />
|
||||
<NoteList entries={vault.entries} selection={selection} selectedNote={activeTab?.entry ?? null} allContent={vault.allContent} modifiedFiles={vault.modifiedFiles} getNoteStatus={vault.getNoteStatus} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={notes.handleCreateNoteImmediate} />
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleNoteListResize} />
|
||||
<div className="app__editor">
|
||||
@@ -239,7 +143,7 @@ function App() {
|
||||
onLoadDiff={vault.loadDiff}
|
||||
onLoadDiffAtCommit={vault.loadDiffAtCommit}
|
||||
getNoteStatus={vault.getNoteStatus}
|
||||
onCreateNote={handleCreateNoteImmediate}
|
||||
onCreateNote={notes.handleCreateNoteImmediate}
|
||||
inspectorCollapsed={layout.inspectorCollapsed}
|
||||
onToggleInspector={() => layout.setInspectorCollapsed((c) => !c)}
|
||||
inspectorWidth={layout.inspectorWidth}
|
||||
@@ -251,9 +155,9 @@ function App() {
|
||||
onUpdateFrontmatter={notes.handleUpdateFrontmatter}
|
||||
onDeleteProperty={notes.handleDeleteProperty}
|
||||
onAddProperty={notes.handleAddProperty}
|
||||
showAIChat={showAIChat}
|
||||
onToggleAIChat={() => setShowAIChat(c => !c)}
|
||||
vaultPath={vaultPath}
|
||||
showAIChat={dialogs.showAIChat}
|
||||
onToggleAIChat={dialogs.toggleAIChat}
|
||||
vaultPath={vaultSwitcher.vaultPath}
|
||||
onTrashNote={entryActions.handleTrashNote}
|
||||
onRestoreNote={entryActions.handleRestoreNote}
|
||||
onArchiveNote={entryActions.handleArchiveNote}
|
||||
@@ -263,19 +167,19 @@ function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultPath} vaults={allVaults} onSwitchVault={handleSwitchVault} onOpenSettings={() => setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onConnectGitHub={() => setShowGitHubVault(true)} onClickPending={() => setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} />
|
||||
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultSwitcher.vaultPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onConnectGitHub={dialogs.openGitHubVault} onClickPending={() => setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} />
|
||||
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
||||
<QuickOpenPalette open={showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={() => setShowQuickOpen(false)} />
|
||||
<CommandPalette open={showCommandPalette} commands={commands} onClose={() => setShowCommandPalette(false)} />
|
||||
<CreateTypeDialog open={showCreateTypeDialog} onClose={() => setShowCreateTypeDialog(false)} onCreate={handleCreateType} />
|
||||
<QuickOpenPalette open={dialogs.showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={dialogs.closeQuickOpen} />
|
||||
<CommandPalette open={dialogs.showCommandPalette} commands={commands} onClose={dialogs.closeCommandPalette} />
|
||||
<CreateTypeDialog open={dialogs.showCreateTypeDialog} onClose={dialogs.closeCreateType} onCreate={handleCreateType} />
|
||||
<CommitDialog open={commitFlow.showCommitDialog} modifiedCount={vault.modifiedFiles.length} onCommit={commitFlow.handleCommitPush} onClose={commitFlow.closeCommitDialog} />
|
||||
<SettingsPanel open={showSettings} settings={settings} onSave={saveSettings} onClose={() => setShowSettings(false)} />
|
||||
<SettingsPanel open={dialogs.showSettings} settings={settings} onSave={saveSettings} onClose={dialogs.closeSettings} />
|
||||
<GitHubVaultModal
|
||||
open={showGitHubVault}
|
||||
open={dialogs.showGitHubVault}
|
||||
githubToken={settings.github_token}
|
||||
onClose={() => setShowGitHubVault(false)}
|
||||
onVaultCloned={handleVaultCloned}
|
||||
onOpenSettings={() => { setShowGitHubVault(false); setShowSettings(true) }}
|
||||
onClose={dialogs.closeGitHubVault}
|
||||
onVaultCloned={vaultSwitcher.handleVaultCloned}
|
||||
onOpenSettings={() => { dialogs.closeGitHubVault(); dialogs.openSettings() }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
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