fix: reload vault entries on tauri startup

This commit is contained in:
lucaronin
2026-04-21 15:02:42 +02:00
parent 8f624350d8
commit d3d663af0b
2 changed files with 335 additions and 167 deletions

View File

@@ -26,39 +26,113 @@ const mockGitHistory: GitCommit[] = [
{ hash: 'abc1234567', shortHash: 'abc1234', message: 'initial commit', author: 'luca', date: 1700000000 },
]
function defaultMockInvoke(cmd: string, args?: Record<string, unknown>) {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve(mockModifiedFiles)
if (cmd === 'get_file_history') return Promise.resolve(mockGitHistory)
if (cmd === 'get_file_diff') return Promise.resolve('--- a/note.md\n+++ b/note.md')
if (cmd === 'get_file_diff_at_commit') return Promise.resolve(`diff for ${(args as Record<string, string>)?.commitHash}`)
if (cmd === 'git_commit') return Promise.resolve('committed')
if (cmd === 'git_push') return Promise.resolve({ status: 'ok', message: 'Pushed to remote' })
return Promise.resolve(null)
type MockCommandHandler = (args?: Record<string, unknown>) => unknown
const defaultMockHandlers: Record<string, MockCommandHandler> = {
list_vault: () => mockEntries,
reload_vault: () => mockEntries,
get_all_content: () => mockContent,
get_modified_files: () => mockModifiedFiles,
get_file_history: () => mockGitHistory,
get_file_diff: () => '--- a/note.md\n+++ b/note.md',
get_file_diff_at_commit: (args) => `diff for ${(args as Record<string, string>)?.commitHash}`,
git_commit: () => 'committed',
git_push: () => ({ status: 'ok', message: 'Pushed to remote' }),
}
const mockInvokeFn = vi.fn(defaultMockInvoke)
function defaultMockInvoke(cmd: string, args?: Record<string, unknown>) {
const handler = defaultMockHandlers[cmd]
return Promise.resolve(handler ? handler(args) : null)
}
let mockIsTauri = false
const backendInvokeFn = vi.fn(defaultMockInvoke)
function isVaultLoadCommand(cmd: string) {
return cmd === 'list_vault' || cmd === 'reload_vault'
}
function buildVaultLoaderMock(options: {
entries?: VaultEntry[]
modifiedFiles?: ModifiedFile[]
pushResult?: { status: string; message: string }
failHistory?: boolean
} = {}) {
const {
entries = mockEntries,
modifiedFiles = mockModifiedFiles,
pushResult,
failHistory = false,
} = options
return ((cmd: string, args?: Record<string, unknown>) => {
if (isVaultLoadCommand(cmd)) return Promise.resolve(entries)
if (cmd === 'get_modified_files') return Promise.resolve(modifiedFiles)
if (cmd === 'list_vault_folders') return Promise.resolve([])
if (cmd === 'list_views') return Promise.resolve([])
if (cmd === 'get_file_history' && failHistory) return Promise.reject(new Error('fail'))
if (cmd === 'git_push' && pushResult) return Promise.resolve(pushResult)
return defaultMockInvoke(cmd, args)
}) as typeof defaultMockInvoke
}
function createDeferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
mockInvoke: (cmd: string, args?: Record<string, unknown>) => mockInvokeFn(cmd, args),
isTauri: () => mockIsTauri,
mockInvoke: (cmd: string, args?: Record<string, unknown>) => backendInvokeFn(cmd, args),
}))
async function waitForEntries(
result: ReturnType<typeof renderHook<ReturnType<typeof useVaultLoader>, undefined>>['result'],
length = 1,
) {
await waitFor(() => {
expect(result.current.entries).toHaveLength(length)
})
}
async function waitForModifiedFiles(
result: ReturnType<typeof renderHook<ReturnType<typeof useVaultLoader>, undefined>>['result'],
length = 1,
) {
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(length)
})
}
/** Render the vault loader hook and wait for initial data to load. */
async function renderVaultLoader() {
const hook = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => { expect(hook.result.current.entries).toHaveLength(1) })
await waitForEntries(hook.result)
return hook
}
async function enableTauriMode() {
mockIsTauri = true
const tauri = await import('@tauri-apps/api/core')
vi.mocked(tauri.invoke).mockImplementation((command: string, args?: Record<string, unknown>) =>
backendInvokeFn(command, args),
)
}
describe('useVaultLoader', () => {
beforeEach(() => {
mockInvokeFn.mockImplementation(defaultMockInvoke)
mockIsTauri = false
backendInvokeFn.mockReset()
backendInvokeFn.mockImplementation(defaultMockInvoke)
})
it('loads entries on mount', async () => {
@@ -70,13 +144,88 @@ describe('useVaultLoader', () => {
it('loads modified files on mount', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(1)
})
await waitForModifiedFiles(result)
expect(result.current.modifiedFiles[0].status).toBe('modified')
})
it('loads initial vault entries from a fresh reload in Tauri mode', async () => {
await enableTauriMode()
backendInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') {
return Promise.resolve([
{ ...mockEntries[0], path: '/vault/stale.md', filename: 'stale.md', title: 'Stale', isA: 'Type' },
])
}
if (cmd === 'reload_vault') {
return Promise.resolve([
{ ...mockEntries[0], path: '/vault/journal.md', filename: 'journal.md', title: 'Journal', isA: 'Type' },
{ ...mockEntries[0], path: '/vault/2026-03-11.md', filename: '2026-03-11.md', title: 'March 11', isA: 'Journal' },
])
}
if (cmd === 'get_modified_files') return Promise.resolve([])
if (cmd === 'list_vault_folders') return Promise.resolve([])
if (cmd === 'list_views') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.title)).toEqual(['Journal', 'March 11'])
})
const issuedCommands = backendInvokeFn.mock.calls.map(([command]) => command)
expect(issuedCommands).toContain('reload_vault')
expect(issuedCommands).not.toContain('list_vault')
})
it('ignores stale reload_vault results after the vault path changes', async () => {
await enableTauriMode()
const firstLoad = createDeferred<VaultEntry[]>()
const secondLoad = createDeferred<VaultEntry[]>()
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
const path = typeof args?.path === 'string' ? args.path : undefined
if (cmd === 'reload_vault') {
if (path === '/vault-a') return firstLoad.promise
if (path === '/vault-b') return secondLoad.promise
}
if (cmd === 'list_vault_folders') return Promise.resolve([])
if (cmd === 'list_views') return Promise.resolve([])
if (cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result, rerender } = renderHook(
({ path }) => useVaultLoader(path),
{ initialProps: { path: '/vault-a' } },
)
rerender({ path: '/vault-b' })
await act(async () => {
firstLoad.resolve([
{ ...mockEntries[0], path: '/vault-a/stale.md', filename: 'stale.md', title: 'Stale', isA: 'Type' },
])
await firstLoad.promise
})
expect(result.current.entries).toEqual([])
await act(async () => {
secondLoad.resolve([
{ ...mockEntries[0], path: '/vault-b/journal.md', filename: 'journal.md', title: 'Journal', isA: 'Type' },
{ ...mockEntries[0], path: '/vault-b/2026-03-11.md', filename: '2026-03-11.md', title: 'March 11', isA: 'Journal' },
])
await secondLoad.promise
})
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.title)).toEqual(['Journal', 'March 11'])
})
})
describe('addEntry', () => {
it('prepends new entry', async () => {
const { result } = await renderVaultLoader()
@@ -174,54 +323,44 @@ describe('useVaultLoader', () => {
expect(result.current.getNoteStatus('/vault/note/brand-new.md')).toBe('new')
})
it('returns new for git-untracked files (saved but not committed)', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([
{ path: '/vault/note/brand-new.md', relativePath: 'note/brand-new.md', status: 'untracked' },
])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
it.each([
{
name: 'returns new for git-untracked files (saved but not committed)',
path: '/vault/note/brand-new.md',
relativePath: 'note/brand-new.md',
status: 'untracked',
},
{
name: 'returns new for git-added files (staged but not committed)',
path: '/vault/note/staged.md',
relativePath: 'note/staged.md',
status: 'added',
},
{
name: 'treats untracked files as new (green dot, not orange)',
path: '/vault/note/hello.md',
relativePath: 'note/hello.md',
status: 'untracked',
},
])('$name', async ({ path, relativePath, status }) => {
backendInvokeFn.mockImplementation(buildVaultLoaderMock({
modifiedFiles: [{ path, relativePath, status }],
}))
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(1)
})
await waitForModifiedFiles(result)
expect(result.current.getNoteStatus('/vault/note/brand-new.md')).toBe('new')
})
it('returns new for git-added files (staged but not committed)', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([
{ path: '/vault/note/staged.md', relativePath: 'note/staged.md', status: 'added' },
])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(1)
})
expect(result.current.getNoteStatus('/vault/note/staged.md')).toBe('new')
expect(result.current.getNoteStatus(path)).toBe('new')
})
it('new status takes priority over git modified', async () => {
// If a path is both new and in modifiedFiles, it should show as new
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([
backendInvokeFn.mockImplementation(buildVaultLoaderMock({
modifiedFiles: [
{ path: '/vault/note/new.md', relativePath: 'note/new.md', status: 'modified' },
])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
],
}))
const { result } = renderHook(() => useVaultLoader('/vault'))
@@ -284,33 +423,11 @@ describe('useVaultLoader', () => {
expect(result.current.getNoteStatus('/vault/note/draft.md')).toBe('new')
})
it('treats untracked files as new (green dot, not orange)', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([
{ path: '/vault/note/hello.md', relativePath: 'note/hello.md', status: 'untracked' },
])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(1)
})
expect(result.current.getNoteStatus('/vault/note/hello.md')).toBe('new')
})
})
describe('loadGitHistory', () => {
it('returns git commits for a file', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.entries).toHaveLength(1)
})
const { result } = await renderVaultLoader()
let history: GitCommit[] = []
await act(async () => {
@@ -322,13 +439,11 @@ describe('useVaultLoader', () => {
})
it('returns empty array on error', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'get_file_history') return Promise.reject(new Error('fail'))
if (cmd === 'list_vault') return Promise.resolve([])
if (cmd === 'get_all_content') return Promise.resolve({})
if (cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
backendInvokeFn.mockImplementation(buildVaultLoaderMock({
entries: [],
modifiedFiles: [],
failHistory: true,
}))
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const { result } = renderHook(() => useVaultLoader('/vault'))
@@ -345,11 +460,7 @@ describe('useVaultLoader', () => {
describe('loadDiff', () => {
it('returns diff string for a file', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.entries).toHaveLength(1)
})
const { result } = await renderVaultLoader()
let diff = ''
await act(async () => {
@@ -362,11 +473,7 @@ describe('useVaultLoader', () => {
describe('loadDiffAtCommit', () => {
it('returns diff for a specific commit', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.entries).toHaveLength(1)
})
const { result } = await renderVaultLoader()
let diff = ''
await act(async () => {
@@ -379,11 +486,7 @@ describe('useVaultLoader', () => {
describe('commitAndPush', () => {
it('commits and pushes in mock mode', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.entries).toHaveLength(1)
})
const { result } = await renderVaultLoader()
let response: { status: string; message: string } = { status: '', message: '' }
await act(async () => {
@@ -393,56 +496,42 @@ describe('useVaultLoader', () => {
expect(response.status).toBe('ok')
})
it('returns rejected status when push is rejected', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([])
if (cmd === 'git_commit') return Promise.resolve('committed')
if (cmd === 'git_push') return Promise.resolve({ status: 'rejected', message: 'Push rejected: remote has new commits. Pull first, then push.' })
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
it.each([
{
name: 'returns rejected status when push is rejected',
pushResult: { status: 'rejected', message: 'Push rejected: remote has new commits. Pull first, then push.' },
expectedStatus: 'rejected',
expectedMessage: 'Pull first',
},
{
name: 'returns network error status on network failure',
pushResult: { status: 'network_error', message: 'Push failed: network error. Check your connection and try again.' },
expectedStatus: 'network_error',
expectedMessage: 'network error',
},
])('$name', async ({ pushResult, expectedStatus, expectedMessage }) => {
backendInvokeFn.mockImplementation(buildVaultLoaderMock({
modifiedFiles: [],
pushResult,
}))
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => { expect(result.current.entries).toHaveLength(1) })
const { result } = await renderVaultLoader()
let response: { status: string; message: string } = { status: '', message: '' }
await act(async () => {
response = await result.current.commitAndPush('test commit')
})
expect(response.status).toBe('rejected')
expect(response.message).toContain('Pull first')
})
it('returns network error status on network failure', async () => {
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
if (cmd === 'get_all_content') return Promise.resolve(mockContent)
if (cmd === 'get_modified_files') return Promise.resolve([])
if (cmd === 'git_commit') return Promise.resolve('committed')
if (cmd === 'git_push') return Promise.resolve({ status: 'network_error', message: 'Push failed: network error. Check your connection and try again.' })
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => { expect(result.current.entries).toHaveLength(1) })
let response: { status: string; message: string } = { status: '', message: '' }
await act(async () => {
response = await result.current.commitAndPush('test commit')
})
expect(response.status).toBe('network_error')
expect(response.message).toContain('network error')
expect(response.status).toBe(expectedStatus)
expect(response.message).toContain(expectedMessage)
})
})
describe('reloadFolders', () => {
it('refreshes folder tree from backend', async () => {
const folders = [{ name: 'projects', path: 'projects', children: [] }]
mockInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault') return Promise.resolve(mockEntries)
backendInvokeFn.mockImplementation(((cmd: string) => {
if (isVaultLoadCommand(cmd)) return Promise.resolve(mockEntries)
if (cmd === 'get_modified_files') return Promise.resolve([])
if (cmd === 'list_vault_folders') return Promise.resolve(folders)
return Promise.resolve(null)
@@ -453,7 +542,7 @@ describe('useVaultLoader', () => {
expect(result.current.folders).toEqual(folders)
const updatedFolders = [...folders, { name: 'journal', path: 'journal', children: [] }]
mockInvokeFn.mockImplementation(((cmd: string) => {
backendInvokeFn.mockImplementation(((cmd: string) => {
if (cmd === 'list_vault_folders') return Promise.resolve(updatedFolders)
return defaultMockInvoke(cmd)
}) as typeof defaultMockInvoke)
@@ -466,11 +555,7 @@ describe('useVaultLoader', () => {
describe('loadModifiedFiles', () => {
it('refreshes modified files list', async () => {
const { result } = renderHook(() => useVaultLoader('/vault'))
await waitFor(() => {
expect(result.current.modifiedFiles).toHaveLength(1)
})
const { result } = await renderVaultLoader()
await act(async () => {
await result.current.loadModifiedFiles()

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { VaultEntry, FolderNode, GitCommit, ModifiedFile, NoteStatus, GitPushResult, ViewFile } from '../types'
@@ -8,13 +8,54 @@ function tauriCall<T>(command: string, tauriArgs: Record<string, unknown>, mockA
return isTauri() ? invoke<T>(command, tauriArgs) : mockInvoke<T>(command, mockArgs ?? tauriArgs)
}
function loadVaultEntries(vaultPath: string): Promise<VaultEntry[]> {
const command = isTauri() ? 'reload_vault' : 'list_vault'
return tauriCall<VaultEntry[]>(command, { path: vaultPath })
}
async function loadVaultData(vaultPath: string) {
if (!isTauri()) console.info('[mock] Using mock Tauri data for browser testing')
const entries = await tauriCall<VaultEntry[]>('list_vault', { path: vaultPath })
const entries = await loadVaultEntries(vaultPath)
console.log(`Vault scan complete: ${entries.length} entries found`)
return { entries }
}
function loadVaultFolders(vaultPath: string): Promise<FolderNode[]> {
return tauriCall<FolderNode[]>('list_vault_folders', { path: vaultPath })
}
function loadVaultViews(vaultPath: string): Promise<ViewFile[]> {
return tauriCall<ViewFile[]>('list_views', { vaultPath })
}
function resetVaultState(options: {
clearNewPaths: () => void
clearUnsaved: () => void
setEntries: (entries: VaultEntry[]) => void
setFolders: (folders: FolderNode[]) => void
setModifiedFiles: (files: ModifiedFile[]) => void
setModifiedFilesError: (message: string | null) => void
setViews: (views: ViewFile[]) => void
}) {
options.setEntries([])
options.setFolders([])
options.setViews([])
options.setModifiedFiles([])
options.setModifiedFilesError(null)
options.clearNewPaths()
options.clearUnsaved()
}
function useCurrentVaultPathGuard(vaultPath: string) {
const currentPathRef = useRef(vaultPath)
useEffect(() => {
currentPathRef.current = vaultPath
}, [vaultPath])
return useCallback((path: string) => currentPathRef.current === path, [])
}
async function commitWithPush(vaultPath: string, message: string): Promise<GitPushResult> {
if (!isTauri()) {
await mockInvoke<string>('git_commit', { message })
@@ -96,32 +137,54 @@ export function useVaultLoader(vaultPath: string) {
const tracker = useNewNoteTracker()
const pendingSave = usePendingSaveTracker()
const unsaved = useUnsavedTracker()
const isCurrentVaultPath = useCurrentVaultPathGuard(vaultPath)
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear stale data then load new vault
setEntries([]); setFolders([]); setViews([]); setModifiedFiles([]); setModifiedFilesError(null); tracker.clear(); unsaved.clearAll()
loadVaultData(vaultPath)
.then(({ entries: e }) => { setEntries(e) })
const path = vaultPath
resetVaultState({
clearNewPaths: tracker.clear,
clearUnsaved: unsaved.clearAll,
setEntries,
setFolders,
setModifiedFiles,
setModifiedFilesError,
setViews,
})
loadVaultData(path)
.then(({ entries: e }) => {
if (!isCurrentVaultPath(path)) return
setEntries(e)
})
.catch((err) => console.warn('Vault scan failed:', err))
tauriCall<FolderNode[]>('list_vault_folders', { path: vaultPath })
.then((f) => { setFolders(f ?? []) })
loadVaultFolders(path)
.then((f) => {
if (!isCurrentVaultPath(path)) return
setFolders(f ?? [])
})
.catch(() => { /* folders are optional — ignore errors */ })
tauriCall<ViewFile[]>('list_views', { vaultPath })
.then((v) => { setViews(v ?? []) })
loadVaultViews(path)
.then((v) => {
if (!isCurrentVaultPath(path)) return
setViews(v ?? [])
})
.catch(() => { /* views are optional — ignore errors */ })
}, [vaultPath]) // eslint-disable-line react-hooks/exhaustive-deps -- tracker.clear is stable
}, [vaultPath, tracker.clear, unsaved.clearAll, isCurrentVaultPath])
const loadModifiedFiles = useCallback(async () => {
const path = vaultPath
try {
setModifiedFilesError(null)
setModifiedFiles(await tauriCall<ModifiedFile[]>('get_modified_files', { vaultPath }, {}))
const files = await tauriCall<ModifiedFile[]>('get_modified_files', { vaultPath: path }, {})
if (!isCurrentVaultPath(path)) return
setModifiedFiles(files)
} catch (err) {
if (!isCurrentVaultPath(path)) return
const message = typeof err === 'string' ? err : 'Failed to load changes'
console.warn('Failed to load modified files:', err)
setModifiedFilesError(message)
setModifiedFiles([])
}
}, [vaultPath])
}, [vaultPath, isCurrentVaultPath])
useEffect(() => { loadModifiedFiles() }, [loadModifiedFiles]) // eslint-disable-line react-hooks/set-state-in-effect -- trigger initial load
@@ -176,27 +239,47 @@ export function useVaultLoader(vaultPath: string) {
commitWithPush(vaultPath, message), [vaultPath])
const reloadFolders = useCallback(
() => tauriCall<FolderNode[]>('list_vault_folders', { path: vaultPath })
.then((f) => { setFolders(f ?? []) })
.catch(() => { /* folders are optional — ignore errors */ }),
[vaultPath],
() => {
const path = vaultPath
return loadVaultFolders(path)
.then((f) => {
if (!isCurrentVaultPath(path)) return [] as FolderNode[]
const nextFolders = f ?? []
setFolders(nextFolders)
return nextFolders
})
.catch(() => [] as FolderNode[])
},
[vaultPath, isCurrentVaultPath],
)
const reloadVault = useCallback(
() => {
const path = vaultPath
clearPrefetchCache()
return tauriCall<VaultEntry[]>('reload_vault', { path: vaultPath })
.then((entries) => { setEntries(entries); loadModifiedFiles(); return entries })
return tauriCall<VaultEntry[]>('reload_vault', { path })
.then((entries) => {
if (!isCurrentVaultPath(path)) return [] as VaultEntry[]
setEntries(entries)
void loadModifiedFiles()
return entries
})
.catch((err) => { console.warn('Vault reload failed:', err); return [] as VaultEntry[] })
},
[vaultPath, loadModifiedFiles],
[vaultPath, loadModifiedFiles, isCurrentVaultPath],
)
const reloadViews = useCallback(async () => {
const path = vaultPath
try {
setViews(await tauriCall<ViewFile[]>('list_views', { vaultPath }) ?? [])
const nextViews = await loadVaultViews(path)
if (!isCurrentVaultPath(path)) return []
const resolvedViews = nextViews ?? []
setViews(resolvedViews)
return resolvedViews
} catch { /* views are optional */ }
}, [vaultPath])
return []
}, [vaultPath, isCurrentVaultPath])
return {
entries, folders, views, modifiedFiles, modifiedFilesError,