diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index 1dbce775..cd871347 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -108,6 +108,20 @@ describe('buildDynamicSections', () => { const sections = buildDynamicSections(entries, typeEntryMap) expect(sections.map((s) => s.type)).not.toContain('Old') }) + + it('excludes the legacy Journal section even when journal notes still exist', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'Daily Log', isA: 'Journal' }, + ] + const typeEntryMap: Record = { + Journal: { ...baseEntry, title: 'Journal', isA: 'Type' }, + journal: { ...baseEntry, title: 'Journal', isA: 'Type' }, + } + + const sections = buildDynamicSections(entries, typeEntryMap) + + expect(sections.map((section) => section.type)).not.toContain('Journal') + }) }) describe('collectActiveTypes', () => { diff --git a/src/hooks/commands/typeCommands.ts b/src/hooks/commands/typeCommands.ts index ba048595..7819dcfc 100644 --- a/src/hooks/commands/typeCommands.ts +++ b/src/hooks/commands/typeCommands.ts @@ -1,22 +1,12 @@ import type { CommandAction } from './types' -import type { SidebarSelection, VaultEntry } from '../../types' +import type { SidebarSelection } from '../../types' +import { canonicalizeTypeName } from '../../utils/vaultTypes' const PLURAL_OVERRIDES: Record = { Person: 'People', Responsibility: 'Responsibilities', } -const DEFAULT_TYPES = ['Event', 'Person', 'Project', 'Note'] -const DEFAULT_TYPE_CANONICAL_CASE = new Map( - DEFAULT_TYPES.map(type => [type.toLowerCase(), type] as const), -) - -function canonicalizeTypeName(type: string): string | null { - const trimmedType = type.trim() - if (!trimmedType) return null - return DEFAULT_TYPE_CANONICAL_CASE.get(trimmedType.toLowerCase()) ?? trimmedType -} - export function pluralizeType(type: string): string { if (PLURAL_OVERRIDES[type]) return PLURAL_OVERRIDES[type] if (type.endsWith('s') || type.endsWith('x') || type.endsWith('ch') || type.endsWith('sh')) return `${type}es` @@ -24,29 +14,6 @@ export function pluralizeType(type: string): string { return `${type}s` } -export function extractVaultTypes(entries: VaultEntry[]): string[] { - const typeMap = new Map() - for (const e of entries) { - const rawType = - e.isA === 'Type' - ? e.title - : e.isA && e.isA !== 'Type' - ? e.isA - : null - if (!rawType) continue - - const canonicalType = canonicalizeTypeName(rawType) - if (!canonicalType) continue - - const typeKey = canonicalType.toLowerCase() - if (!typeMap.has(typeKey)) { - typeMap.set(typeKey, canonicalType) - } - } - if (typeMap.size === 0) return DEFAULT_TYPES - return Array.from(typeMap.values()).sort() -} - export function buildTypeCommands( types: string[], onCreateNoteOfType: (type: string) => void, diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index bc346e58..9b748171 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -306,6 +306,26 @@ describe('extractVaultTypes', () => { expect(extractVaultTypes(entries)).toEqual(['Note', 'Project']) }) + + it('omits the legacy Journal type from extracted command-palette types', () => { + 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']) + }) + + it('omits hidden types from extracted command-palette types', () => { + const entries = [ + { path: '/recipe.md', title: 'Recipe', isA: 'Type', visible: false }, + { path: '/dinner.md', title: 'Dinner', isA: 'Recipe' }, + { path: '/project.md', title: 'Project', isA: 'Type' }, + ] as never[] + + expect(extractVaultTypes(entries)).toEqual(['Project']) + }) }) describe('groupSortKey', () => { diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index 9b7966d3..b9701bc6 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -8,13 +8,15 @@ import { buildGitCommands } from './commands/gitCommands' import { buildViewCommands } from './commands/viewCommands' import { buildSettingsCommands } from './commands/settingsCommands' import { buildAiAgentCommands } from './commands/aiAgentCommands' -import { buildTypeCommands, extractVaultTypes } from './commands/typeCommands' +import { buildTypeCommands } from './commands/typeCommands' import { buildFilterCommands } from './commands/filterCommands' +import { extractVaultTypes } from '../utils/vaultTypes' // Re-export types and helpers for backward compatibility export type { CommandAction, CommandGroup } from './commands/types' export { groupSortKey } from './commands/types' -export { pluralizeType, extractVaultTypes, buildTypeCommands } from './commands/typeCommands' +export { pluralizeType, buildTypeCommands } from './commands/typeCommands' +export { extractVaultTypes } from '../utils/vaultTypes' export { buildViewCommands } from './commands/viewCommands' interface CommandRegistryConfig { diff --git a/src/hooks/useNoteCreation.test.ts b/src/hooks/useNoteCreation.test.ts index 1af3e8e2..47e9650a 100644 --- a/src/hooks/useNoteCreation.test.ts +++ b/src/hooks/useNoteCreation.test.ts @@ -158,6 +158,12 @@ describe('resolveNewNote', () => { const { entry } = resolveNewNote({ title: 'ML', type: 'Topic', vaultPath: '/vault' }) expect(entry.status).toBeNull() }) + + it('treats Journal as a regular type after removing the journaling flow', () => { + const { entry, content } = resolveNewNote({ title: 'Reflection', type: 'Journal', vaultPath: '/vault' }) + expect(entry.status).toBe('Active') + expect(content).toContain('status: Active') + }) }) describe('resolveNewType', () => { diff --git a/src/hooks/useNoteCreation.ts b/src/hooks/useNoteCreation.ts index 16ead6e3..a06986fe 100644 --- a/src/hooks/useNoteCreation.ts +++ b/src/hooks/useNoteCreation.ts @@ -63,7 +63,7 @@ export function entryMatchesTarget({ entry, target }: EntryMatchParams): boolean return resolveEntry([entry], target) === entry } -const NO_STATUS_TYPES = new Set(['Topic', 'Person', 'Journal']) +const NO_STATUS_TYPES = new Set(['Topic', 'Person']) /** Default templates for built-in types. Used when the type entry has no custom template. */ export const DEFAULT_TEMPLATES: Record = { diff --git a/src/utils/legacyTypes.ts b/src/utils/legacyTypes.ts new file mode 100644 index 00000000..e80adc3f --- /dev/null +++ b/src/utils/legacyTypes.ts @@ -0,0 +1,5 @@ +const LEGACY_JOURNAL_TYPE = 'journal' + +export function isLegacyJournalingType(type: string | null | undefined): boolean { + return type?.trim().toLowerCase() === LEGACY_JOURNAL_TYPE +} diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index 24d8986f..d4a3a85e 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -7,6 +7,7 @@ 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 { Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, Tag, StackSimple, @@ -28,12 +29,28 @@ 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 resolveEntrySectionType(entry: VaultEntry): string { + return entry.isA || 'Note' +} + +function shouldCollectActiveType(entry: VaultEntry): boolean { + if (!isActive(entry) || !isMarkdown(entry)) return false + return isSupportedSectionType(resolveEntrySectionType(entry)) +} + +function shouldIncludeTypeDefinition(name: string, entry: VaultEntry): boolean { + if (name !== entry.title || !isActive(entry)) return false + return isSupportedSectionType(name) +} /** Collect unique isA values from active (non-archived) markdown entries. Untyped entries count as 'Note'. */ export function collectActiveTypes(entries: VaultEntry[]): Set { const types = new Set() for (const e of entries) { - if (isActive(e) && isMarkdown(e)) types.add(e.isA || 'Note') + if (!shouldCollectActiveType(e)) continue + types.add(resolveEntrySectionType(e)) } return types } @@ -59,9 +76,8 @@ export function buildSectionGroup(type: string, typeEntryMap: Record): SectionGroup[] { const activeTypes = collectActiveTypes(entries) for (const [name, entry] of Object.entries(typeEntryMap)) { - if (name === entry.title && isActive(entry)) { - activeTypes.add(name) - } + if (!shouldIncludeTypeDefinition(name, entry)) continue + activeTypes.add(name) } return Array.from(activeTypes, (type) => buildSectionGroup(type, typeEntryMap)) } diff --git a/src/utils/vaultTypes.ts b/src/utils/vaultTypes.ts new file mode 100644 index 00000000..41220e34 --- /dev/null +++ b/src/utils/vaultTypes.ts @@ -0,0 +1,64 @@ +import type { VaultEntry } from '../types' +import { isLegacyJournalingType } from './legacyTypes' + +const DEFAULT_TYPES = ['Event', 'Person', 'Project', 'Note'] +const DEFAULT_TYPE_CANONICAL_CASE = new Map( + DEFAULT_TYPES.map((type) => [type.toLowerCase(), type] as const), +) + +export function canonicalizeTypeName(type: string): string | null { + const trimmedType = type.trim() + if (!trimmedType) return null + return DEFAULT_TYPE_CANONICAL_CASE.get(trimmedType.toLowerCase()) ?? trimmedType +} + +function resolveEntryType(entry: VaultEntry): string | null { + if (entry.isA === 'Type') return entry.title + if (entry.isA && entry.isA !== 'Type') return entry.isA + return null +} + +function addCanonicalType(typeMap: Map, rawType: string | null): void { + if (!rawType) return + + const canonicalType = canonicalizeTypeName(rawType) + if (!canonicalType) return + + const typeKey = canonicalType.toLowerCase() + if (!typeMap.has(typeKey)) { + typeMap.set(typeKey, canonicalType) + } +} + +function collectHiddenTypeKeys(entries: VaultEntry[]): Set { + const hiddenTypeKeys = new Set() + + for (const entry of entries) { + if (entry.isA !== 'Type' || entry.visible !== false) continue + + const canonicalType = canonicalizeTypeName(entry.title) + if (!canonicalType) continue + + hiddenTypeKeys.add(canonicalType.toLowerCase()) + } + + return hiddenTypeKeys +} + +function shouldIncludeCommandPaletteType(type: string, hiddenTypeKeys: Set): boolean { + const typeKey = type.toLowerCase() + return !hiddenTypeKeys.has(typeKey) && !isLegacyJournalingType(type) +} + +export function extractVaultTypes(entries: VaultEntry[]): string[] { + const typeMap = new Map() + + for (const entry of entries) { + addCanonicalType(typeMap, resolveEntryType(entry)) + } + + const hiddenTypeKeys = collectHiddenTypeKeys(entries) + const sourceTypes = typeMap.size === 0 ? DEFAULT_TYPES : Array.from(typeMap.values()).sort() + + return sourceTypes.filter((type) => shouldIncludeCommandPaletteType(type, hiddenTypeKeys)) +} diff --git a/tests/smoke/command-palette-hides-legacy-journal.spec.ts b/tests/smoke/command-palette-hides-legacy-journal.spec.ts new file mode 100644 index 00000000..d2c6fa74 --- /dev/null +++ b/tests/smoke/command-palette-hides-legacy-journal.spec.ts @@ -0,0 +1,54 @@ +import fs from 'fs' +import path from 'path' +import { test, expect } from '@playwright/test' +import { + createFixtureVaultCopy, + openFixtureVault, + removeFixtureVaultCopy, +} from '../helpers/fixtureVault' +import { openCommandPalette } from './helpers' + +let tempVaultDir: string + +function seedLegacyJournalVault(vaultPath: string): void { + fs.writeFileSync(path.join(vaultPath, 'journal.md'), `--- +type: Type +order: 12 +icon: book-bookmark +color: yellow +--- + +# Journal +`) + + fs.writeFileSync(path.join(vaultPath, 'daily-log.md'), `--- +title: Daily Log +type: Journal +--- + +# Daily Log +`) +} + +test.describe('command palette hides the legacy Journal type', () => { + test.beforeEach(() => { + tempVaultDir = createFixtureVaultCopy() + seedLegacyJournalVault(tempVaultDir) + }) + + test.afterEach(() => { + removeFixtureVaultCopy(tempVaultDir) + }) + + test('legacy Journal does not appear in the sidebar or command palette', async ({ page }) => { + await openFixtureVault(page, tempVaultDir) + + await expect(page.locator('nav').getByText('Journals', { exact: true })).toHaveCount(0) + + await openCommandPalette(page) + await page.locator('input[placeholder="Type a command..."]').fill('journal') + + await expect(page.getByText('No matching commands', { exact: true })).toBeVisible() + await expect(page.locator('div.mx-1.flex.cursor-pointer')).toHaveCount(0) + }) +})