fix: show type label for all notes in wiki-link autocomplete (#76)

* fix: show type label for all notes in wiki-link autocomplete

Notes with the default 'Note' type (isA: null) had noteType set to
undefined, hiding their type label. Now every note always shows its
type in the autocomplete dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* design: add wiki-autocomplete-type-fix wireframe

Shows the fixed autocomplete dropdown where every note displays its
type label, including default "Note" types in muted color.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Rossi
2026-02-25 18:30:35 +01:00
committed by GitHub
parent 48cf359eaa
commit 462b920da1
3 changed files with 269 additions and 2 deletions

View File

@@ -0,0 +1,238 @@
{
"children": [
{
"type": "frame",
"id": "wikiTypeFix",
"x": 0,
"y": 0,
"name": "Wiki-link Autocomplete — All Notes Show Type Labels",
"clip": true,
"width": 400,
"height": 340,
"fill": "#FFFFFF",
"layout": "vertical",
"gap": 0,
"children": [
{
"type": "frame",
"id": "wtfHeader",
"name": "Context: Editor line with [[query",
"width": "fill_container",
"height": 40,
"fill": "#FAFAFA",
"padding": [10, 16],
"alignItems": "center",
"children": [
{
"type": "text",
"id": "wtfQuery",
"content": "Linking to [[no",
"fontSize": 15,
"fill": "#1a1a1a"
}
]
},
{
"type": "frame",
"id": "wtfMenu",
"name": "Suggestion Menu — every item shows type",
"width": 320,
"fill": "#FFFFFF",
"layout": "vertical",
"gap": 0,
"padding": 4,
"cornerRadius": 6,
"stroke": {
"align": "outside",
"thickness": 1,
"fill": "rgba(0,0,0,0.1)"
},
"children": [
{
"type": "frame",
"id": "wtfItem1",
"name": "Item: Daily Notes (Note — default type shown)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"fill": "rgba(21,93,255,0.08)",
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle1",
"content": "Daily Notes",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType1",
"content": "Note",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--muted-foreground, #888888)",
"opacity": 0.8
}
]
},
{
"type": "frame",
"id": "wtfItem2",
"name": "Item: Node.js Setup (Note — no frontmatter type)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle2",
"content": "Node.js Setup",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType2",
"content": "Note",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--muted-foreground, #888888)",
"opacity": 0.8
}
]
},
{
"type": "frame",
"id": "wtfItem3",
"name": "Item: Notification System (Project — explicit type)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle3",
"content": "Notification System",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType3",
"content": "Project",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--accent-red, #E53935)",
"opacity": 0.8
}
]
},
{
"type": "frame",
"id": "wtfItem4",
"name": "Item: NoSQL Patterns (Topic — explicit type)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle4",
"content": "NoSQL Patterns",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType4",
"content": "Topic",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--accent-green, #43A047)",
"opacity": 0.8
}
]
},
{
"type": "frame",
"id": "wtfItem5",
"name": "Item: Norman (Person — explicit type)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle5",
"content": "Norman",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType5",
"content": "Person",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--accent-yellow, #FDD835)",
"opacity": 0.8
}
]
},
{
"type": "frame",
"id": "wtfItem6",
"name": "Item: Notes on Architecture (Note — default type shown)",
"width": "fill_container",
"height": 32,
"padding": [6, 10],
"cornerRadius": 4,
"alignItems": "center",
"justifyContent": "space-between",
"children": [
{
"type": "text",
"id": "wtfTitle6",
"content": "Notes on Architecture",
"fontSize": 13,
"fill": "#1a1a1a",
"fontWeight": "400"
},
{
"type": "text",
"id": "wtfType6",
"content": "Note",
"fontSize": 11,
"fontWeight": "500",
"fill": "var(--muted-foreground, #888888)",
"opacity": 0.8
}
]
}
]
}
]
}
],
"variables": {}
}

View File

@@ -390,6 +390,35 @@ describe('wikilink autocomplete', () => {
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
})
it('always includes noteType for every item, including default Note type (regression)', async () => {
const mixedEntries: VaultEntry[] = [
{ ...mockEntry, title: 'My Project', filename: 'proj.md', path: '/vault/proj.md', isA: 'Project', aliases: [] },
{ ...mockEntry, title: 'Plain Note', filename: 'plain.md', path: '/vault/plain.md', isA: null, aliases: [] },
{ ...mockEntry, title: 'Explicit Note', filename: 'explicit.md', path: '/vault/explicit.md', isA: 'Note', aliases: [] },
]
capturedGetItems = null
mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items)
render(
<Editor
{...defaultProps}
tabs={[mockTab]}
activeTabPath={mockEntry.path}
entries={mixedEntries}
/>
)
const items = await capturedGetItems!('Note')
// Every item must have a defined noteType — none should be blank
for (const item of items) {
expect(item.noteType).toBeTruthy()
expect(item.typeColor).toBeTruthy()
}
// Default notes (isA: null) should show 'Note' as their type
const plainNote = items.find((i: { title: string }) => i.title === 'Plain Note')
expect(plainNote).toBeDefined()
expect(plainNote!.noteType).toBe('Note')
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: [] },

View File

@@ -165,8 +165,8 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e
const final = disambiguateTitles(deduplicateByPath(filtered))
return final.map(({ group, ...rest }) => ({
...rest,
noteType: group !== 'Note' ? group : undefined,
typeColor: group !== 'Note' ? getTypeColor(group) : undefined,
noteType: group,
typeColor: getTypeColor(group),
}))
}, [baseItems, editor])