diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 480ebc3e..c975b2b8 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { NoteList } from './NoteList' -import { getSortComparator, filterEntries, countByFilter } from '../utils/noteListHelpers' +import { getSortComparator, filterEntries, countByFilter, countAllByFilter } from '../utils/noteListHelpers' import type { NoteListFilter } from '../utils/noteListHelpers' import type { VaultEntry, SidebarSelection } from '../types' @@ -1293,11 +1293,44 @@ describe('NoteList — filter pills', () => { expect(screen.getByTestId('filter-pill-trashed')).toBeInTheDocument() }) - it('does not show filter pills in All Notes view', () => { + it('shows filter pills in All Notes view', () => { render( ) - expect(screen.queryByTestId('filter-pills')).not.toBeInTheDocument() + expect(screen.getByTestId('filter-pills')).toBeInTheDocument() + expect(screen.getByTestId('filter-pill-open')).toBeInTheDocument() + expect(screen.getByTestId('filter-pill-archived')).toBeInTheDocument() + expect(screen.getByTestId('filter-pill-trashed')).toBeInTheDocument() + }) + + it('shows correct All Notes count badges across all types', () => { + render( + + ) + // projectEntries: 2 open Projects + 1 open Note = 3 open, 1 archived, 1 trashed + const openPill = screen.getByTestId('filter-pill-open') + const archivedPill = screen.getByTestId('filter-pill-archived') + const trashedPill = screen.getByTestId('filter-pill-trashed') + expect(openPill).toHaveTextContent('3') + expect(archivedPill).toHaveTextContent('1') + expect(trashedPill).toHaveTextContent('1') + }) + + it('shows archived notes in All Notes when filter is archived', () => { + render( + + ) + expect(screen.getByText('Archived Project')).toBeInTheDocument() + expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() + expect(screen.queryByText('Some Note')).not.toBeInTheDocument() + }) + + it('shows trashed notes in All Notes when filter is trashed', () => { + render( + + ) + expect(screen.getByText('Trashed Project')).toBeInTheDocument() + expect(screen.queryByText('Open Project 1')).not.toBeInTheDocument() }) it('shows correct count badges for each filter', () => { @@ -1377,4 +1410,33 @@ describe('NoteList — filterEntries with subFilter', () => { const result = filterEntries(entries, { kind: 'sectionGroup', type: 'Project' }) expect(result.map(e => e.title)).toEqual(['Active']) }) + + it('filters all notes by open sub-filter', () => { + const result = filterEntries(entries, { kind: 'filter', filter: 'all' }, 'open') + expect(result.map(e => e.title)).toEqual(['Active', 'Other']) + }) + + it('filters all notes by archived sub-filter', () => { + const result = filterEntries(entries, { kind: 'filter', filter: 'all' }, 'archived') + expect(result.map(e => e.title)).toEqual(['Archived']) + }) + + it('filters all notes by trashed sub-filter', () => { + const result = filterEntries(entries, { kind: 'filter', filter: 'all' }, 'trashed') + expect(result.map(e => e.title)).toEqual(['Trashed']) + }) +}) + +describe('countAllByFilter', () => { + it('counts all entries by filter status', () => { + const entries = [ + makeEntry({ path: '/1.md', isA: 'Project' }), + makeEntry({ path: '/2.md', isA: 'Note' }), + makeEntry({ path: '/3.md', isA: 'Project', archived: true }), + makeEntry({ path: '/4.md', isA: 'Note', trashed: true }), + makeEntry({ path: '/5.md', isA: 'Person', archived: true, trashed: true }), + ] + const counts = countAllByFilter(entries) + expect(counts).toEqual({ open: 2, archived: 1, trashed: 2 }) + }) }) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 73d791e8..e9cecfa8 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -1,7 +1,7 @@ import { useState, useMemo, useCallback, useEffect, memo } from 'react' import type { VaultEntry, SidebarSelection, ModifiedFile, NoteStatus, InboxPeriod } from '../types' import type { NoteListFilter } from '../utils/noteListHelpers' -import { countByFilter, countInboxByPeriod } from '../utils/noteListHelpers' +import { countByFilter, countAllByFilter, countInboxByPeriod } from '../utils/noteListHelpers' import { NoteItem } from './NoteItem' import { prefetchNoteContent } from '../hooks/useTabManagement' import { BulkActionBar } from './BulkActionBar' @@ -48,11 +48,13 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot const isSectionGroup = selection.kind === 'sectionGroup' const isInboxView = selection.kind === 'filter' && selection.filter === 'inbox' - const subFilter = isSectionGroup ? noteListFilter : undefined + const isAllNotesView = selection.kind === 'filter' && selection.filter === 'all' + const showFilterPills = isSectionGroup || isAllNotesView + const subFilter = showFilterPills ? noteListFilter : undefined const filterCounts = useMemo( - () => isSectionGroup ? countByFilter(entries, selection.type) : { open: 0, archived: 0, trashed: 0 }, - [entries, isSectionGroup, selection], + () => isSectionGroup ? countByFilter(entries, selection.type) : isAllNotesView ? countAllByFilter(entries) : { open: 0, archived: 0, trashed: 0 }, + [entries, isSectionGroup, isAllNotesView, selection], ) const inboxCounts = useMemo( @@ -75,7 +77,7 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot const noteListKeyboard = useNoteListKeyboard({ items: searched, selectedNotePath: selectedNote?.path ?? null, onOpen: onReplaceActiveTab, enabled: !isEntityView }) const multiSelect = useMultiSelect(searched, selectedNote?.path ?? null) - useEffect(() => { multiSelect.clear() }, [selection]) // eslint-disable-line react-hooks/exhaustive-deps -- clear on selection change only + useEffect(() => { multiSelect.clear() }, [selection, noteListFilter]) // eslint-disable-line react-hooks/exhaustive-deps -- clear on selection/filter change const handleClickNote = useCallback((entry: VaultEntry, e: React.MouseEvent) => { routeNoteClick(entry, e, { onReplace: onReplaceActiveTab, onSelect: onSelectNote, onOpenInNewWindow, multiSelect }) @@ -100,7 +102,7 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot return (
- {isSectionGroup && } + {showFilterPills && } {isInboxView && onInboxPeriodChange && }
diff --git a/src/utils/noteListHelpers.ts b/src/utils/noteListHelpers.ts index 23202b55..aeaa8c26 100644 --- a/src/utils/noteListHelpers.ts +++ b/src/utils/noteListHelpers.ts @@ -315,6 +315,7 @@ function filterByKind(entries: VaultEntry[], selection: SidebarSelection, subFil const typeEntries = entries.filter((e) => e.isA === selection.type) return subFilter ? applySubFilter(typeEntries, subFilter) : typeEntries.filter(isActive) } + if (selection.filter === 'all' && subFilter) return applySubFilter(entries, subFilter) return filterByFilterType(entries, selection.filter) } @@ -342,6 +343,17 @@ export function countByFilter(entries: VaultEntry[], type: string): Record { + let open = 0, archived = 0, trashed = 0 + for (const e of entries) { + if (e.trashed) trashed++ + else if (e.archived) archived++ + else open++ + } + return { open, archived, trashed } +} + // --- Inbox --- /** Build a set of all valid link targets (titles, aliases, filename stems, path stems). */