Files
tolaria/src/hooks/useVaultSwitcher.ts
lucaronin 164b76d35c fix: resolve demo vault path relative to project root — fix smoke test failures
DEFAULT_VAULTS was hard-coded to the main repo path, causing the Vault API
to read stale/duplicate files when running from worktrees. Now uses Vite's
define to inject the correct path at build time. Also fix theme heading
titles (remove redundant "Theme" suffix) and make smoke tests resilient
to Theme-type notes appearing first in the note list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:14:56 +01:00

164 lines
5.7 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import { pickFolder } from '../utils/vault-dialog'
import { loadVaultList, saveVaultList } from '../utils/vaultListStore'
import type { VaultOption } from '../components/StatusBar'
export type { PersistedVaultList } from '../utils/vaultListStore'
export const GETTING_STARTED_LABEL = 'Getting Started'
declare const __DEMO_VAULT_PATH__: string
export const DEFAULT_VAULTS: VaultOption[] = [
{ label: GETTING_STARTED_LABEL, path: typeof __DEMO_VAULT_PATH__ !== 'undefined' ? __DEMO_VAULT_PATH__ : '/Users/luca/Workspace/laputa-app/demo-vault-v2' },
]
interface UseVaultSwitcherOptions {
onSwitch: () => void
onToast: (msg: string) => void
}
function labelFromPath(path: string): string {
return path.split('/').pop() || 'Local Vault'
}
function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> {
return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args)
}
/** Manages vault path, extra vaults, switching, cloning, and local folder opening.
* Vault list and active vault are persisted via Tauri backend to survive app updates. */
export function useVaultSwitcher({ onSwitch, onToast }: UseVaultSwitcherOptions) {
const [vaultPath, setVaultPath] = useState(DEFAULT_VAULTS[0].path)
const [extraVaults, setExtraVaults] = useState<VaultOption[]>([])
const [hiddenDefaults, setHiddenDefaults] = useState<string[]>([])
const [loaded, setLoaded] = useState(false)
const visibleDefaults = useMemo(
() => DEFAULT_VAULTS.filter(v => !hiddenDefaults.includes(v.path)),
[hiddenDefaults],
)
const allVaults = useMemo(
() => [...visibleDefaults, ...extraVaults],
[visibleDefaults, extraVaults],
)
const isGettingStartedHidden = useMemo(
() => hiddenDefaults.includes(DEFAULT_VAULTS[0].path),
[hiddenDefaults],
)
const onSwitchRef = useRef(onSwitch)
const onToastRef = useRef(onToast)
useEffect(() => { onSwitchRef.current = onSwitch; onToastRef.current = onToast })
const hasLoadedRef = useRef(false)
useEffect(() => {
let cancelled = false
loadVaultList()
.then(({ vaults, activeVault, hiddenDefaults: hidden }) => {
if (cancelled) return
setExtraVaults(vaults)
setHiddenDefaults(hidden)
if (activeVault) {
setVaultPath(activeVault)
onSwitchRef.current()
}
})
.catch(err => console.warn('Failed to load vault list:', err))
.finally(() => {
hasLoadedRef.current = true
setLoaded(true)
})
return () => { cancelled = true }
}, [])
useEffect(() => {
if (!hasLoadedRef.current) return
saveVaultList(extraVaults, vaultPath, hiddenDefaults).catch(err =>
console.warn('Failed to persist vault list:', err),
)
}, [extraVaults, vaultPath, hiddenDefaults])
const addVault = useCallback((path: string, label: string) => {
setExtraVaults(prev => {
const exists = prev.some(v => v.path === path)
return exists ? prev : [...prev, { label, path, available: true }]
})
}, [])
const switchVault = useCallback((path: string) => {
setVaultPath(path)
onSwitchRef.current()
}, [])
const addAndSwitch = useCallback((path: string, label: string) => {
addVault(path, label)
switchVault(path)
}, [addVault, switchVault])
const handleVaultCloned = useCallback((path: string, label: string) => {
addAndSwitch(path, label)
onToastRef.current(`Vault "${label}" cloned and opened`)
}, [addAndSwitch])
const handleOpenLocalFolder = useCallback(async () => {
const path = await pickFolder('Open vault folder')
if (!path) return
const label = labelFromPath(path)
addAndSwitch(path, label)
onToastRef.current(`Vault "${label}" opened`)
}, [addAndSwitch])
const removeVault = useCallback((path: string) => {
const isDefault = DEFAULT_VAULTS.some(v => v.path === path)
if (isDefault) {
setHiddenDefaults(prev => prev.includes(path) ? prev : [...prev, path])
} else {
setExtraVaults(prev => prev.filter(v => v.path !== path))
}
// If removing the active vault, switch to the first remaining vault
setVaultPath(currentPath => {
if (currentPath !== path) return currentPath
const remaining = [
...DEFAULT_VAULTS.filter(v => v.path !== path && !(isDefault ? [] : hiddenDefaults).includes(v.path)),
...extraVaults.filter(v => v.path !== path),
]
if (remaining.length > 0) {
onSwitchRef.current()
return remaining[0].path
}
return currentPath
})
const vault = [...DEFAULT_VAULTS, ...extraVaults].find(v => v.path === path)
onToastRef.current(`Vault "${vault?.label ?? labelFromPath(path)}" removed from list`)
}, [extraVaults, hiddenDefaults])
const restoreGettingStarted = useCallback(async () => {
const defaultPath = DEFAULT_VAULTS[0].path
// Un-hide the Getting Started vault
setHiddenDefaults(prev => prev.filter(p => p !== defaultPath))
// Try to create the vault if it doesn't exist on disk
try {
const exists = await tauriCall<boolean>('check_vault_exists', { path: defaultPath })
if (!exists) {
await tauriCall<string>('create_getting_started_vault', { targetPath: defaultPath })
}
} catch {
// In mock/test mode, creation may fail — that's fine
}
switchVault(defaultPath)
onToastRef.current('Getting Started vault restored')
}, [switchVault])
return {
vaultPath, allVaults, switchVault, handleVaultCloned, handleOpenLocalFolder, loaded,
removeVault, restoreGettingStarted, isGettingStartedHidden,
}
}