fix: sync sidebar types with live vault state
This commit is contained in:
@@ -113,6 +113,16 @@ describe('buildDynamicSections', () => {
|
||||
const entries: VaultEntry[] = [
|
||||
{ ...baseEntry, title: 'Daily Log', isA: 'Journal' },
|
||||
]
|
||||
|
||||
const sections = buildDynamicSections(entries, {})
|
||||
|
||||
expect(sections.map((section) => section.type)).not.toContain('Journal')
|
||||
})
|
||||
|
||||
it('includes Journal when a real Journal type definition exists', () => {
|
||||
const entries: VaultEntry[] = [
|
||||
{ ...baseEntry, title: 'Daily Log', isA: 'journal' },
|
||||
]
|
||||
const typeEntryMap: Record<string, VaultEntry> = {
|
||||
Journal: { ...baseEntry, title: 'Journal', isA: 'Type' },
|
||||
journal: { ...baseEntry, title: 'Journal', isA: 'Type' },
|
||||
@@ -120,7 +130,7 @@ describe('buildDynamicSections', () => {
|
||||
|
||||
const sections = buildDynamicSections(entries, typeEntryMap)
|
||||
|
||||
expect(sections.map((section) => section.type)).not.toContain('Journal')
|
||||
expect(sections.filter((section) => section.type === 'Journal')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -517,11 +517,17 @@ describe('Sidebar', () => {
|
||||
})
|
||||
|
||||
describe('type visibility via visible property', () => {
|
||||
const makeTypeEntry = (title: string, visible: boolean | null): VaultEntry => ({
|
||||
path: `/vault/${title.toLowerCase()}.md`,
|
||||
filename: `${title.toLowerCase()}.md`,
|
||||
title,
|
||||
isA: 'Type',
|
||||
const makeSidebarEntry = (overrides: {
|
||||
path: string
|
||||
filename: string
|
||||
title: string
|
||||
isA: string
|
||||
visible?: boolean | null
|
||||
}): VaultEntry => ({
|
||||
path: overrides.path,
|
||||
filename: overrides.filename,
|
||||
title: overrides.title,
|
||||
isA: overrides.isA,
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
@@ -542,11 +548,19 @@ describe('Sidebar', () => {
|
||||
template: null,
|
||||
sort: null,
|
||||
view: null,
|
||||
visible,
|
||||
visible: overrides.visible ?? null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
})
|
||||
|
||||
const makeTypeEntry = (title: string, visible: boolean | null): VaultEntry => makeSidebarEntry({
|
||||
path: `/vault/${title.toLowerCase()}.md`,
|
||||
filename: `${title.toLowerCase()}.md`,
|
||||
title,
|
||||
isA: 'Type',
|
||||
visible,
|
||||
})
|
||||
|
||||
it('hides a section when its Type entry has visible: false', () => {
|
||||
const entries: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
@@ -620,6 +634,34 @@ describe('Sidebar', () => {
|
||||
expect(screen.getByLabelText('Toggle Topics')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the sidebar type picker when Journal becomes a real type while the app stays open', () => {
|
||||
const journalEntries: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
makeSidebarEntry({
|
||||
path: '/vault/april-21.md',
|
||||
filename: 'april-21.md',
|
||||
title: 'April 21',
|
||||
isA: 'journal',
|
||||
}),
|
||||
]
|
||||
const { rerender } = render(<Sidebar entries={journalEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
|
||||
fireEvent.click(screen.getByTitle('Customize sections'))
|
||||
expect(screen.queryByLabelText('Toggle Journals')).not.toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<Sidebar
|
||||
entries={[...journalEntries, makeTypeEntry('Journal', null)]}
|
||||
selection={defaultSelection}
|
||||
onSelect={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByLabelText('Toggle Journals')).toBeInTheDocument()
|
||||
|
||||
rerender(<Sidebar entries={journalEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
expect(screen.queryByLabelText('Toggle Journals')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('preserves custom section colors in the customize popover', () => {
|
||||
const entries: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
|
||||
@@ -376,14 +376,23 @@ describe('extractVaultTypes', () => {
|
||||
expect(extractVaultTypes(entries)).toEqual(['Note', 'Project'])
|
||||
})
|
||||
|
||||
it('omits the legacy Journal type from extracted command-palette types', () => {
|
||||
it('omits the legacy Journal type when no Type document defines it', () => {
|
||||
const entries = [
|
||||
{ path: '/2026-03-11.md', title: 'March 11', isA: 'Journal' },
|
||||
{ path: '/note.md', title: 'General Note', isA: 'Note' },
|
||||
] as never[]
|
||||
|
||||
expect(extractVaultTypes(entries)).toEqual(['Note'])
|
||||
})
|
||||
|
||||
it('includes Journal when a real Type document defines it', () => {
|
||||
const entries = [
|
||||
{ path: '/journal.md', title: 'Journal', isA: 'Type' },
|
||||
{ path: '/2026-03-11.md', title: 'March 11', isA: 'Journal' },
|
||||
{ path: '/note.md', title: 'General Note', isA: 'Note' },
|
||||
] as never[]
|
||||
|
||||
expect(extractVaultTypes(entries)).toEqual(['Note'])
|
||||
expect(extractVaultTypes(entries)).toEqual(['Journal', 'Note'])
|
||||
})
|
||||
|
||||
it('omits hidden types from extracted command-palette types', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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,
|
||||
@@ -29,17 +30,44 @@ const BUILT_IN_TYPE_MAP = new Map(BUILT_IN_SECTION_GROUPS.map((sg) => [sg.type,
|
||||
|
||||
const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind
|
||||
const isActive = (e: VaultEntry) => !e.archived
|
||||
const isSupportedSectionType = (type: string) => !isLegacyJournalingType(type)
|
||||
|
||||
function shouldCollectActiveType(entry: VaultEntry): boolean {
|
||||
if (!isActive(entry) || !isMarkdown(entry)) return false
|
||||
if (!entry.isA) return false
|
||||
return isSupportedSectionType(entry.isA)
|
||||
return Boolean(entry.isA)
|
||||
}
|
||||
|
||||
function shouldIncludeTypeDefinition(name: string, entry: VaultEntry): boolean {
|
||||
if (name !== entry.title || !isActive(entry)) return false
|
||||
return isSupportedSectionType(name)
|
||||
return name === entry.title && isActive(entry)
|
||||
}
|
||||
|
||||
function resolveTypeEntry(type: string, typeEntryMap: Record<string, VaultEntry>): VaultEntry | undefined {
|
||||
return typeEntryMap[type] ?? typeEntryMap[type.toLowerCase()]
|
||||
}
|
||||
|
||||
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. */
|
||||
@@ -71,12 +99,17 @@ export function buildSectionGroup(type: string, typeEntryMap: Record<string, Vau
|
||||
|
||||
/** Build sections dynamically from vault entries and defined types — types with 0 notes still appear */
|
||||
export function buildDynamicSections(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>): SectionGroup[] {
|
||||
const activeTypes = collectActiveTypes(entries)
|
||||
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
|
||||
activeTypes.add(name)
|
||||
addSectionType(activeTypes, name, typeEntryMap)
|
||||
}
|
||||
return Array.from(activeTypes, (type) => buildSectionGroup(type, typeEntryMap))
|
||||
return Array.from(activeTypes.values())
|
||||
.filter((type) => shouldIncludeSectionType(type, typeEntryMap))
|
||||
.map((type) => buildSectionGroup(type, typeEntryMap))
|
||||
}
|
||||
|
||||
export function sortSections(groups: SectionGroup[], typeEntryMap: Record<string, VaultEntry>): SectionGroup[] {
|
||||
|
||||
@@ -45,9 +45,16 @@ function collectHiddenTypeKeys(entries: VaultEntry[]): Set<string> {
|
||||
return hiddenTypeKeys
|
||||
}
|
||||
|
||||
function shouldIncludeCommandPaletteType(type: string, hiddenTypeKeys: Set<string>): boolean {
|
||||
function hasExplicitTypeDefinition(entries: VaultEntry[], type: string): boolean {
|
||||
const typeKey = type.trim().toLowerCase()
|
||||
return entries.some((entry) => entry.isA === 'Type' && !entry.archived && entry.title.trim().toLowerCase() === typeKey)
|
||||
}
|
||||
|
||||
function shouldIncludeCommandPaletteType(type: string, hiddenTypeKeys: Set<string>, entries: VaultEntry[]): boolean {
|
||||
const typeKey = type.toLowerCase()
|
||||
return !hiddenTypeKeys.has(typeKey) && !isLegacyJournalingType(type)
|
||||
if (hiddenTypeKeys.has(typeKey)) return false
|
||||
if (!isLegacyJournalingType(type)) return true
|
||||
return hasExplicitTypeDefinition(entries, type)
|
||||
}
|
||||
|
||||
export function extractVaultTypes(entries: VaultEntry[]): string[] {
|
||||
@@ -60,5 +67,5 @@ export function extractVaultTypes(entries: VaultEntry[]): string[] {
|
||||
const hiddenTypeKeys = collectHiddenTypeKeys(entries)
|
||||
const sourceTypes = typeMap.size === 0 ? DEFAULT_TYPES : Array.from(typeMap.values()).sort()
|
||||
|
||||
return sourceTypes.filter((type) => shouldIncludeCommandPaletteType(type, hiddenTypeKeys))
|
||||
return sourceTypes.filter((type) => shouldIncludeCommandPaletteType(type, hiddenTypeKeys, entries))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user