fix: clear codacy high severity batches

This commit is contained in:
lucaronin
2026-05-07 19:52:14 +02:00
parent f5866a2376
commit 8d35fbcba3
37 changed files with 800 additions and 350 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useCallback, useMemo, useEffect, type ComponentType, type SVGAttributes } from 'react'
import type { VaultEntry } from '../types'
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
import { scrollSelectedHTMLChildIntoView } from '../utils/domScroll'
import { getTypeIcon } from './NoteItem'
import { NoteTitleIcon } from './NoteTitleIcon'
import './WikilinkSuggestionMenu.css'
@@ -8,6 +9,8 @@ import './WikilinkSuggestionMenu.css'
const MIN_QUERY_LENGTH = 2
const MAX_RESULTS = 10
type AutocompleteKeyAction = 'next' | 'previous' | 'select' | 'close'
interface NoteAutocompleteProps {
entries: VaultEntry[]
typeEntryMap: Record<string, VaultEntry>
@@ -29,26 +32,106 @@ interface MatchedEntry {
TypeIcon?: ComponentType<SVGAttributes<SVGSVGElement>>
}
function entryMatchesQuery(entry: VaultEntry, lowerQuery: string): boolean {
return entry.title.toLowerCase().includes(lowerQuery)
|| entry.aliases.some(alias => alias.toLowerCase().includes(lowerQuery))
}
function buildMatchedEntry(entry: VaultEntry, typeEntryMap: Record<string, VaultEntry>): MatchedEntry {
const isA = entry.isA
const typeEntry = typeEntryMap[isA ?? '']
const noteType = isA || undefined
return {
title: entry.title,
noteIcon: entry.icon,
noteType,
typeColor: noteType ? getTypeColor(isA, typeEntry?.color) : undefined,
typeLightColor: noteType ? getTypeLightColor(isA, typeEntry?.color) : undefined,
TypeIcon: noteType ? getTypeIcon(isA, typeEntry?.icon) : undefined,
}
}
function matchEntries(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>, query: string): MatchedEntry[] {
if (query.length < MIN_QUERY_LENGTH) return []
const lowerQuery = query.toLowerCase()
const matches = entries.filter(e =>
e.title.toLowerCase().includes(lowerQuery) ||
e.aliases.some(a => a.toLowerCase().includes(lowerQuery)),
return entries
.filter(entry => entryMatchesQuery(entry, lowerQuery))
.slice(0, MAX_RESULTS)
.map(entry => buildMatchedEntry(entry, typeEntryMap))
}
function resolveOpenAutocompleteKeyAction(key: string): AutocompleteKeyAction | null {
switch (key) {
case 'ArrowDown':
return 'next'
case 'ArrowUp':
return 'previous'
case 'Enter':
return 'select'
case 'Escape':
return 'close'
default:
return null
}
}
function nextAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
return (currentIndex + 1) % matchCount
}
function previousAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
return currentIndex <= 0 ? matchCount - 1 : currentIndex - 1
}
function handleClosedAutocompleteKey(
key: string,
value: string,
onSelect: (noteTitle: string) => void,
onEscape: (() => void) | undefined,
): void {
if (key === 'Enter') {
onSelect(value)
return
}
if (key === 'Escape') onEscape?.()
}
function preventAutocompleteMouseDown(event: React.MouseEvent): void {
event.preventDefault()
}
interface NoteAutocompleteMenuItemProps {
item: MatchedEntry
selected: boolean
onSelect: (title: string) => void
onHover: () => void
}
function NoteAutocompleteMenuItem({
item,
selected,
onSelect,
onHover,
}: NoteAutocompleteMenuItemProps) {
return (
<div
className={`wikilink-menu__item${selected ? ' wikilink-menu__item--selected' : ''}`}
onMouseDown={preventAutocompleteMouseDown}
onClick={() => onSelect(item.title)}
onMouseEnter={onHover}
>
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
<NoteTitleIcon icon={item.noteIcon} size={14} />
{item.title}
</span>
{item.noteType && (
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
{item.noteType}
</span>
)}
</div>
)
return matches.slice(0, MAX_RESULTS).map(e => {
const isA = e.isA
const te = typeEntryMap[isA ?? '']
const noteType = isA || undefined
return {
title: e.title,
noteIcon: e.icon,
noteType,
typeColor: noteType ? getTypeColor(isA, te?.color) : undefined,
typeLightColor: noteType ? getTypeLightColor(isA, te?.color) : undefined,
TypeIcon: noteType ? getTypeIcon(isA, te?.icon) : undefined,
}
})
}
export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSelect, onEscape, placeholder, autoFocus, testId }: NoteAutocompleteProps) {
@@ -65,8 +148,7 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
// Scroll selected item into view
useEffect(() => {
if (selectedIndex < 0 || !menuRef.current) return
const el = menuRef.current.children[selectedIndex] as HTMLElement | undefined
el?.scrollIntoView?.({ block: 'nearest' })
scrollSelectedHTMLChildIntoView(menuRef.current, selectedIndex)
}, [selectedIndex])
// Close on outside click
@@ -95,28 +177,31 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (!open || matches.length === 0) {
if (e.key === 'Enter') { onSelect(value); return }
if (e.key === 'Escape') { onEscape?.(); return }
handleClosedAutocompleteKey(e.key, value, onSelect, onEscape)
return
}
if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedIndex(i => (i + 1) % matches.length)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedIndex(i => (i <= 0 ? matches.length - 1 : i - 1))
} else if (e.key === 'Enter') {
e.preventDefault()
if (selectedIndex >= 0 && selectedIndex < matches.length) {
handleSelect(matches[selectedIndex].title)
} else {
onSelect(value)
}
} else if (e.key === 'Escape') {
e.preventDefault()
const action = resolveOpenAutocompleteKeyAction(e.key)
if (!action) return
e.preventDefault()
if (action === 'next') {
setSelectedIndex(i => nextAutocompleteSelectionIndex(i, matches.length))
return
}
if (action === 'previous') {
setSelectedIndex(i => previousAutocompleteSelectionIndex(i, matches.length))
return
}
if (action === 'close') {
setOpen(false)
onEscape?.()
return
}
const match = matches[selectedIndex]
if (match) handleSelect(match.title)
else onSelect(value)
}, [open, matches, selectedIndex, value, handleSelect, onSelect, onEscape])
return (
@@ -136,24 +221,13 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
{open && matches.length > 0 && (
<div className="wikilink-menu" ref={menuRef} style={{ position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 2, minWidth: 'auto' }}>
{matches.map((item, index) => (
<div
<NoteAutocompleteMenuItem
key={item.title}
className={`wikilink-menu__item${index === selectedIndex ? ' wikilink-menu__item--selected' : ''}`}
onMouseDown={e => e.preventDefault()}
onClick={() => handleSelect(item.title)}
onMouseEnter={() => setSelectedIndex(index)}
>
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
<NoteTitleIcon icon={item.noteIcon} size={14} />
{item.title}
</span>
{item.noteType && (
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
{item.noteType}
</span>
)}
</div>
item={item}
selected={index === selectedIndex}
onSelect={handleSelect}
onHover={() => setSelectedIndex(index)}
/>
))}
</div>
)}