Files
tolaria/src/utils/sidebarSections.ts
2026-05-26 10:52:53 +02:00

141 lines
5.6 KiB
TypeScript

/**
* 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<string, VaultEntry>): VaultEntry | undefined {
return (Reflect.get(typeEntryMap, type) as VaultEntry | undefined)
?? (Reflect.get(typeEntryMap, type.toLowerCase()) as VaultEntry | undefined)
}
function hasExplicitTypeDefinition(type: string, typeEntryMap: Record<string, VaultEntry>): boolean {
const typeEntry = resolveTypeEntry(type, typeEntryMap)
return Boolean(typeEntry && typeEntry.title.trim().toLowerCase() === type.trim().toLowerCase() && isActive(typeEntry))
}
function shouldIncludeSectionType(type: string, typeEntryMap: Record<string, VaultEntry>): boolean {
if (!isLegacyJournalingType(type)) return true
return hasExplicitTypeDefinition(type, typeEntryMap)
}
function canonicalizeSectionType(type: string, typeEntryMap: Record<string, VaultEntry>): string | null {
const trimmedType = type.trim()
if (!trimmedType) return null
return resolveTypeEntry(trimmedType, typeEntryMap)?.title ?? canonicalizeTypeName(trimmedType)
}
function addSectionType(typeMap: Map<string, string>, rawType: string, typeEntryMap: Record<string, VaultEntry>): 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<string> {
const types = new Set<string>()
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<string, VaultEntry>,
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<string, VaultEntry>,
pluralizeTypeLabels = true,
): SectionGroup[] {
const activeTypes = new Map<string, string>()
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<string, VaultEntry>): 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 }