feat: status property — Notion-style dropdown with color chips (#97)
* fix: resolve search panel freeze by making search_vault async The search_vault Tauri command was synchronous (fn, not async fn), blocking the main thread for 30+ seconds during hybrid/semantic search on large vaults (9200+ files). This caused the macOS beachball. Changes: - Make search_vault async with tokio::spawn_blocking (runs qmd off main thread) - Cache collection name per vault path (avoid repeated qmd collection list calls) - Cancel inflight searches and debounce timers when panel closes - Add regression test for stale result cancellation on panel close Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * design: add status property dropdown wireframes Three frames: closed pill state, open dropdown with suggested/vault statuses, and custom status creation flow. Also bump flaky NoteList 9000-entry test timeout from 15s to 30s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace status text edit with Notion-style dropdown - Extract STATUS_STYLES to shared statusStyles.ts utility - New StatusDropdown component: filterable popover with colored pills - StatusPill reusable component for consistent status chip rendering - Vault-wide status aggregation from entries prop - Dropdown shows "From vault" and "Suggested" sections - Custom status creation via type-and-Enter - Escape/backdrop click cancels without saving - Keyboard navigation (ArrowUp/Down + Enter) - 22 new tests covering dropdown behavior + integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: cargo fmt --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -350,7 +350,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
expect(screen.getByText('Modified')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles editing status', () => {
|
||||
it('opens status dropdown on click and selects a status', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
@@ -359,12 +359,13 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
// Click status pill to start editing (rendered with CSS uppercase, DOM text is "Active")
|
||||
// Click status pill to open dropdown
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
// Should show edit input
|
||||
const input = screen.getByDisplayValue('Active')
|
||||
fireEvent.change(input, { target: { value: 'Done' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
// Should show dropdown with search input
|
||||
expect(screen.getByTestId('status-dropdown')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-search-input')).toBeInTheDocument()
|
||||
// Click on "Done" option in the suggested list
|
||||
fireEvent.click(screen.getByTestId('status-option-Done'))
|
||||
expect(onUpdateProperty).toHaveBeenCalledWith('Status', 'Done')
|
||||
})
|
||||
|
||||
@@ -742,6 +743,74 @@ describe('DynamicPropertiesPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('status dropdown interaction', () => {
|
||||
it('closes dropdown on Escape without saving', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{ Status: 'Active' }}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
expect(screen.getByTestId('status-dropdown')).toBeInTheDocument()
|
||||
fireEvent.keyDown(screen.getByTestId('status-search-input'), { key: 'Escape' })
|
||||
expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument()
|
||||
expect(onUpdateProperty).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes dropdown on backdrop click without saving', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{ Status: 'Active' }}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-dropdown-backdrop'))
|
||||
expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument()
|
||||
expect(onUpdateProperty).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates custom status by typing and pressing Enter', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{ Status: 'Active' }}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
const input = screen.getByTestId('status-search-input')
|
||||
fireEvent.change(input, { target: { value: 'Needs Review' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
expect(onUpdateProperty).toHaveBeenCalledWith('Status', 'Needs Review')
|
||||
})
|
||||
|
||||
it('shows vault statuses from entries', () => {
|
||||
const entriesWithStatuses = [
|
||||
makeEntry({ path: '/vault/a.md', status: 'Reviewing' }),
|
||||
makeEntry({ path: '/vault/b.md', status: 'Shipped' }),
|
||||
]
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{ Status: 'Active' }}
|
||||
entries={entriesWithStatuses}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
expect(screen.getByTestId('status-option-Reviewing')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-option-Shipped')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('smart property display — boolean', () => {
|
||||
it('renders boolean toggle for true values', () => {
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user