Compare commits

...

1 Commits

Author SHA1 Message Date
Test
c6fa1f48cb feat: add filter pills (Open/Archived/Trashed) to All Notes view
Reuses the existing FilterPills component and sub-filter mechanism from
sectionGroup views. Adds countAllByFilter helper for counting across all
entry types and extends filterByKind to support subFilter for the 'all'
filter. Bulk actions automatically adapt based on active pill. Switching
pills resets multi-selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 23:03:17 +01:00
3 changed files with 85 additions and 9 deletions

View File

@@ -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(
<NoteList {...defaultFilterProps} entries={projectEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
)
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(
<NoteList {...defaultFilterProps} entries={projectEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
)
// 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(
<NoteList noteListFilter="archived" onNoteListFilterChange={noopFilterChange} entries={projectEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
)
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(
<NoteList noteListFilter="trashed" onNoteListFilterChange={noopFilterChange} entries={projectEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />
)
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 })
})
})

View File

@@ -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 (
<div className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
<NoteListHeader title={title} typeDocument={typeDocument} isEntityView={isEntityView} isTrashView={isTrashView} trashCount={searched.length} listSort={listSort} listDirection={listDirection} customProperties={customProperties} sidebarCollapsed={sidebarCollapsed} searchVisible={searchVisible} search={search} onSortChange={handleSortChange} onCreateNote={onCreateNote} onOpenType={onReplaceActiveTab} onToggleSearch={toggleSearch} onSearchChange={setSearch} onEmptyTrash={onEmptyTrash} />
{isSectionGroup && <FilterPills active={noteListFilter} counts={filterCounts} onChange={onNoteListFilterChange} />}
{showFilterPills && <FilterPills active={noteListFilter} counts={filterCounts} onChange={onNoteListFilterChange} />}
{isInboxView && onInboxPeriodChange && <InboxFilterPills active={inboxPeriod} counts={inboxCounts} onChange={onInboxPeriodChange} />}
<div className="flex flex-1 flex-col overflow-hidden outline-none" style={{ minHeight: 0 }} tabIndex={0} onKeyDown={noteListKeyboard.handleKeyDown} onFocus={noteListKeyboard.handleFocus} data-testid="note-list-container">
<div className="flex-1 overflow-hidden" style={{ minHeight: 0 }}>

View File

@@ -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<NoteL
return { open, archived, trashed }
}
/** Count notes per sub-filter across all entries (no type filter). */
export function countAllByFilter(entries: VaultEntry[]): Record<NoteListFilter, number> {
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). */