- Wrap ref assignments in useEffect to fix refs-during-render in useTabManagement, useKeyboardNavigation, and Editor - Rewrite useAppKeyboard to define keyMap inside useEffect, eliminating ref access during render - Add eslint-disable for react-hooks/set-state-in-effect on legitimate dialog reset patterns (CommitDialog, CreateNoteDialog, CreateTypeDialog, QuickOpenPalette, AIChatPanel, useVaultLoader) - Add eslint-disable for react-hooks/static-components on icon lookups (NoteItem, NoteList PinnedCard) — stateless icon components from a static map - Add eslint-disable for react-hooks/purity on Date.now() in NoteItem TrashDateLine — intentionally memoized on trashedAt - Remove unused `model` param from buildSystemPrompt (ai-chat.ts) and update callers - Fix exhaustive-deps: suppress vault object dep in App.tsx git history effect (vault object is unstable, loadGitHistory is the real dep) - Remove unused `modifiedFiles` from useNoteListData params and caller - Add missing `ref` to Sidebar useOutsideClick deps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { isTauri, mockInvoke } from '../mock-tauri'
|
|
import type { VaultEntry, GitCommit, ModifiedFile } from '../types'
|
|
|
|
function tauriCall<T>(command: string, tauriArgs: Record<string, unknown>, mockArgs?: Record<string, unknown>): Promise<T> {
|
|
return isTauri() ? invoke<T>(command, tauriArgs) : mockInvoke<T>(command, mockArgs ?? tauriArgs)
|
|
}
|
|
|
|
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 })
|
|
console.log(`Vault scan complete: ${entries.length} entries found`)
|
|
const allContent = isTauri() ? {} : await mockInvoke<Record<string, string>>('get_all_content', { path: vaultPath })
|
|
return { entries, allContent }
|
|
}
|
|
|
|
async function commitWithPush(vaultPath: string, message: string): Promise<string> {
|
|
if (!isTauri()) {
|
|
await mockInvoke<string>('git_commit', { message })
|
|
await mockInvoke<string>('git_push', {})
|
|
return 'Committed and pushed'
|
|
}
|
|
await invoke<string>('git_commit', { vaultPath, message })
|
|
try {
|
|
await invoke<string>('git_push', { vaultPath })
|
|
return 'Committed and pushed'
|
|
} catch {
|
|
return 'Committed (push failed)'
|
|
}
|
|
}
|
|
|
|
export function useVaultLoader(vaultPath: string) {
|
|
const [entries, setEntries] = useState<VaultEntry[]>([])
|
|
const [allContent, setAllContent] = useState<Record<string, string>>({})
|
|
const [modifiedFiles, setModifiedFiles] = useState<ModifiedFile[]>([])
|
|
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear stale data then load new vault
|
|
setEntries([]); setAllContent({}); setModifiedFiles([])
|
|
loadVaultData(vaultPath)
|
|
.then(({ entries: e, allContent: c }) => { setEntries(e); setAllContent(c) })
|
|
.catch((err) => console.warn('Vault scan failed:', err))
|
|
}, [vaultPath])
|
|
|
|
const loadModifiedFiles = useCallback(async () => {
|
|
try {
|
|
setModifiedFiles(await tauriCall<ModifiedFile[]>('get_modified_files', { vaultPath }, {}))
|
|
} catch (err) {
|
|
console.warn('Failed to load modified files:', err)
|
|
setModifiedFiles([])
|
|
}
|
|
}, [vaultPath])
|
|
|
|
useEffect(() => { loadModifiedFiles() }, [loadModifiedFiles]) // eslint-disable-line react-hooks/set-state-in-effect -- trigger initial load
|
|
|
|
const addEntry = useCallback((entry: VaultEntry, content: string) => {
|
|
setEntries((prev) => [entry, ...prev])
|
|
setAllContent((prev) => ({ ...prev, [entry.path]: content }))
|
|
}, [])
|
|
|
|
const updateContent = useCallback((path: string, content: string) => {
|
|
setAllContent((prev) => ({ ...prev, [path]: content }))
|
|
}, [])
|
|
|
|
const updateEntry = useCallback((path: string, patch: Partial<VaultEntry>) => {
|
|
setEntries((prev) => prev.map((e) => e.path === path ? { ...e, ...patch } : e))
|
|
}, [])
|
|
|
|
const replaceEntry = useCallback((oldPath: string, patch: Partial<VaultEntry> & { path: string }, newContent: string) => {
|
|
setEntries((prev) => prev.map((e) => e.path === oldPath ? { ...e, ...patch } : e))
|
|
setAllContent((prev) => {
|
|
const next = { ...prev }
|
|
delete next[oldPath]
|
|
next[patch.path] = newContent
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const loadGitHistory = useCallback(async (path: string): Promise<GitCommit[]> => {
|
|
try { return await tauriCall<GitCommit[]>('get_file_history', { vaultPath, path }, { path }) }
|
|
catch (err) { console.warn('Failed to load git history:', err); return [] }
|
|
}, [vaultPath])
|
|
|
|
const loadDiffAtCommit = useCallback((path: string, commitHash: string): Promise<string> =>
|
|
tauriCall<string>('get_file_diff_at_commit', { vaultPath, path, commitHash }, { path, commitHash }),
|
|
[vaultPath])
|
|
|
|
const loadDiff = useCallback((path: string): Promise<string> =>
|
|
tauriCall<string>('get_file_diff', { vaultPath, path }, { path }),
|
|
[vaultPath])
|
|
|
|
const isFileModified = useCallback((path: string): boolean =>
|
|
modifiedFiles.some((f) => f.path === path),
|
|
[modifiedFiles])
|
|
|
|
const commitAndPush = useCallback((message: string): Promise<string> =>
|
|
commitWithPush(vaultPath, message),
|
|
[vaultPath])
|
|
|
|
return {
|
|
entries,
|
|
allContent,
|
|
modifiedFiles,
|
|
addEntry,
|
|
updateEntry,
|
|
replaceEntry,
|
|
updateContent,
|
|
loadModifiedFiles,
|
|
loadGitHistory,
|
|
loadDiff,
|
|
loadDiffAtCommit,
|
|
isFileModified,
|
|
commitAndPush,
|
|
}
|
|
}
|