Add type filter pills to note list
Pill bar below search: All | Projects | Notes | Events | People | Experiments | Procedures | Responsibilities. Clicking a pill filters the displayed entries by isA type. "All" resets the filter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,3 +64,19 @@ test('search bar filters by title substring', async ({ page }) => {
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/search-budget.png' })
|
||||
})
|
||||
|
||||
test('type filter pills narrow results', async ({ page }) => {
|
||||
// Click "Projects" pill
|
||||
await page.locator('.note-list__pill', { hasText: 'Projects' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).not.toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/pill-projects.png' })
|
||||
|
||||
// Click "All" to reset
|
||||
await page.locator('.note-list__pill', { hasText: 'All' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await expect(count).toHaveText('12')
|
||||
})
|
||||
|
||||
@@ -55,6 +55,37 @@
|
||||
border-color: #4a4a7a;
|
||||
}
|
||||
|
||||
/* Type filter pills */
|
||||
.note-list__pills {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #2a2a4a;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.note-list__pill {
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-list__pill:hover {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.note-list__pill--active {
|
||||
background: #3a3a5a;
|
||||
color: #ffffff;
|
||||
border-color: #4a4a7a;
|
||||
}
|
||||
|
||||
.note-list__items {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -160,4 +160,31 @@ describe('NoteList', () => {
|
||||
const titleTexts = titles.map((el) => el.textContent)
|
||||
expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest'])
|
||||
})
|
||||
|
||||
it('renders type filter pills', () => {
|
||||
const { container } = render(<NoteList entries={mockEntries} selection={allSelection} />)
|
||||
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')
|
||||
})
|
||||
|
||||
it('filters by type pill', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} />)
|
||||
fireEvent.click(screen.getByText('Projects'))
|
||||
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(<NoteList entries={mockEntries} selection={allSelection} />)
|
||||
fireEvent.click(screen.getByText('People'))
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByText('All'))
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,8 +55,20 @@ function sortByModified(a: VaultEntry, b: VaultEntry): number {
|
||||
return (b.modifiedAt ?? 0) - (a.modifiedAt ?? 0)
|
||||
}
|
||||
|
||||
const TYPE_PILLS = [
|
||||
{ label: 'All', type: null },
|
||||
{ label: 'Projects', type: 'Project' },
|
||||
{ label: 'Notes', type: 'Note' },
|
||||
{ label: 'Events', type: 'Event' },
|
||||
{ label: 'People', type: 'Person' },
|
||||
{ label: 'Experiments', type: 'Experiment' },
|
||||
{ label: 'Procedures', type: 'Procedure' },
|
||||
{ label: 'Responsibilities', type: 'Responsibility' },
|
||||
] as const
|
||||
|
||||
export function NoteList({ entries, selection }: NoteListProps) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [typeFilter, setTypeFilter] = useState<string | null>(null)
|
||||
|
||||
const filtered = filterEntries(entries, selection)
|
||||
|
||||
@@ -71,10 +83,15 @@ export function NoteList({ entries, selection }: NoteListProps) {
|
||||
|
||||
// Search filter (title substring, case-insensitive)
|
||||
const query = search.trim().toLowerCase()
|
||||
const displayed = query
|
||||
const searched = query
|
||||
? sorted.filter((e) => e.title.toLowerCase().includes(query))
|
||||
: sorted
|
||||
|
||||
// Type filter pills
|
||||
const displayed = typeFilter
|
||||
? searched.filter((e) => e.isA === typeFilter)
|
||||
: searched
|
||||
|
||||
return (
|
||||
<div className="note-list">
|
||||
<div className="note-list__header">
|
||||
@@ -90,6 +107,17 @@ export function NoteList({ entries, selection }: NoteListProps) {
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="note-list__pills">
|
||||
{TYPE_PILLS.map(({ label, type }) => (
|
||||
<button
|
||||
key={label}
|
||||
className={`note-list__pill${typeFilter === type ? ' note-list__pill--active' : ''}`}
|
||||
onClick={() => setTypeFilter(type)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="note-list__items">
|
||||
{displayed.length === 0 ? (
|
||||
<div className="note-list__empty">No notes found</div>
|
||||
|
||||
Reference in New Issue
Block a user