diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index cd871347..06b224c6 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -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 = { 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) }) }) diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 75cfcd10..bc63a47c 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -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( {}} />) + + fireEvent.click(screen.getByTitle('Customize sections')) + expect(screen.queryByLabelText('Toggle Journals')).not.toBeInTheDocument() + + rerender( + {}} + />, + ) + expect(screen.getByLabelText('Toggle Journals')).toBeInTheDocument() + + rerender( {}} />) + expect(screen.queryByLabelText('Toggle Journals')).not.toBeInTheDocument() + }) + it('preserves custom section colors in the customize popover', () => { const entries: VaultEntry[] = [ ...mockEntries, diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index bf127b98..6dae1c1b 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -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', () => { diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index 21c64ef0..3b0061ee 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -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): VaultEntry | undefined { + return typeEntryMap[type] ?? typeEntryMap[type.toLowerCase()] +} + +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. */ @@ -71,12 +99,17 @@ export function buildSectionGroup(type: string, typeEntryMap: Record): SectionGroup[] { - const activeTypes = collectActiveTypes(entries) + 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 - 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): SectionGroup[] { diff --git a/src/utils/vaultTypes.ts b/src/utils/vaultTypes.ts index 41220e34..0e86550f 100644 --- a/src/utils/vaultTypes.ts +++ b/src/utils/vaultTypes.ts @@ -45,9 +45,16 @@ function collectHiddenTypeKeys(entries: VaultEntry[]): Set { return hiddenTypeKeys } -function shouldIncludeCommandPaletteType(type: string, hiddenTypeKeys: Set): 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, 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)) }