fix: deduplicate autocomplete suggestions and disambiguate same-title notes (#54)
BlockNote's SuggestionMenu uses item.title as React key, causing duplicate rendering and broken arrow-key navigation when multiple notes share the same title. Fix by: - Adding path-based deduplication to filter out duplicate entries - Disambiguating same-title notes with parent folder name (e.g. "Standup (work)") - Deduplicating aliases within each item (filename vs entry.aliases overlap) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -360,4 +360,50 @@ describe('wikilink autocomplete', () => {
|
||||
' ',
|
||||
])
|
||||
})
|
||||
|
||||
it('deduplicates entries with the same path', async () => {
|
||||
const dupEntries: VaultEntry[] = [
|
||||
{ ...mockEntry, title: 'Dup Note', filename: 'dup.md', path: '/vault/dup.md', aliases: [] },
|
||||
{ ...mockEntry, title: 'Dup Note Copy', filename: 'dup.md', path: '/vault/dup.md', aliases: [] },
|
||||
{ ...mockEntry, title: 'Other Note', filename: 'other.md', path: '/vault/other.md', aliases: [] },
|
||||
]
|
||||
capturedGetItems = null
|
||||
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
|
||||
render(
|
||||
<Editor
|
||||
{...defaultProps}
|
||||
tabs={[mockTab]}
|
||||
activeTabPath={mockEntry.path}
|
||||
entries={dupEntries}
|
||||
/>
|
||||
)
|
||||
const items = await capturedGetItems!('Note')
|
||||
const paths = items.map((i: { path: string }) => i.path)
|
||||
expect(new Set(paths).size).toBe(paths.length)
|
||||
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
|
||||
})
|
||||
|
||||
it('disambiguates entries with the same title by appending folder name', async () => {
|
||||
const sameTitle: VaultEntry[] = [
|
||||
{ ...mockEntry, title: 'Standup', filename: 'standup.md', path: '/vault/work/standup.md', aliases: [] },
|
||||
{ ...mockEntry, title: 'Standup', filename: 'standup.md', path: '/vault/personal/standup.md', aliases: [] },
|
||||
]
|
||||
capturedGetItems = null
|
||||
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
|
||||
render(
|
||||
<Editor
|
||||
{...defaultProps}
|
||||
tabs={[mockTab]}
|
||||
activeTabPath={mockEntry.path}
|
||||
entries={sameTitle}
|
||||
/>
|
||||
)
|
||||
const items = await capturedGetItems!('Standup')
|
||||
expect(items).toHaveLength(2)
|
||||
const titles = items.map((i: { title: string }) => i.title)
|
||||
expect(new Set(titles).size).toBe(2)
|
||||
expect(titles).toContain('Standup (work)')
|
||||
expect(titles).toContain('Standup (personal)')
|
||||
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ import { TabBar } from './TabBar'
|
||||
import { BreadcrumbBar } from './BreadcrumbBar'
|
||||
import { useEditorTheme } from '../hooks/useTheme'
|
||||
import { splitFrontmatter, preProcessWikilinks, injectWikilinks, restoreWikilinksInBlocks, countWords } from '../utils/wikilinks'
|
||||
import { preFilterWikilinks, MAX_RESULTS, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions'
|
||||
import { preFilterWikilinks, deduplicateByPath, disambiguateTitles, MAX_RESULTS, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions'
|
||||
import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors'
|
||||
import './Editor.css'
|
||||
import './EditorTheme.css'
|
||||
@@ -132,12 +132,13 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e
|
||||
}, [editor])
|
||||
|
||||
const baseItems = useMemo(
|
||||
() => entries.map(entry => ({
|
||||
() => deduplicateByPath(entries.map(entry => ({
|
||||
title: entry.title,
|
||||
aliases: [entry.filename.replace(/\.md$/, ''), ...entry.aliases],
|
||||
aliases: [...new Set([entry.filename.replace(/\.md$/, ''), ...entry.aliases])],
|
||||
group: entry.isA || 'Note',
|
||||
entryTitle: entry.title,
|
||||
})),
|
||||
path: entry.path,
|
||||
}))),
|
||||
[entries]
|
||||
)
|
||||
|
||||
@@ -157,7 +158,8 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e
|
||||
])
|
||||
},
|
||||
}))
|
||||
return filterSuggestionItems(items, query).slice(0, MAX_RESULTS)
|
||||
const filtered = filterSuggestionItems(items, query).slice(0, MAX_RESULTS)
|
||||
return disambiguateTitles(deduplicateByPath(filtered))
|
||||
}, [baseItems, editor])
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user