From ed9ceff1d2cdca4a769cd6a3e6748f34d922dc4b Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 18 Apr 2026 13:07:17 +0200 Subject: [PATCH] fix: align Notes counts with explicit type --- src/components/NoteList.behavior.test.tsx | 17 +++++++++ src/components/Sidebar.test.tsx | 41 ++++++++++++++++++++-- src/components/sidebar/SidebarSections.tsx | 5 ++- src/utils/sidebarSections.ts | 11 +++--- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/components/NoteList.behavior.test.tsx b/src/components/NoteList.behavior.test.tsx index 4c551667..d1678407 100644 --- a/src/components/NoteList.behavior.test.tsx +++ b/src/components/NoteList.behavior.test.tsx @@ -262,6 +262,23 @@ describe('NoteList filter pills', () => { expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() }) + it('shows only explicit Note entries for the Notes type filter', () => { + const noteEntries = [ + makeEntry({ title: 'Note Type', isA: 'Type', path: '/types/note.md', filename: 'note.md' }), + makeEntry({ title: 'Explicit Note', isA: 'Note', path: '/explicit-note.md', filename: 'explicit-note.md' }), + makeEntry({ title: 'Untyped Note', isA: null, path: '/untyped-note.md', filename: 'untyped-note.md' }), + makeEntry({ title: 'Archived Explicit Note', isA: 'Note', archived: true, path: '/archived-note.md', filename: 'archived-note.md' }), + ] + + renderNoteList({ entries: noteEntries, selection: { kind: 'sectionGroup', type: 'Note' } }) + + expect(screen.getByText('Explicit Note')).toBeInTheDocument() + expect(screen.queryByText('Untyped Note')).not.toBeInTheDocument() + expect(screen.queryByText('Archived Explicit Note')).not.toBeInTheDocument() + expect(screen.getByTestId('filter-pill-open')).toHaveTextContent('1') + expect(screen.getByTestId('filter-pill-archived')).toHaveTextContent('1') + }) + it('shows the archived empty state when a section has no archived notes', () => { renderNoteList({ entries: projectEntries.filter((entry) => !entry.archived), diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 4e9a657d..824aab12 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -780,6 +780,14 @@ describe('Sidebar', () => { icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, outgoingLinks: [], properties: {}, }, + { + path: '/vault/binary-note.pdf', filename: 'binary-note.pdf', title: 'Binary Note', + isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, + cadence: null, archived: false, modifiedAt: 1700000000, + createdAt: null, fileSize: 150, snippet: '', wordCount: 0, relationships: {}, + icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, + outgoingLinks: [], properties: {}, fileKind: 'binary', + }, ] it('shows Notes section when Note entries exist', () => { @@ -787,13 +795,40 @@ describe('Sidebar', () => { expect(screen.getByText('Notes')).toBeInTheDocument() }) - it('counts both explicit and untyped notes in Notes section chip', () => { + it('counts only explicit Note entries in the Notes section chip', () => { render( {}} />) const notesHeader = screen.getByText('Notes').closest('[class*="group/section"]')! + expect(notesHeader.textContent).toContain('1') + }) + + it('ignores non-markdown Note entries in the Notes section chip', () => { + render( {}} />) + const notesHeader = screen.getByText('Notes').closest('[class*="group/section"]')! + expect(notesHeader.textContent).toContain('1') + expect(notesHeader.textContent).not.toContain('2') + }) + + it('keeps the Notes section count aligned when an entry changes to or from Note', () => { + const { rerender } = render( {}} />) + let notesHeader = screen.getByText('Notes').closest('[class*="group/section"]')! + expect(notesHeader.textContent).toContain('1') + + const withoutExplicitNote = noteEntries.map((entry) => + entry.path === '/vault/explicit-note.md' ? { ...entry, isA: null } : entry, + ) + rerender( {}} />) + notesHeader = screen.getByText('Notes').closest('[class*="group/section"]')! + expect(notesHeader.textContent).toBe('Notes') + + const withNewExplicitNote = noteEntries.map((entry) => + entry.path === '/vault/untyped-note.md' ? { ...entry, isA: 'Note' } : entry, + ) + rerender( {}} />) + notesHeader = screen.getByText('Notes').closest('[class*="group/section"]')! expect(notesHeader.textContent).toContain('2') }) - it('shows Notes section for untyped entries even without explicit Note entries', () => { + it('does not show Notes section for untyped entries without explicit Note entries', () => { const untypedOnly: VaultEntry[] = [ { path: '/vault/plain.md', filename: 'plain.md', title: 'Plain Note', @@ -805,7 +840,7 @@ describe('Sidebar', () => { }, ] render( {}} />) - expect(screen.getByText('Notes')).toBeInTheDocument() + expect(screen.queryByText('Notes')).not.toBeInTheDocument() }) }) diff --git a/src/components/sidebar/SidebarSections.tsx b/src/components/sidebar/SidebarSections.tsx index f63500b5..592a1c7a 100644 --- a/src/components/sidebar/SidebarSections.tsx +++ b/src/components/sidebar/SidebarSections.tsx @@ -20,6 +20,7 @@ import { TypeCustomizePopover } from '../TypeCustomizePopover' import { useDragRegion } from '../../hooks/useDragRegion' import { SidebarGroupHeader } from './SidebarGroupHeader' import { SidebarViewItem } from './SidebarViewItem' +import { countByFilter } from '../../utils/noteListHelpers' export { SidebarTopNav } from './SidebarTopNav' export { FavoritesSection } from './FavoritesSection' @@ -94,9 +95,7 @@ function SortableSection({ sectionProps: SidebarSectionProps }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: group.type }) - const itemCount = sectionProps.entries.filter((entry) => - !entry.archived && (group.type === 'Note' ? (entry.isA === 'Note' || !entry.isA) : entry.isA === group.type), - ).length + const itemCount = countByFilter(sectionProps.entries, group.type).open const isRenaming = sectionProps.renamingType === group.type return ( diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index d4a3a85e..21c64ef0 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -31,13 +31,10 @@ 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)) + if (!entry.isA) return false + return isSupportedSectionType(entry.isA) } function shouldIncludeTypeDefinition(name: string, entry: VaultEntry): boolean { @@ -45,12 +42,12 @@ function shouldIncludeTypeDefinition(name: string, entry: VaultEntry): boolean { return isSupportedSectionType(name) } -/** Collect unique isA values from active (non-archived) markdown entries. Untyped entries count as 'Note'. */ +/** Collect unique explicit isA values from active (non-archived) markdown entries. */ export function collectActiveTypes(entries: VaultEntry[]): Set { const types = new Set() for (const e of entries) { if (!shouldCollectActiveType(e)) continue - types.add(resolveEntrySectionType(e)) + types.add(e.isA!) } return types }