/** * Pure functions for building sidebar section groups from vault entries. * Extracted from Sidebar.tsx for testability. */ import type { VaultEntry } from '../types' import type { SectionGroup } from '../components/SidebarParts' import { resolveIcon } from './iconRegistry' import { pluralizeType } from '../hooks/useCommandRegistry' import { isLegacyJournalingType } from './legacyTypes' import { canonicalizeTypeName } from './vaultTypes' import { Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, Tag, StackSimple, } from '@phosphor-icons/react' const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [ { label: 'Projects', type: 'Project', Icon: Wrench }, { label: 'Experiments', type: 'Experiment', Icon: Flask }, { label: 'Responsibilities', type: 'Responsibility', Icon: Target }, { label: 'Procedures', type: 'Procedure', Icon: ArrowsClockwise }, { label: 'People', type: 'Person', Icon: Users }, { label: 'Events', type: 'Event', Icon: CalendarBlank }, { label: 'Topics', type: 'Topic', Icon: Tag }, { label: 'Types', type: 'Type', Icon: StackSimple }, ] /** 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])) const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind const isActive = (e: VaultEntry) => !e.archived function shouldCollectActiveType(entry: VaultEntry): boolean { if (!isActive(entry) || !isMarkdown(entry)) return false return Boolean(entry.isA) } function shouldIncludeTypeDefinition(name: string, entry: VaultEntry): boolean { return name === entry.title && isActive(entry) } function resolveTypeEntry(type: string, typeEntryMap: Record): VaultEntry | undefined { return (Reflect.get(typeEntryMap, type) as VaultEntry | undefined) ?? (Reflect.get(typeEntryMap, type.toLowerCase()) as VaultEntry | undefined) } function hasExplicitTypeDefinition(type: string, typeEntryMap: Record): boolean { const typeEntry = resolveTypeEntry(type, typeEntryMap) return Boolean(typeEntry && typeEntry.title.trim().toLowerCase() === type.trim().toLowerCase() && isActive(typeEntry)) } function shouldIncludeSectionType(type: string, typeEntryMap: Record): boolean { if (!isLegacyJournalingType(type)) return true return hasExplicitTypeDefinition(type, typeEntryMap) } function canonicalizeSectionType(type: string, typeEntryMap: Record): string | null { const trimmedType = type.trim() if (!trimmedType) return null return resolveTypeEntry(trimmedType, typeEntryMap)?.title ?? canonicalizeTypeName(trimmedType) } function addSectionType(typeMap: Map, rawType: string, typeEntryMap: Record): void { const canonicalType = canonicalizeSectionType(rawType, typeEntryMap) if (!canonicalType) return const typeKey = canonicalType.toLowerCase() if (!typeMap.has(typeKey)) { typeMap.set(typeKey, canonicalType) } } /** Collect unique explicit isA values from active (non-archived) markdown entries. */ export function collectActiveTypes(entries: VaultEntry[]): Set { const types = new Set() for (const e of entries) { if (!shouldCollectActiveType(e)) continue const typeName = e.isA if (typeName) types.add(typeName) } return types } function resolveLabel( type: string, typeEntry: VaultEntry | undefined, builtIn: SectionGroup | undefined, pluralizeTypeLabel: boolean, ): string { if (typeEntry?.sidebarLabel) return typeEntry.sidebarLabel if (!pluralizeTypeLabel) return type return 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, pluralizeTypeLabel = true, ): SectionGroup { const builtIn = BUILT_IN_TYPE_MAP.get(type) const typeEntry = Reflect.get(typeEntryMap, type) as VaultEntry | undefined const customColor = typeEntry?.color ?? null const label = resolveLabel(type, typeEntry, builtIn, pluralizeTypeLabel) const icon = resolveIcon(typeEntry?.icon ?? null) if (builtIn) { return { ...builtIn, label, Icon: typeEntry?.icon ? icon : builtIn.Icon, customColor } } return { label, type, Icon: icon, customColor } } /** Build sections dynamically from vault entries and defined types — types with 0 notes still appear */ export function buildDynamicSections( entries: VaultEntry[], typeEntryMap: Record, pluralizeTypeLabels = true, ): SectionGroup[] { const activeTypes = new Map() for (const type of collectActiveTypes(entries)) { addSectionType(activeTypes, type, typeEntryMap) } for (const [name, entry] of Object.entries(typeEntryMap)) { if (!shouldIncludeTypeDefinition(name, entry)) continue addSectionType(activeTypes, name, typeEntryMap) } return Array.from(activeTypes.values()) .filter((type) => shouldIncludeSectionType(type, typeEntryMap)) .map((type) => buildSectionGroup(type, typeEntryMap, pluralizeTypeLabels)) } export function sortSections(groups: SectionGroup[], typeEntryMap: Record): SectionGroup[] { return [...groups].sort((a, b) => { const orderA = (Reflect.get(typeEntryMap, a.type) as VaultEntry | undefined)?.order ?? Infinity const orderB = (Reflect.get(typeEntryMap, b.type) as VaultEntry | undefined)?.order ?? Infinity return orderA !== orderB ? orderA - orderB : a.label.localeCompare(b.label) }) } export { BUILT_IN_SECTION_GROUPS }