fix(lint): resolve eslint errors in suggestion components

- Fix import order issues
- Fix TypeScript type annotations
- Use .at() for array access to satisfy lint rules
- Cast props to correct types in command handlers
- Fix template literal type issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Anton Tranelis 2026-01-14 15:38:56 +01:00
parent c644f182e4
commit 0e9548e25f
14 changed files with 58 additions and 59 deletions

View File

@ -10,8 +10,13 @@ import { Markdown } from 'tiptap-markdown'
import { useGetItemColor } from '#components/Map/hooks/useItemColor'
import { useItems } from '#components/Map/hooks/useItems'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import { Hashtag, ItemMention, VideoEmbed } from '#components/TipTap/extensions'
import { createHashtagSuggestion, createItemMentionSuggestion } from '#components/TipTap/extensions'
import {
Hashtag,
ItemMention,
VideoEmbed,
createHashtagSuggestion,
createItemMentionSuggestion,
} from '#components/TipTap/extensions'
import { preprocessMarkdown } from '#components/TipTap/utils/preprocessMarkdown'
import { InputLabel } from './InputLabel'

View File

@ -12,7 +12,7 @@ export const useGetItemColor = (): ((item: Item | undefined, fallback?: string)
const getItemTags = useGetItemTags()
return useCallback(
(item: Item | undefined, fallback: string = '#000') => {
(item: Item | undefined, fallback = '#000') => {
if (!item) return fallback
// 1. Item's own color takes highest priority
@ -36,7 +36,7 @@ export const useGetItemColor = (): ((item: Item | undefined, fallback?: string)
* Hook that returns the calculated color for a specific item.
* Priority: item.color > first tag color > layer default color > fallback
*/
export const useItemColor = (item: Item | undefined, fallback: string = '#000'): string => {
export const useItemColor = (item: Item | undefined, fallback = '#000'): string => {
const getItemColor = useGetItemColor()
return getItemColor(item, fallback)
}

View File

@ -1,16 +1,16 @@
/* eslint-disable camelcase */ // Directus database fields use snake_case
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import classNames from 'classnames'
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '#components/Auth/useAuth'
import { useGetItemColor } from '#components/Map/hooks/useItemColor'
import { useItems, useUpdateItem, useAddItem } from '#components/Map/hooks/useItems'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useGetItemColor } from '#components/Map/hooks/useItemColor'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import { MapOverlayPage } from '#components/Templates'

View File

@ -48,7 +48,9 @@ export function ActionButton({
<>
{hasUserPermission(collection, 'update', item) && (
<>
<div className={`tw:absolute tw:right-6 tw:bottom-4 tw:flex tw:flex-col ${customStyle}`}>
<div
className={`tw:absolute tw:right-6 tw:bottom-4 tw:flex tw:flex-col ${customStyle ?? ''}`}
>
{triggerItemSelected && (
<button
tabIndex={0}

View File

@ -6,8 +6,8 @@ import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { StartEndView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { TextViewStatic } from '#components/Map/Subcomponents/ItemPopupComponents/TextViewStatic'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { TextViewStatic } from '#components/Map/Subcomponents/ItemPopupComponents/TextViewStatic'
import { DateUserInfo } from './DateUserInfo'

View File

@ -8,8 +8,7 @@ import type { Tag } from '#types/Tag'
import type { NodeViewProps } from '@tiptap/react'
import type { SuggestionOptions } from '@tiptap/suggestion'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySuggestionOptions = Partial<SuggestionOptions<any>>
type AnySuggestionOptions = Partial<SuggestionOptions>
export interface HashtagOptions {
tags: Tag[]
@ -93,10 +92,7 @@ export const Hashtag = Node.create<HashtagOptions>({
addStorage() {
return {
markdown: {
serialize(
state: { write: (text: string) => void },
node: { attrs: { label: string } },
) {
serialize(state: { write: (text: string) => void }, node: { attrs: { label: string } }) {
// Write as plain hashtag
state.write(`#${node.attrs.label}`)
},

View File

@ -7,8 +7,7 @@ import type { Item } from '#types/Item'
import type { NodeViewProps } from '@tiptap/react'
import type { SuggestionOptions } from '@tiptap/suggestion'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySuggestionOptions = Partial<SuggestionOptions<any>>
type AnySuggestionOptions = Partial<SuggestionOptions>
export interface ItemMentionOptions {
HTMLAttributes: Record<string, unknown>
@ -137,7 +136,7 @@ function ItemMentionComponent({ node, editor, extension }: NodeViewProps) {
const item = options.items?.find((i) => i.id === id)
const color = options.getItemColor
? options.getItemColor(item, 'var(--color-primary)')
: item?.color ?? 'var(--color-primary)'
: (item?.color ?? 'var(--color-primary)')
const handleClick = (e: React.MouseEvent) => {
// Don't navigate when in edit mode

View File

@ -1,6 +1,6 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import type { NodeViewProps } from '@tiptap/react'
@ -13,17 +13,17 @@ const RUMBLE_REGEX = /(?:https?:\/\/)?rumble\.com\/embed\/([a-zA-Z0-9_-]+)/
* Extracts video provider and ID from a URL
*/
function parseVideoUrl(url: string): { provider: 'youtube' | 'rumble'; videoId: string } | null {
let match = url.match(YOUTUBE_REGEX)
let match = YOUTUBE_REGEX.exec(url)
if (match) {
return { provider: 'youtube', videoId: match[1] }
}
match = url.match(YOUTUBE_SHORT_REGEX)
match = YOUTUBE_SHORT_REGEX.exec(url)
if (match) {
return { provider: 'youtube', videoId: match[1] }
}
match = url.match(RUMBLE_REGEX)
match = RUMBLE_REGEX.exec(url)
if (match) {
return { provider: 'rumble', videoId: match[1] }
}
@ -57,7 +57,10 @@ export const VideoEmbed = Node.create<VideoEmbedOptions>({
addStorage() {
return {
markdown: {
serialize(state: { write: (text: string) => void }, node: { attrs: { provider: string; videoId: string } }) {
serialize(
state: { write: (text: string) => void },
node: { attrs: { provider: string; videoId: string } },
) {
const { provider, videoId } = node.attrs
const url =
provider === 'youtube'

View File

@ -2,10 +2,6 @@ export { Hashtag } from './Hashtag'
export { ItemMention } from './ItemMention'
export { VideoEmbed } from './VideoEmbed'
export {
createHashtagSuggestion,
createItemMentionSuggestion,
SuggestionList,
} from './suggestions'
export { createHashtagSuggestion, createItemMentionSuggestion, SuggestionList } from './suggestions'
export type { SuggestionListRef } from './suggestions'

View File

@ -4,10 +4,10 @@ import tippy from 'tippy.js'
import { SuggestionList } from './SuggestionList'
import type { Instance as TippyInstance } from 'tippy.js'
import type { SuggestionOptions } from '@tiptap/suggestion'
import type { Tag } from '#types/Tag'
import type { SuggestionListRef } from './SuggestionList'
import type { SuggestionOptions } from '@tiptap/suggestion'
import type { Instance as TippyInstance } from 'tippy.js'
export const HashtagSuggestionPluginKey = new PluginKey('hashtagSuggestion')
@ -103,9 +103,9 @@ export function createHashtagSuggestion(
}
// Space key triggers selection of the first item (or creates new tag)
if (props.event.key === ' ') {
const firstItem = currentItems[0]
if (firstItem && currentCommand) {
currentCommand(firstItem)
const firstItem = currentItems.at(0)
if (firstItem) {
currentCommand?.(firstItem)
return true
}
}
@ -123,10 +123,10 @@ export function createHashtagSuggestion(
let tagToInsert: Tag
// Create new tag if needed
if ('isNew' in props && props.isNew) {
if ('isNew' in props && (props as { isNew: boolean }).isNew) {
const newTag: Tag = {
id: crypto.randomUUID(),
name: props.name,
name: (props as { name: string }).name,
color: '#888888', // Default color
}
if (addTag) {

View File

@ -4,10 +4,10 @@ import tippy from 'tippy.js'
import { SuggestionList } from './SuggestionList'
import type { Instance as TippyInstance } from 'tippy.js'
import type { SuggestionOptions } from '@tiptap/suggestion'
import type { Item } from '#types/Item'
import type { SuggestionListRef } from './SuggestionList'
import type { SuggestionOptions } from '@tiptap/suggestion'
import type { Instance as TippyInstance } from 'tippy.js'
export const ItemMentionSuggestionPluginKey = new PluginKey('itemMentionSuggestion')
@ -96,6 +96,7 @@ export function createItemMentionSuggestion(
command: ({ editor, range, props }) => {
// Insert item mention and a space after it
// Using a single insertContent call with an array ensures atomic insertion
const item = props as Item
editor
.chain()
.focus()
@ -104,8 +105,8 @@ export function createItemMentionSuggestion(
{
type: 'itemMention',
attrs: {
id: props.id,
label: props.name,
id: item.id,
label: item.name,
},
},
{

View File

@ -35,7 +35,7 @@ export const SuggestionList = forwardRef<SuggestionListRef, SuggestionListProps>
return true
}
if (event.key === 'Enter') {
const item = items[selectedIndex]
const item = items.at(selectedIndex)
if (item) command(item)
return true
}
@ -67,7 +67,11 @@ export const SuggestionList = forwardRef<SuggestionListRef, SuggestionListProps>
color = getItemColor(item as Item)
}
const key = isNewTag ? `new-${item.name}` : 'id' in item ? item.id : `item-${index}`
const key = isNewTag
? `new-${item.name}`
: 'id' in item
? item.id
: `item-${String(index)}`
return (
<button
@ -76,7 +80,9 @@ export const SuggestionList = forwardRef<SuggestionListRef, SuggestionListProps>
index === selectedIndex ? 'tw:bg-base-200' : ''
}`}
style={color ? { color } : undefined}
onClick={() => command(item)}
onClick={() => {
command(item)
}}
>
{isNewTag ? (
<span className='tw:flex tw:items-center tw:gap-1'>

View File

@ -8,10 +8,7 @@ import type { JSONContent, Extensions } from '@tiptap/core'
* Converts pre-processed markdown/HTML to TipTap JSON format.
* Creates a temporary editor instance to parse the content.
*/
export function markdownToTiptapJson(
content: string,
extensions: Extensions,
): JSONContent {
export function markdownToTiptapJson(content: string, extensions: Extensions): JSONContent {
// Create a temporary editor to parse HTML/markdown
const editor = new Editor({
extensions,

View File

@ -13,10 +13,7 @@ export function simpleMarkdownToHtml(text: string, tags: Tag[]): string {
let html = text
// Escape HTML first (but preserve our preprocessed tags)
html = html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
html = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
// Restore our preprocessed tags
html = html
@ -69,18 +66,15 @@ export function simpleMarkdownToHtml(text: string, tags: Tag[]): string {
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
// Links: [text](url)
html = html.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_, linkText: string, url: string) => {
const isExternal = url.startsWith('http')
const attrs = isExternal ? 'target="_blank" rel="noopener noreferrer"' : ''
return `<a href="${url}" ${attrs}>${linkText}</a>`
},
)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText: string, url: string) => {
const isExternal = url.startsWith('http')
const attrs = isExternal ? 'target="_blank" rel="noopener noreferrer"' : ''
return `<a href="${url}" ${attrs}>${linkText}</a>`
})
// Headers: # text, ## text, etc.
html = html.replace(/^(#{1,6})\s+(.*)$/gm, (_, hashes: string, content: string) => {
const level = hashes.length
const level = String(hashes.length)
return `<h${level}>${content}</h${level}>`
})