diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index abaaaca6..c2d19716 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -987,4 +987,67 @@ describe('Sidebar', () => { expect(onCreateNewType).toHaveBeenCalledOnce() }) }) + + describe('view note count chips', () => { + const mockViews = [ + { + filename: 'active-projects.yml', + definition: { + name: 'Active Projects', + icon: '🚀', + color: null, + sort: null, + filters: { all: [{ field: 'type', op: 'equals' as const, value: 'Project' }] }, + }, + }, + { + filename: 'all-topics.yml', + definition: { + name: 'All Topics', + icon: null, + color: null, + sort: null, + filters: { all: [{ field: 'type', op: 'equals' as const, value: 'Topic' }] }, + }, + }, + ] + + it('shows note count chip for each view matching the filter results', () => { + render( + {}} views={mockViews} /> + ) + // 'Active Projects' filters for type=Project -> mockEntries has 1 Project (build-app.md) + const projectLabel = screen.getByText('Active Projects') + const projectNavItem = projectLabel.closest('[class*="cursor-pointer"]')! + // The count chip is a sibling span inside the NavItem + const projectCount = projectNavItem.querySelector('span:last-child') + expect(projectCount?.textContent).toBe('1') + + // 'All Topics' filters for type=Topic -> mockEntries has 2 Topics + const topicLabel = screen.getByText('All Topics') + const topicNavItem = topicLabel.closest('[class*="cursor-pointer"]')! + const topicCount = topicNavItem.querySelector('span:last-child') + expect(topicCount?.textContent).toBe('2') + }) + + it('does not show count chip for views with 0 matching notes', () => { + const emptyView = [{ + filename: 'empty.yml', + definition: { + name: 'Empty View', + icon: null, + color: null, + sort: null, + filters: { all: [{ field: 'type', op: 'equals' as const, value: 'Nonexistent' }] }, + }, + }] + render( + {}} views={emptyView} /> + ) + expect(screen.getByText('Empty View')).toBeInTheDocument() + // No count chip rendered for 0 results (NavItem hides count <= 0) + const viewContainer = screen.getByText('Empty View').closest('div') + expect(viewContainer?.querySelector('span:last-child')?.textContent).not.toBe('0') + }) + }) }) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e0d499ac..870e0442 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -15,6 +15,7 @@ import { FileText, Trash, Archive, CaretLeft, Tray, CaretRight, CaretDown, Plus, Funnel, PencilSimple, } from '@phosphor-icons/react' import { isEmoji } from '../utils/emoji' +import { evaluateView } from '../utils/viewFilters' import { arrayMove } from '@dnd-kit/sortable' import { SlidersHorizontal } from 'lucide-react' import { @@ -169,19 +170,22 @@ function SidebarGroupHeader({ label, collapsed, onToggle, count, children }: { ) } -function ViewItem({ view, isActive, onSelect, onEditView, onDeleteView }: { +function ViewItem({ view, isActive, onSelect, onEditView, onDeleteView, entries }: { view: ViewFile isActive: boolean onSelect: () => void onEditView?: (filename: string) => void onDeleteView?: (filename: string) => void + entries: VaultEntry[] }) { + const count = useMemo(() => evaluateView(view.definition, entries).length, [view.definition, entries]) return (
@@ -209,7 +213,7 @@ function ViewItem({ view, isActive, onSelect, onEditView, onDeleteView }: { ) } -function ViewsSection({ views, selection, onSelect, collapsed, onToggle, onCreateView, onEditView, onDeleteView }: { +function ViewsSection({ views, selection, onSelect, collapsed, onToggle, onCreateView, onEditView, onDeleteView, entries }: { views: ViewFile[] selection: SidebarSelection onSelect: (sel: SidebarSelection) => void @@ -218,6 +222,7 @@ function ViewsSection({ views, selection, onSelect, collapsed, onToggle, onCreat onCreateView?: () => void onEditView?: (filename: string) => void onDeleteView?: (filename: string) => void + entries: VaultEntry[] }) { return (
@@ -240,6 +245,7 @@ function ViewsSection({ views, selection, onSelect, collapsed, onToggle, onCreat onSelect={() => onSelect({ kind: 'view', filename: v.filename })} onEditView={onEditView} onDeleteView={onDeleteView} + entries={entries} /> ))}
@@ -581,7 +587,7 @@ export const Sidebar = memo(function Sidebar({ {/* Views */} {hasViews && ( - toggleGroup('views')} onCreateView={onCreateView} onEditView={onEditView} onDeleteView={onDeleteView} /> + toggleGroup('views')} onCreateView={onCreateView} onEditView={onEditView} onDeleteView={onDeleteView} entries={entries} /> )} {/* Types */}