From efd79e9a3dbc7102223d21b5877ca2cfa02ab4e8 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 08:53:43 +0100 Subject: [PATCH] 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 --- src/components/NoteList.test.tsx | 3 +- src/components/NoteList.tsx | 9 ++--- src/components/SettingsPanel.tsx | 31 +++++---------- src/hooks/useAppKeyboard.ts | 40 ++++++++++--------- src/hooks/useKeyboardNavigation.test.ts | 4 +- src/hooks/useNoteActions.ts | 1 + src/hooks/useSettings.test.ts | 6 +-- src/hooks/useVaultLoader.test.ts | 6 +-- src/utils/wikilinks.test.ts | 53 ++++++++++++++----------- 9 files changed, 74 insertions(+), 79 deletions(-) diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 6e00d480..6143e21e 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -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' } diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index c5849bab..99affcc3 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -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 (
onSelectNote(entry)}> + {/* eslint-disable-next-line react-hooks/static-components */}
{entry.title}
{entry.snippet}
@@ -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) diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index e77cc1d7..7699c419 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -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 +} + +function SettingsPanelInner({ settings, onSave, onClose }: Omit) { + 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
) } - -// Export maskKey for use in other components -export { maskKey } diff --git a/src/hooks/useAppKeyboard.ts b/src/hooks/useAppKeyboard.ts index 183611e8..bbd13640 100644 --- a/src/hooks/useAppKeyboard.ts +++ b/src/hooks/useAppKeyboard.ts @@ -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 => ({ - 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 = { + 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]) } diff --git a/src/hooks/useKeyboardNavigation.test.ts b/src/hooks/useKeyboardNavigation.test.ts index 7b3e1805..a64369ee 100644 --- a/src/hooks/useKeyboardNavigation.test.ts +++ b/src/hooks/useKeyboardNavigation.test.ts @@ -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) }) }) diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index bb3be8b4..696a4a34 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -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) => { diff --git a/src/hooks/useSettings.test.ts b/src/hooks/useSettings.test.ts index 6369d4ee..e8f2ddd7 100644 --- a/src/hooks/useSettings.test.ts +++ b/src/hooks/useSettings.test.ts @@ -19,10 +19,10 @@ const savedSettings: Settings = { let mockSettingsStore: Settings = { ...defaultSettings } -const mockInvokeFn = vi.fn((cmd: string, args?: any): Promise => { +const mockInvokeFn = vi.fn((cmd: string, args?: Record): Promise => { 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) => mockInvokeFn(cmd, args), })) describe('useSettings', () => { diff --git a/src/hooks/useVaultLoader.test.ts b/src/hooks/useVaultLoader.test.ts index 7549c9ed..f17231cd 100644 --- a/src/hooks/useVaultLoader.test.ts +++ b/src/hooks/useVaultLoader.test.ts @@ -32,7 +32,7 @@ function defaultMockInvoke(cmd: string, args?: Record) { 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)?.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) => 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')) diff --git a/src/utils/wikilinks.test.ts b/src/utils/wikilinks.test.ts index d172928a..899ea953 100644 --- a/src/utils/wikilinks.test.ts +++ b/src/utils/wikilinks.test.ts @@ -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 + 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') }) })