fix: include inverse neighborhood groups

This commit is contained in:
lucaronin
2026-04-19 14:13:10 +02:00
parent 00cb938766
commit cb1cc4b394
3 changed files with 154 additions and 3 deletions

View File

@@ -379,6 +379,48 @@ describe('NoteList rendering', () => {
expect(screen.getAllByText('Shared Note')).toHaveLength(2)
})
it('shows all real inverse relationship groups for custom relationship keys', () => {
const parent = makeEntry({
path: '/vault/parent.md',
filename: 'parent.md',
title: 'Parent',
isA: 'Project',
})
const topicNote = makeEntry({
path: '/vault/topic-note.md',
filename: 'topic-note.md',
title: 'Topic Note',
isA: 'Note',
relationships: { Topics: ['[[parent]]'] },
})
const mentorNote = makeEntry({
path: '/vault/mentor-note.md',
filename: 'mentor-note.md',
title: 'Mentor Note',
isA: 'Note',
relationships: { Mentors: ['[[parent]]'] },
})
const hostEvent = makeEntry({
path: '/vault/host-event.md',
filename: 'host-event.md',
title: 'Host Event',
isA: 'Event',
relationships: { Hosts: ['[[parent]]'] },
})
renderNoteList({
entries: [parent, topicNote, mentorNote, hostEvent],
selection: { kind: 'entity', entry: parent },
})
expect(screen.getByText('← Topics')).toBeInTheDocument()
expect(screen.getByText('← Mentors')).toBeInTheDocument()
expect(screen.getByText('← Hosts')).toBeInTheDocument()
expect(screen.getByText('Topic Note')).toBeInTheDocument()
expect(screen.getByText('Mentor Note')).toBeInTheDocument()
expect(screen.getByText('Host Event')).toBeInTheDocument()
})
it('collapses and expands entity groups', () => {
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()

View File

@@ -164,6 +164,42 @@ describe('buildRelationshipGroups', () => {
expect(groups.find((group) => group.label === 'Referenced By')?.entries).toEqual([shared])
})
it('includes all inverse relationship groups for non-core relationship keys', () => {
const parent = makeEntry({
path: '/vault/parent.md',
filename: 'parent.md',
title: 'Parent',
isA: 'Project',
})
const topicNote = makeEntry({
path: '/vault/topic-note.md',
filename: 'topic-note.md',
title: 'Topic Note',
isA: 'Note',
relationships: { Topics: ['[[parent]]'] },
})
const mentorNote = makeEntry({
path: '/vault/mentor-note.md',
filename: 'mentor-note.md',
title: 'Mentor Note',
isA: 'Note',
relationships: { Mentors: ['[[parent]]'] },
})
const hostEvent = makeEntry({
path: '/vault/host-event.md',
filename: 'host-event.md',
title: 'Host Event',
isA: 'Event',
relationships: { Hosts: ['[[parent]]'] },
})
const groups = buildRelationshipGroups(parent, [parent, topicNote, mentorNote, hostEvent])
expect(groups.find((group) => group.label === '← Topics')?.entries).toEqual([topicNote])
expect(groups.find((group) => group.label === '← Mentors')?.entries).toEqual([mentorNote])
expect(groups.find((group) => group.label === '← Hosts')?.entries).toEqual([hostEvent])
})
it('does not treat path-qualified refs for a different note as reverse matches', () => {
const parent = makeEntry({
path: '/vault/projects/alpha.md',

View File

@@ -294,6 +294,81 @@ class GroupBuilder {
}
}
function appendInverseRelationshipEntries(
inverseGroups: Map<string, VaultEntry[]>,
label: string,
entry: VaultEntry,
) {
const existing = inverseGroups.get(label)
if (existing) {
existing.push(entry)
return
}
inverseGroups.set(label, [entry])
}
function resolveInverseRelationshipLabel(key: string, entry: VaultEntry): string {
if (key === 'Belongs to') return entry.isA === 'Event' ? 'Events' : 'Children'
if (key === 'Related to') return entry.isA === 'Event' ? 'Events' : 'Referenced By'
return `${key}`
}
function appendLegacyInverseRelationshipEntries(
inverseGroups: Map<string, VaultEntry[]>,
entity: VaultEntry,
entry: VaultEntry,
) {
if (refsMatch(entry.belongsTo, entity)) {
appendInverseRelationshipEntries(inverseGroups, resolveInverseRelationshipLabel('Belongs to', entry), entry)
}
if (refsMatch(entry.relatedTo, entity)) {
appendInverseRelationshipEntries(inverseGroups, resolveInverseRelationshipLabel('Related to', entry), entry)
}
}
function appendDynamicInverseRelationshipEntries(
inverseGroups: Map<string, VaultEntry[]>,
entity: VaultEntry,
entry: VaultEntry,
) {
for (const [key, refs] of Object.entries(entry.relationships ?? {})) {
if (key === 'Type' || !refsMatch(refs, entity)) continue
appendInverseRelationshipEntries(inverseGroups, resolveInverseRelationshipLabel(key, entry), entry)
}
}
function orderInverseRelationshipLabels(inverseGroups: Map<string, VaultEntry[]>): string[] {
const preferredOrder = ['Children', 'Events', 'Referenced By']
const customLabels = [...inverseGroups.keys()]
.filter((label) => !preferredOrder.includes(label))
.sort((a, b) => a.localeCompare(b))
return [...preferredOrder, ...customLabels]
}
function collectInverseRelationshipGroups(
entity: VaultEntry,
allEntries: VaultEntry[],
): RelationshipGroup[] {
const inverseGroups = new Map<string, VaultEntry[]>()
for (const other of allEntries) {
if (other.path === entity.path) continue
appendLegacyInverseRelationshipEntries(inverseGroups, entity, other)
appendDynamicInverseRelationshipEntries(inverseGroups, entity, other)
}
return orderInverseRelationshipLabels(inverseGroups)
.map((label) => {
const entries = inverseGroups.get(label)
if (!entries) return null
return { label, entries: [...entries].sort(sortByModified) }
})
.filter((group): group is RelationshipGroup => group !== null)
}
export function buildRelationshipGroups(
entity: VaultEntry,
allEntries: VaultEntry[],
@@ -313,9 +388,7 @@ export function buildRelationshipGroups(
.sort((a, b) => a.localeCompare(b))
.forEach((key) => b.addFromRefs(key, rels[key] ?? []))
b.filterAndAdd('Children', (e) => e.isA !== 'Event' && refsMatch(e.belongsTo, entity))
b.filterAndAdd('Events', (e) => e.isA === 'Event' && (refsMatch(e.belongsTo, entity) || refsMatch(e.relatedTo, entity)))
b.filterAndAdd('Referenced By', (e) => e.isA !== 'Event' && refsMatch(e.relatedTo, entity))
collectInverseRelationshipGroups(entity, allEntries).forEach((group) => b.add(group.label, group.entries))
b.add('Backlinks', findBacklinks(entity, allEntries).sort(sortByModified))
return b.groups