From b10cf8d14eaca87920bc93fe79f870df809befaa Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 5 Apr 2026 10:31:47 +0200 Subject: [PATCH] fix: show types with 0 notes in sidebar by iterating typeEntryMap Types created via "Create Type" now appear in the sidebar immediately, even before any notes of that type exist. The buildDynamicSections function now adds all active (non-trashed/archived) type definitions from typeEntryMap to the active types set, using name === entry.title guard to avoid duplicate entries from lowercase aliases. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/Sidebar.test.ts | 70 +++++++++++++++++++++++++++++++++- src/utils/sidebarSections.ts | 26 +++++++++---- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index 195c189a..1f5f3444 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { buildSectionGroup } from '../utils/sidebarSections' +import { buildSectionGroup, buildDynamicSections, collectActiveTypes } from '../utils/sidebarSections' import { resolveIcon } from '../utils/iconRegistry' import type { VaultEntry } from '../types' import { GearSix, CookingPot, FileText } from '@phosphor-icons/react' @@ -71,3 +71,71 @@ describe('buildSectionGroup', () => { expect(group.Icon).toBe(GearSix) }) }) + +describe('buildDynamicSections', () => { + it('includes types with 0 notes from typeEntryMap', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'My Note', isA: 'Note' }, + ] + const typeEntryMap: Record = { + Recipe: { ...baseEntry, title: 'Recipe', isA: 'Type', icon: 'cooking-pot' }, + recipe: { ...baseEntry, title: 'Recipe', isA: 'Type', icon: 'cooking-pot' }, + } + const sections = buildDynamicSections(entries, typeEntryMap) + const types = sections.map((s) => s.type) + expect(types).toContain('Note') + expect(types).toContain('Recipe') + }) + + it('does not duplicate types that have both entries and type definitions', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'My Project', isA: 'Project' }, + ] + const typeEntryMap: Record = { + Project: { ...baseEntry, title: 'Project', isA: 'Type' }, + project: { ...baseEntry, title: 'Project', isA: 'Type' }, + } + const sections = buildDynamicSections(entries, typeEntryMap) + const projectSections = sections.filter((s) => s.type === 'Project') + expect(projectSections).toHaveLength(1) + }) + + it('excludes trashed type definitions', () => { + const entries: VaultEntry[] = [] + const typeEntryMap: Record = { + Deleted: { ...baseEntry, title: 'Deleted', isA: 'Type', trashed: true }, + } + const sections = buildDynamicSections(entries, typeEntryMap) + expect(sections.map((s) => s.type)).not.toContain('Deleted') + }) + + it('excludes archived type definitions', () => { + const entries: VaultEntry[] = [] + const typeEntryMap: Record = { + Old: { ...baseEntry, title: 'Old', isA: 'Type', archived: true }, + } + const sections = buildDynamicSections(entries, typeEntryMap) + expect(sections.map((s) => s.type)).not.toContain('Old') + }) +}) + +describe('collectActiveTypes', () => { + it('excludes non-markdown entries from type collection', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'My Note', isA: 'Note', fileKind: 'markdown' }, + { ...baseEntry, title: 'config.yml', isA: null, fileKind: 'text' }, + { ...baseEntry, title: 'photo.png', isA: null, fileKind: 'binary' }, + ] + const types = collectActiveTypes(entries) + expect(types).toContain('Note') + expect(types.size).toBe(1) + }) + + it('treats entries without fileKind as markdown', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'Legacy Note', isA: 'Note' }, + ] + const types = collectActiveTypes(entries) + expect(types).toContain('Note') + }) +}) diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index 1ab3e8fe..0cd9d2cd 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -26,31 +26,43 @@ const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [ /** Metadata lookup for well-known types (icon/label only — NOT used to determine which sections to show) */ const BUILT_IN_TYPE_MAP = new Map(BUILT_IN_SECTION_GROUPS.map((sg) => [sg.type, sg])) -/** Collect unique isA values from active (non-trashed, non-archived) entries. Untyped entries count as 'Note'. */ +const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind +const isActive = (e: VaultEntry) => !e.trashed && !e.archived + +/** Collect unique isA values from active (non-trashed, non-archived) markdown entries. Untyped entries count as 'Note'. */ export function collectActiveTypes(entries: VaultEntry[]): Set { const types = new Set() for (const e of entries) { - if (!e.trashed && !e.archived) types.add(e.isA || 'Note') + if (isActive(e) && isMarkdown(e)) types.add(e.isA || 'Note') } return types } +function resolveLabel(type: string, typeEntry: VaultEntry | undefined, builtIn: SectionGroup | undefined): string { + return typeEntry?.sidebarLabel || builtIn?.label || pluralizeType(type) +} + /** Build a single SectionGroup for a type, using built-in metadata or Type entry for icon/label */ export function buildSectionGroup(type: string, typeEntryMap: Record): SectionGroup { const builtIn = BUILT_IN_TYPE_MAP.get(type) const typeEntry = typeEntryMap[type] const customColor = typeEntry?.color ?? null - const label = typeEntry?.sidebarLabel || (builtIn?.label ?? pluralizeType(type)) + const label = resolveLabel(type, typeEntry, builtIn) + const icon = resolveIcon(typeEntry?.icon ?? null) if (builtIn) { - const Icon = typeEntry?.icon ? resolveIcon(typeEntry.icon) : builtIn.Icon - return { ...builtIn, label, Icon, customColor } + return { ...builtIn, label, Icon: typeEntry?.icon ? icon : builtIn.Icon, customColor } } - return { label, type, Icon: resolveIcon(typeEntry?.icon ?? null), customColor } + return { label, type, Icon: icon, customColor } } -/** Build sections dynamically from actual vault entries — only types with ≥1 active note appear */ +/** Build sections dynamically from vault entries and defined types — types with 0 notes still appear */ export function buildDynamicSections(entries: VaultEntry[], typeEntryMap: Record): SectionGroup[] { const activeTypes = collectActiveTypes(entries) + for (const [name, entry] of Object.entries(typeEntryMap)) { + if (name === entry.title && isActive(entry)) { + activeTypes.add(name) + } + } return Array.from(activeTypes, (type) => buildSectionGroup(type, typeEntryMap)) }