From ba511e40115d3cc74690902c9939a377e3866e0e Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 15 Feb 2026 10:05:19 +0100 Subject: [PATCH] Add count badges to filter pills and hide empty types Each type pill now shows its item count (e.g. "Notes 2"). Pills for types with zero items are hidden. The "All" pill shows the total count. Counts are computed from the search-filtered results so they update dynamically. Co-Authored-By: Claude Opus 4.6 --- src/components/NoteList.css | 10 ++++++ src/components/NoteList.test.tsx | 53 ++++++++++++++++++-------------- src/components/NoteList.tsx | 33 ++++++++++++++------ 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/components/NoteList.css b/src/components/NoteList.css index 457d405a..d44041c4 100644 --- a/src/components/NoteList.css +++ b/src/components/NoteList.css @@ -117,12 +117,22 @@ color: #ccc; } +.note-list__pill-count { + font-size: 10px; + opacity: 0.6; + margin-left: 2px; +} + .note-list__pill--active { background: #4a9eff22; color: #4a9eff; border-color: #4a9eff44; } +.note-list__pill--active .note-list__pill-count { + opacity: 0.8; +} + .note-list__items { flex: 1; overflow-y: auto; diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 9bb2e037..46ec5da3 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -81,24 +81,24 @@ const mockEntries: VaultEntry[] = [ describe('NoteList', () => { it('shows empty state when no entries', () => { - render() + const { container } = render() expect(screen.getByText('No notes found')).toBeInTheDocument() - expect(screen.getByText('0')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('0') }) it('renders all entries with All Notes filter', () => { - render() + const { container } = render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() - expect(screen.getByText('5')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('5') }) it('filters by People', () => { - render() + const { container } = render() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('filters by Events', () => { @@ -108,21 +108,21 @@ describe('NoteList', () => { }) it('filters by section group type', () => { - render() + const { container } = render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('shows entity pinned at top with children', () => { - render( + const { container } = render( ) // Pinned entity + child (Facebook Ads Strategy belongsTo this project) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() - expect(screen.getByText('2')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('2') }) it('filters by topic (relatedTo references)', () => { @@ -140,12 +140,12 @@ describe('NoteList', () => { }) it('filters by search query (case-insensitive substring)', () => { - render() + const { container } = render() const input = screen.getByPlaceholderText('Search notes...') fireEvent.change(input, { target: { value: 'facebook' } }) expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('sorts entries by last modified descending', () => { @@ -160,30 +160,37 @@ describe('NoteList', () => { expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest']) }) - it('renders type filter pills', () => { + it('renders type filter pills with counts and hides empty types', () => { const { container } = render() const pills = container.querySelectorAll('.note-list__pill') const labels = Array.from(pills).map((p) => p.textContent) - expect(labels).toContain('All') - expect(labels).toContain('Projects') - expect(labels).toContain('Notes') - expect(labels).toContain('Events') - expect(labels).toContain('People') + expect(labels).toContain('All 5') + expect(labels).toContain('Projects 1') + expect(labels).toContain('Notes 1') + expect(labels).toContain('Events 1') + expect(labels).toContain('People 1') + // Empty types should be hidden + expect(labels.some((l) => l?.startsWith('Experiments'))).toBe(false) + expect(labels.some((l) => l?.startsWith('Procedures'))).toBe(false) + expect(labels.some((l) => l?.startsWith('Responsibilities'))).toBe(false) }) it('filters by type pill', () => { - render() - fireEvent.click(screen.getByText('Projects')) + const { container } = render() + const projectsPill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('Projects'))! + fireEvent.click(projectsPill) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() expect(screen.queryByText('Facebook Ads Strategy')).not.toBeInTheDocument() }) it('clicking All pill resets type filter', () => { - render() - fireEvent.click(screen.getByText('People')) + const { container } = render() + const peoplePill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('People'))! + fireEvent.click(peoplePill) expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - fireEvent.click(screen.getByText('All')) + const allPill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('All'))! + fireEvent.click(allPill) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() }) }) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index c49e0602..2a36cb4d 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -130,6 +130,15 @@ export function NoteList({ entries, selection, selectedNote, allContent, onSelec ? sorted.filter((e) => e.title.toLowerCase().includes(query)) : sorted + // Compute per-type counts from the searched results (before type filter) + const typeCounts = new Map() + typeCounts.set(null, searched.length) // "All" count + for (const entry of searched) { + if (entry.isA) { + typeCounts.set(entry.isA, (typeCounts.get(entry.isA) ?? 0) + 1) + } + } + // Type filter pills const displayed = typeFilter ? searched.filter((e) => e.isA === typeFilter) @@ -156,15 +165,21 @@ export function NoteList({ entries, selection, selectedNote, allContent, onSelec />
- {TYPE_PILLS.map(({ label, type }) => ( - - ))} + {TYPE_PILLS.filter(({ type }) => { + const count = typeCounts.get(type) ?? 0 + return type === null || count > 0 + }).map(({ label, type }) => { + const count = typeCounts.get(type) ?? 0 + return ( + + ) + })}
{displayed.length === 0 ? (