fix: show all relationships for topics in note list

Topics used a separate 'topic' selection kind that only showed reverse
relatedTo matches. Now topics use 'entity' kind like all other types,
going through buildRelationshipGroups to show all frontmatter
relationships, children, events, referenced-by, and backlinks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-18 23:29:39 +01:00
parent abe465cc74
commit 7db3c2f00a
6 changed files with 17 additions and 30 deletions

View File

@@ -505,7 +505,6 @@ function App() {
const aiNoteListFilter = useMemo(() => {
if (selection.kind === 'sectionGroup') return { type: selection.type, query: '' }
if (selection.kind === 'topic') return { type: null, query: selection.entry.title }
if (selection.kind === 'entity') return { type: null, query: selection.entry.title }
return { type: null, query: '' }
}, [selection])

View File

@@ -200,13 +200,14 @@ describe('NoteList', () => {
expect(screen.getByText('Related to')).toBeInTheDocument()
})
it('filters by topic (relatedTo references)', () => {
it('shows entity view with relationship groups for topics', () => {
render(
<NoteList {...defaultFilterProps} entries={mockEntries} selection={{ kind: 'topic', entry: mockEntries[4] }} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
<NoteList {...defaultFilterProps} entries={mockEntries} selection={{ kind: 'entity', entry: mockEntries[4] }} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
)
// Build Laputa App has relatedTo: [[topic/software-development]]
// Build Laputa App references this topic via relatedTo — should appear in Referenced By
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
expect(screen.queryByText('Facebook Ads Strategy')).not.toBeInTheDocument()
// Entity view shows group headers
expect(screen.getByText('Referenced By')).toBeInTheDocument()
})
it('shows search input when search icon is clicked', () => {
@@ -806,17 +807,9 @@ describe('filterEntries — trash', () => {
expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined()
})
it('topic filter excludes trashed entries', () => {
const topicEntry: VaultEntry = { ...mockEntries[4] } // Software Development topic
const trashedWithTopic: VaultEntry = {
...trashedEntry,
relatedTo: ['[[topic/software-development]]'],
}
const all = [...mockEntries, trashedWithTopic]
const result = filterEntries(all, { kind: 'topic', entry: topicEntry })
expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined()
// Normal entry with that topic should still appear
expect(result.find((e) => e.title === 'Build Laputa App')).toBeDefined()
it('entity filter returns empty (entity view uses relationship groups instead)', () => {
const result = filterEntries(mockEntries, { kind: 'entity', entry: mockEntries[4] })
expect(result).toHaveLength(0)
})
})

View File

@@ -359,13 +359,13 @@ describe('Sidebar', () => {
expect(screen.getByText('Trading')).toBeInTheDocument()
})
it('calls onSelect with topic kind when clicking a topic', () => {
it('calls onSelect with entity kind when clicking a topic', () => {
const onSelect = vi.fn()
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={onSelect} />)
fireEvent.click(screen.getByLabelText('Expand Topics'))
fireEvent.click(screen.getByText('Software Development'))
expect(onSelect).toHaveBeenCalledWith({
kind: 'topic',
kind: 'entity',
entry: mockEntries[4],
})
})

View File

@@ -18,8 +18,7 @@ export function isSelectionActive(current: SidebarSelection, check: SidebarSelec
switch (check.kind) {
case 'filter': return (current as typeof check).filter === check.filter
case 'sectionGroup': return (current as typeof check).type === check.type
case 'entity':
case 'topic': return (current as typeof check).entry.path === check.entry.path
case 'entity': return (current as typeof check).entry.path === check.entry.path
default: return false
}
}
@@ -83,8 +82,8 @@ export interface SectionContentProps {
onRenameCancel?: () => void
}
function childSelection(type: string, entry: VaultEntry): SidebarSelection {
return type === 'Topic' ? { kind: 'topic', entry } : { kind: 'entity', entry }
function childSelection(entry: VaultEntry): SidebarSelection {
return { kind: 'entity', entry }
}
function resolveCreateHandler(type: string, onCreateType?: (type: string) => void, onCreateNewType?: () => void): (() => void) | undefined {
@@ -123,7 +122,7 @@ export function SectionContent({
/>
{!isCollapsed && items.length > 0 && (
<SectionChildList
items={items} type={type} selection={selection}
items={items} selection={selection}
sectionColor={sectionColor} sectionLightColor={sectionLightColor}
onSelect={onSelect} onSelectNote={onSelectNote}
/>
@@ -132,15 +131,15 @@ export function SectionContent({
)
}
function SectionChildList({ items, type, selection, sectionColor, sectionLightColor, onSelect, onSelectNote }: {
items: VaultEntry[]; type: string; selection: SidebarSelection
function SectionChildList({ items, selection, sectionColor, sectionLightColor, onSelect, onSelectNote }: {
items: VaultEntry[]; selection: SidebarSelection
sectionColor: string; sectionLightColor: string
onSelect: (sel: SidebarSelection) => void; onSelectNote?: (entry: VaultEntry) => void
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{items.map((entry) => {
const sel = childSelection(type, entry)
const sel = childSelection(entry)
const active = isSelectionActive(selection, sel)
return (
<SectionChildItem

View File

@@ -182,4 +182,3 @@ export type SidebarSelection =
| { kind: 'filter'; filter: SidebarFilter }
| { kind: 'sectionGroup'; type: string }
| { kind: 'entity'; entry: VaultEntry }
| { kind: 'topic'; entry: VaultEntry }

View File

@@ -327,9 +327,6 @@ function filterByKind(entries: VaultEntry[], selection: SidebarSelection, subFil
const typeEntries = entries.filter((e) => e.isA === selection.type)
return subFilter ? applySubFilter(typeEntries, subFilter) : typeEntries.filter(isActive)
}
if (selection.kind === 'topic') {
return entries.filter((e) => refsMatch(e.relatedTo, selection.entry) && isActive(e))
}
return filterByFilterType(entries, selection.filter)
}