141 lines
5.6 KiB
TypeScript
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 }
|