fix: resolve all ESLint errors (lint now exits 0)
- useAppKeyboard: move all logic inside useEffect, fixes refs-in-render and no-unused-expressions for view mode shortcut handler - SettingsPanel: refactor to inner component to fix setState-in-effect; remove unused maskKey function - NoteList: remove re-exports (consumers import from noteListHelpers directly); fix no-unused-expressions in toggleGroup; eslint-disable for Icon-in-render - useNoteActions: eslint-disable tabsRef.current assignment (valid pattern) - Test files: fix no-explicit-any in useKeyboardNavigation, useSettings, useVaultLoader, wikilinks tests; update NoteList.test import path
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { NoteList, getSortComparator, filterEntries } from './NoteList'
|
||||
import { NoteList } from './NoteList'
|
||||
import { getSortComparator, filterEntries } from '../utils/noteListHelpers'
|
||||
import type { VaultEntry, SidebarSelection } from '../types'
|
||||
|
||||
const allSelection: SidebarSelection = { kind: 'filter', filter: 'all' }
|
||||
|
||||
@@ -11,14 +11,10 @@ import {
|
||||
type SortOption, type SortDirection, type SortConfig, type RelationshipGroup,
|
||||
getSortComparator,
|
||||
buildRelationshipGroups, filterEntries,
|
||||
sortByModified, relativeDate, getDisplayDate,
|
||||
relativeDate, getDisplayDate,
|
||||
loadSortPreferences, saveSortPreferences,
|
||||
} from '../utils/noteListHelpers'
|
||||
|
||||
// Re-export for consumers
|
||||
export { sortByModified, filterEntries, buildRelationshipGroups, getSortComparator }
|
||||
export type { SortOption }
|
||||
|
||||
interface NoteListProps {
|
||||
entries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
@@ -41,6 +37,7 @@ function PinnedCard({ entry, typeEntryMap, onSelectNote, showDate }: {
|
||||
const Icon = getTypeIcon(entry.isA, te?.icon)
|
||||
return (
|
||||
<div className="relative cursor-pointer border-b border-[var(--border)]" style={{ backgroundColor: bgColor, padding: '14px 16px' }} onClick={() => onSelectNote(entry)}>
|
||||
{/* eslint-disable-next-line react-hooks/static-components */}
|
||||
<Icon width={16} height={16} className="absolute right-3 top-3.5" style={{ color }} data-testid="type-icon" />
|
||||
<div className="pr-6 text-[14px] font-bold" style={{ color }}>{entry.title}</div>
|
||||
<div className="mt-1 text-[12px] leading-[1.5] opacity-80" style={{ color, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{entry.snippet}</div>
|
||||
@@ -219,7 +216,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
|
||||
}, [])
|
||||
|
||||
const toggleGroup = useCallback((label: string) => {
|
||||
setCollapsedGroups((prev) => { const next = new Set(prev); next.has(label) ? next.delete(label) : next.add(label); return next })
|
||||
setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(label)) { next.delete(label) } else { next.add(label) }; return next })
|
||||
}, [])
|
||||
|
||||
const typeEntryMap = useTypeEntryMap(entries)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { X, Eye, EyeSlash } from '@phosphor-icons/react'
|
||||
import type { Settings } from '../types'
|
||||
|
||||
@@ -9,10 +9,6 @@ interface SettingsPanelProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function maskKey(key: string): string {
|
||||
if (key.length <= 8) return '•'.repeat(key.length)
|
||||
return key.slice(0, 7) + '•'.repeat(Math.min(key.length - 11, 12)) + key.slice(-4)
|
||||
}
|
||||
|
||||
interface KeyFieldProps {
|
||||
label: string
|
||||
@@ -70,21 +66,15 @@ function KeyField({ label, placeholder, value, onChange, onClear }: KeyFieldProp
|
||||
}
|
||||
|
||||
export function SettingsPanel({ open, settings, onSave, onClose }: SettingsPanelProps) {
|
||||
const [anthropicKey, setAnthropicKey] = useState('')
|
||||
const [openaiKey, setOpenaiKey] = useState('')
|
||||
const [googleKey, setGoogleKey] = useState('')
|
||||
const [githubToken, setGithubToken] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setAnthropicKey(settings.anthropic_key ?? '')
|
||||
setOpenaiKey(settings.openai_key ?? '')
|
||||
setGoogleKey(settings.google_key ?? '')
|
||||
setGithubToken(settings.github_token ?? '')
|
||||
}
|
||||
}, [open, settings])
|
||||
|
||||
if (!open) return null
|
||||
return <SettingsPanelInner settings={settings} onSave={onSave} onClose={onClose} />
|
||||
}
|
||||
|
||||
function SettingsPanelInner({ settings, onSave, onClose }: Omit<SettingsPanelProps, 'open'>) {
|
||||
const [anthropicKey, setAnthropicKey] = useState(settings.anthropic_key ?? '')
|
||||
const [openaiKey, setOpenaiKey] = useState(settings.openai_key ?? '')
|
||||
const [googleKey, setGoogleKey] = useState(settings.google_key ?? '')
|
||||
const [githubToken, setGithubToken] = useState(settings.github_token ?? '')
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed: Settings = {
|
||||
@@ -218,6 +208,3 @@ export function SettingsPanel({ open, settings, onSave, onClose }: SettingsPanel
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Export maskKey for use in other components
|
||||
export { maskKey }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
|
||||
interface KeyboardActions {
|
||||
@@ -48,27 +48,29 @@ export function useAppKeyboard({
|
||||
onQuickOpen, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote,
|
||||
onSetViewMode, activeTabPathRef, handleCloseTabRef,
|
||||
}: KeyboardActions) {
|
||||
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
|
||||
const path = activeTabPathRef.current
|
||||
if (path) fn(path)
|
||||
}
|
||||
|
||||
const cmdKeyMap = useMemo((): Record<string, ShortcutHandler> => ({
|
||||
p: onQuickOpen,
|
||||
n: onCreateNote,
|
||||
s: onSave,
|
||||
',': onOpenSettings,
|
||||
e: withActiveTab(onArchiveNote),
|
||||
w: withActiveTab((path) => handleCloseTabRef.current(path)),
|
||||
Backspace: withActiveTab(onTrashNote),
|
||||
Delete: withActiveTab(onTrashNote),
|
||||
}), [onQuickOpen, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef])
|
||||
|
||||
useEffect(() => {
|
||||
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
|
||||
const path = activeTabPathRef.current
|
||||
if (path) fn(path)
|
||||
}
|
||||
|
||||
const cmdKeyMap: Record<string, ShortcutHandler> = {
|
||||
p: onQuickOpen,
|
||||
n: onCreateNote,
|
||||
s: onSave,
|
||||
',': onOpenSettings,
|
||||
e: withActiveTab(onArchiveNote),
|
||||
w: withActiveTab((path) => handleCloseTabRef.current(path)),
|
||||
Backspace: withActiveTab(onTrashNote),
|
||||
Delete: withActiveTab(onTrashNote),
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
handleViewModeKey(e, onSetViewMode) || handleCmdKey(e, cmdKeyMap)
|
||||
if (!handleViewModeKey(e, onSetViewMode)) {
|
||||
handleCmdKey(e, cmdKeyMap)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [cmdKeyMap, onSetViewMode])
|
||||
}, [onQuickOpen, onCreateNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode])
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ describe('useKeyboardNavigation', () => {
|
||||
// Track added listeners for cleanup verification
|
||||
const origAdd = window.addEventListener
|
||||
const origRemove = window.removeEventListener
|
||||
vi.spyOn(window, 'addEventListener').mockImplementation((type: string, handler: EventListenerOrEventListenerObject, opts?: any) => {
|
||||
vi.spyOn(window, 'addEventListener').mockImplementation((type: string, handler: EventListenerOrEventListenerObject, opts?: boolean | AddEventListenerOptions) => {
|
||||
addedListeners.push({ type, handler })
|
||||
origAdd.call(window, type, handler, opts)
|
||||
})
|
||||
vi.spyOn(window, 'removeEventListener').mockImplementation((type: string, handler: EventListenerOrEventListenerObject, opts?: any) => {
|
||||
vi.spyOn(window, 'removeEventListener').mockImplementation((type: string, handler: EventListenerOrEventListenerObject, opts?: boolean | EventListenerOptions) => {
|
||||
origRemove.call(window, type, handler, opts)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -197,6 +197,7 @@ export function useNoteActions(config: NoteActionsConfig) {
|
||||
const tabMgmt = useTabManagement()
|
||||
const { setTabs, handleSelectNote, activeTabPathRef, handleSwitchTab } = tabMgmt
|
||||
const tabsRef = useRef(tabMgmt.tabs)
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
tabsRef.current = tabMgmt.tabs
|
||||
|
||||
const updateTabContent = useCallback((path: string, newContent: string) => {
|
||||
|
||||
@@ -19,10 +19,10 @@ const savedSettings: Settings = {
|
||||
|
||||
let mockSettingsStore: Settings = { ...defaultSettings }
|
||||
|
||||
const mockInvokeFn = vi.fn((cmd: string, args?: any): Promise<any> => {
|
||||
const mockInvokeFn = vi.fn((cmd: string, args?: Record<string, unknown>): Promise<unknown> => {
|
||||
if (cmd === 'get_settings') return Promise.resolve({ ...mockSettingsStore })
|
||||
if (cmd === 'save_settings') {
|
||||
mockSettingsStore = { ...args.settings }
|
||||
mockSettingsStore = { ...(args as { settings: Settings }).settings }
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
return Promise.resolve(null)
|
||||
@@ -34,7 +34,7 @@ vi.mock('@tauri-apps/api/core', () => ({
|
||||
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: () => false,
|
||||
mockInvoke: (cmd: string, args?: any) => mockInvokeFn(cmd, args),
|
||||
mockInvoke: (cmd: string, args?: Record<string, unknown>) => mockInvokeFn(cmd, args),
|
||||
}))
|
||||
|
||||
describe('useSettings', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ function defaultMockInvoke(cmd: string, args?: Record<string, unknown>) {
|
||||
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?.commitHash}`)
|
||||
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('pushed')
|
||||
return Promise.resolve(null)
|
||||
@@ -46,7 +46,7 @@ vi.mock('@tauri-apps/api/core', () => ({
|
||||
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: () => false,
|
||||
mockInvoke: (cmd: string, args?: any) => mockInvokeFn(cmd, args),
|
||||
mockInvoke: (cmd: string, args?: Record<string, unknown>) => mockInvokeFn(cmd, args),
|
||||
}))
|
||||
|
||||
describe('useVaultLoader', () => {
|
||||
@@ -170,7 +170,7 @@ describe('useVaultLoader', () => {
|
||||
if (cmd === 'get_all_content') return Promise.resolve({})
|
||||
if (cmd === 'get_modified_files') return Promise.resolve([])
|
||||
return Promise.resolve(null)
|
||||
}) as any)
|
||||
}) as typeof defaultMockInvoke)
|
||||
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { result } = renderHook(() => useVaultLoader('/vault'))
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { preProcessWikilinks, injectWikilinks, splitFrontmatter, countWords } from './wikilinks'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper for asserting on opaque block structures
|
||||
type AnyBlock = any
|
||||
interface TestBlock {
|
||||
type?: string
|
||||
text?: string
|
||||
content?: TestBlock[]
|
||||
children?: TestBlock[]
|
||||
props?: Record<string, string>
|
||||
href?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
describe('preProcessWikilinks', () => {
|
||||
it('replaces [[target]] with placeholder tokens', () => {
|
||||
@@ -43,15 +50,15 @@ describe('injectWikilinks', () => {
|
||||
],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[] as AnyBlock[]
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
expect(result[0].content).toHaveLength(3)
|
||||
expect(result[0].content[0]).toEqual({ type: 'text', text: 'before ' })
|
||||
expect(result[0].content[1]).toEqual({
|
||||
expect(result[0].content![0]).toEqual({ type: 'text', text: 'before ' })
|
||||
expect(result[0].content![1]).toEqual({
|
||||
type: 'wikilink',
|
||||
props: { target: 'My Note' },
|
||||
content: undefined,
|
||||
})
|
||||
expect(result[0].content[2]).toEqual({ type: 'text', text: ' after' })
|
||||
expect(result[0].content![2]).toEqual({ type: 'text', text: ' after' })
|
||||
})
|
||||
|
||||
it('handles multiple wikilinks in one text node', () => {
|
||||
@@ -61,11 +68,11 @@ describe('injectWikilinks', () => {
|
||||
],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
const wikilinkNodes = result[0].content.filter((n: AnyBlock) => n.type === 'wikilink')
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
const wikilinkNodes = result[0].content!.filter((n: TestBlock) => n.type === 'wikilink')
|
||||
expect(wikilinkNodes).toHaveLength(2)
|
||||
expect(wikilinkNodes[0].props.target).toBe('A')
|
||||
expect(wikilinkNodes[1].props.target).toBe('B')
|
||||
expect(wikilinkNodes[0].props!.target).toBe('A')
|
||||
expect(wikilinkNodes[1].props!.target).toBe('B')
|
||||
})
|
||||
|
||||
it('passes through non-text content items unchanged', () => {
|
||||
@@ -75,8 +82,8 @@ describe('injectWikilinks', () => {
|
||||
],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
expect(result[0].content[0].type).toBe('link')
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
expect(result[0].content![0].type).toBe('link')
|
||||
})
|
||||
|
||||
it('recursively processes children blocks', () => {
|
||||
@@ -89,16 +96,16 @@ describe('injectWikilinks', () => {
|
||||
}],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
const childContent = result[0].children[0].content
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
const childContent = result[0].children![0].content!
|
||||
expect(childContent).toHaveLength(2)
|
||||
expect(childContent[1].type).toBe('wikilink')
|
||||
expect(childContent[1].props.target).toBe('Nested')
|
||||
expect(childContent[1].props!.target).toBe('Nested')
|
||||
})
|
||||
|
||||
it('handles blocks without content or children', () => {
|
||||
const blocks = [{ type: 'heading', props: { level: 1 } }]
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
const result = injectWikilinks(blocks as unknown[]) as TestBlock[]
|
||||
expect(result).toEqual(blocks)
|
||||
})
|
||||
|
||||
@@ -109,10 +116,10 @@ describe('injectWikilinks', () => {
|
||||
],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
expect(result[0].content[0].type).toBe('wikilink')
|
||||
expect(result[0].content[0].props.target).toBe('First')
|
||||
expect(result[0].content[1].text).toBe(' text')
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
expect(result[0].content![0].type).toBe('wikilink')
|
||||
expect(result[0].content![0].props!.target).toBe('First')
|
||||
expect(result[0].content![1].text).toBe(' text')
|
||||
})
|
||||
|
||||
it('handles text node that ends with wikilink', () => {
|
||||
@@ -122,9 +129,9 @@ describe('injectWikilinks', () => {
|
||||
],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as AnyBlock[]
|
||||
expect(result[0].content[0].text).toBe('text ')
|
||||
expect(result[0].content[1].type).toBe('wikilink')
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
expect(result[0].content![0].text).toBe('text ')
|
||||
expect(result[0].content![1].type).toBe('wikilink')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user