Add create note dialog with type selector (M5 Task 1)
This commit is contained in:
57
e2e/create-note.spec.ts
Normal file
57
e2e/create-note.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('clicking + button opens create note dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click the + button
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Dialog should be visible
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note')
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-dialog.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('create a new note via dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open dialog
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Type a title
|
||||
await page.fill('.create-dialog__input', 'My Test Note')
|
||||
|
||||
// Select "Project" type
|
||||
await page.click('.create-dialog__type-btn:text("Project")')
|
||||
|
||||
// Click Create
|
||||
await page.click('.create-dialog__btn--create')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('.create-dialog')).not.toBeVisible()
|
||||
|
||||
// New note should appear in the list and be opened in editor
|
||||
await expect(page.locator('.note-list__item:has-text("My Test Note")').first()).toBeVisible()
|
||||
await expect(page.locator('.editor__tab--active:has-text("My Test Note")')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-note-result.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('escape closes create dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.create-dialog')).not.toBeVisible()
|
||||
})
|
||||
65
src/App.tsx
65
src/App.tsx
@@ -5,7 +5,8 @@ import { NoteList } from './components/NoteList'
|
||||
import { Editor } from './components/Editor'
|
||||
import { Inspector } from './components/Inspector'
|
||||
import { ResizeHandle } from './components/ResizeHandle'
|
||||
import { isTauri, mockInvoke } from './mock-tauri'
|
||||
import { CreateNoteDialog, type NoteType } from './components/CreateNoteDialog'
|
||||
import { isTauri, mockInvoke, addMockEntry } from './mock-tauri'
|
||||
import type { VaultEntry, SidebarSelection, GitCommit } from './types'
|
||||
import './App.css'
|
||||
|
||||
@@ -25,6 +26,7 @@ function App() {
|
||||
const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
|
||||
const [allContent, setAllContent] = useState<Record<string, string>>({})
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadVault = async () => {
|
||||
@@ -152,6 +154,60 @@ function App() {
|
||||
}
|
||||
}, [entries, handleSelectNote])
|
||||
|
||||
const handleCreateNote = useCallback(async (title: string, type: NoteType) => {
|
||||
// Build file path: type determines folder
|
||||
const typeToFolder: Record<string, string> = {
|
||||
Note: 'note',
|
||||
Project: 'project',
|
||||
Experiment: 'experiment',
|
||||
Responsibility: 'responsibility',
|
||||
Procedure: 'procedure',
|
||||
Person: 'person',
|
||||
Event: 'event',
|
||||
Topic: 'topic',
|
||||
}
|
||||
const folder = typeToFolder[type] || 'note'
|
||||
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
const path = `/Users/luca/Laputa/${folder}/${slug}.md`
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
const newEntry: VaultEntry = {
|
||||
path,
|
||||
filename: `${slug}.md`,
|
||||
title,
|
||||
isA: type,
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: type === 'Topic' || type === 'Person' ? null : 'Active',
|
||||
owner: null,
|
||||
cadence: null,
|
||||
modifiedAt: now,
|
||||
fileSize: 0,
|
||||
}
|
||||
|
||||
const frontmatter = [
|
||||
'---',
|
||||
`title: ${title}`,
|
||||
`is_a: ${type}`,
|
||||
...(newEntry.status ? [`status: ${newEntry.status}`] : []),
|
||||
'---',
|
||||
].join('\n')
|
||||
const content = `${frontmatter}\n\n# ${title}\n\n`
|
||||
|
||||
if (isTauri()) {
|
||||
// TODO: Add Tauri command for creating notes
|
||||
} else {
|
||||
addMockEntry(newEntry, content)
|
||||
}
|
||||
|
||||
setEntries((prev) => [newEntry, ...prev])
|
||||
setAllContent((prev) => ({ ...prev, [path]: content }))
|
||||
|
||||
// Open the new note
|
||||
handleSelectNote(newEntry)
|
||||
}, [handleSelectNote])
|
||||
|
||||
const handleSidebarResize = useCallback((delta: number) => {
|
||||
setSidebarWidth((w) => Math.max(150, Math.min(400, w + delta)))
|
||||
}, [])
|
||||
@@ -174,7 +230,7 @@ function App() {
|
||||
</div>
|
||||
<ResizeHandle onResize={handleSidebarResize} />
|
||||
<div className="app__note-list" style={{ width: noteListWidth }}>
|
||||
<NoteList entries={entries} selection={selection} selectedNote={activeTab?.entry ?? null} onSelectNote={handleSelectNote} />
|
||||
<NoteList entries={entries} selection={selection} selectedNote={activeTab?.entry ?? null} onSelectNote={handleSelectNote} onCreateNote={() => setShowCreateDialog(true)} />
|
||||
</div>
|
||||
<ResizeHandle onResize={handleNoteListResize} />
|
||||
<div className="app__editor">
|
||||
@@ -202,6 +258,11 @@ function App() {
|
||||
onNavigate={handleNavigateWikilink}
|
||||
/>
|
||||
</div>
|
||||
<CreateNoteDialog
|
||||
open={showCreateDialog}
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onCreate={handleCreateNote}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
131
src/components/CreateNoteDialog.css
Normal file
131
src/components/CreateNoteDialog.css
Normal file
@@ -0,0 +1,131 @@
|
||||
.create-dialog__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.create-dialog {
|
||||
background: #1e1e3a;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.create-dialog__title {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.create-dialog__field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.create-dialog__label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.create-dialog__input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #16162a;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.create-dialog__input:focus {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.create-dialog__input::placeholder {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.create-dialog__types {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.create-dialog__type-btn {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.create-dialog__type-btn:hover {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.create-dialog__type-btn--active {
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.create-dialog__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.create-dialog__btn {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.create-dialog__btn--cancel {
|
||||
background: transparent;
|
||||
color: #888;
|
||||
border: 1px solid #2a2a4a;
|
||||
}
|
||||
|
||||
.create-dialog__btn--cancel:hover {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.create-dialog__btn--create {
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.create-dialog__btn--create:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.create-dialog__btn--create:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
100
src/components/CreateNoteDialog.tsx
Normal file
100
src/components/CreateNoteDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import './CreateNoteDialog.css'
|
||||
|
||||
const NOTE_TYPES = [
|
||||
'Note',
|
||||
'Project',
|
||||
'Experiment',
|
||||
'Responsibility',
|
||||
'Procedure',
|
||||
'Person',
|
||||
'Event',
|
||||
'Topic',
|
||||
] as const
|
||||
|
||||
export type NoteType = (typeof NOTE_TYPES)[number]
|
||||
|
||||
interface CreateNoteDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onCreate: (title: string, type: NoteType) => void
|
||||
}
|
||||
|
||||
export function CreateNoteDialog({ open, onClose, onCreate }: CreateNoteDialogProps) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [type, setType] = useState<NoteType>('Note')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTitle('')
|
||||
setType('Note')
|
||||
// Focus input after render
|
||||
setTimeout(() => inputRef.current?.focus(), 50)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
window.addEventListener('keydown', handleKey)
|
||||
return () => window.removeEventListener('keydown', handleKey)
|
||||
}, [open, onClose])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const trimmed = title.trim()
|
||||
if (!trimmed) return
|
||||
onCreate(trimmed, type)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="create-dialog__overlay" onClick={onClose}>
|
||||
<div className="create-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="create-dialog__title">Create New Note</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="create-dialog__field">
|
||||
<label className="create-dialog__label">Title</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="create-dialog__input"
|
||||
type="text"
|
||||
placeholder="Enter note title..."
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="create-dialog__field">
|
||||
<label className="create-dialog__label">Type</label>
|
||||
<div className="create-dialog__types">
|
||||
{NOTE_TYPES.map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
className={`create-dialog__type-btn${type === t ? ' create-dialog__type-btn--active' : ''}`}
|
||||
onClick={() => setType(t)}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="create-dialog__actions">
|
||||
<button type="button" className="create-dialog__btn create-dialog__btn--cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="create-dialog__btn create-dialog__btn--create" disabled={!title.trim()}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,12 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.note-list__header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.note-list__count {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
@@ -29,6 +35,28 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.note-list__add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #2a2a4a;
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.note-list__add-btn:hover {
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.note-list__search {
|
||||
padding: 8px 12px;
|
||||
|
||||
@@ -81,13 +81,13 @@ const mockEntries: VaultEntry[] = [
|
||||
|
||||
describe('NoteList', () => {
|
||||
it('shows empty state when no entries', () => {
|
||||
render(<NoteList entries={[]} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={[]} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByText('No notes found')).toBeInTheDocument()
|
||||
expect(screen.getByText('0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders all entries with All Notes filter', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
|
||||
@@ -95,20 +95,20 @@ describe('NoteList', () => {
|
||||
})
|
||||
|
||||
it('filters by People', () => {
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'filter', filter: 'people' }} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'filter', filter: 'people' }} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters by Events', () => {
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'filter', filter: 'events' }} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'filter', filter: 'events' }} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByText('Kickoff Meeting')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters by section group type', () => {
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'sectionGroup', type: 'Project' }} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={{ kind: 'sectionGroup', type: 'Project' }} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
@@ -116,7 +116,7 @@ describe('NoteList', () => {
|
||||
|
||||
it('shows entity pinned at top with children', () => {
|
||||
render(
|
||||
<NoteList entries={mockEntries} selection={{ kind: 'entity', entry: mockEntries[0] }} selectedNote={null} onSelectNote={noopSelect} />
|
||||
<NoteList entries={mockEntries} selection={{ kind: 'entity', entry: mockEntries[0] }} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />
|
||||
)
|
||||
// Pinned entity + child (Facebook Ads Strategy belongsTo this project)
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
@@ -127,7 +127,7 @@ describe('NoteList', () => {
|
||||
|
||||
it('filters by topic (relatedTo references)', () => {
|
||||
render(
|
||||
<NoteList entries={mockEntries} selection={{ kind: 'topic', entry: mockEntries[4] }} selectedNote={null} onSelectNote={noopSelect} />
|
||||
<NoteList entries={mockEntries} selection={{ kind: 'topic', entry: mockEntries[4] }} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />
|
||||
)
|
||||
// Build Laputa App has relatedTo: [[topic/software-development]]
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
@@ -135,12 +135,12 @@ describe('NoteList', () => {
|
||||
})
|
||||
|
||||
it('renders a search bar', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
expect(screen.getByPlaceholderText('Search notes...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters by search query (case-insensitive substring)', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
const input = screen.getByPlaceholderText('Search notes...')
|
||||
fireEvent.change(input, { target: { value: 'facebook' } })
|
||||
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
||||
@@ -154,14 +154,14 @@ describe('NoteList', () => {
|
||||
{ ...mockEntries[1], modifiedAt: 3000, title: 'Newest', path: '/p2' },
|
||||
{ ...mockEntries[2], modifiedAt: 2000, title: 'Middle', path: '/p3' },
|
||||
]
|
||||
render(<NoteList entries={entriesWithDifferentDates} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={entriesWithDifferentDates} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
const titles = screen.getAllByText(/Oldest|Newest|Middle/)
|
||||
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} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
const { container } = render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
const pills = container.querySelectorAll('.note-list__pill')
|
||||
const labels = Array.from(pills).map((p) => p.textContent)
|
||||
expect(labels).toContain('All')
|
||||
@@ -172,7 +172,7 @@ describe('NoteList', () => {
|
||||
})
|
||||
|
||||
it('filters by type pill', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
fireEvent.click(screen.getByText('Projects'))
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument()
|
||||
@@ -180,7 +180,7 @@ describe('NoteList', () => {
|
||||
})
|
||||
|
||||
it('clicking All pill resets type filter', () => {
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} />)
|
||||
render(<NoteList entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onCreateNote={vi.fn()} />)
|
||||
fireEvent.click(screen.getByText('People'))
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByText('All'))
|
||||
|
||||
@@ -7,6 +7,7 @@ interface NoteListProps {
|
||||
selection: SidebarSelection
|
||||
selectedNote: VaultEntry | null
|
||||
onSelectNote: (entry: VaultEntry) => void
|
||||
onCreateNote: () => void
|
||||
}
|
||||
|
||||
/** Check if a wikilink array (e.g. belongsTo) references a given entry by path stem */
|
||||
@@ -68,7 +69,7 @@ const TYPE_PILLS = [
|
||||
{ label: 'Responsibilities', type: 'Responsibility' },
|
||||
] as const
|
||||
|
||||
export function NoteList({ entries, selection, selectedNote, onSelectNote }: NoteListProps) {
|
||||
export function NoteList({ entries, selection, selectedNote, onSelectNote, onCreateNote }: NoteListProps) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [typeFilter, setTypeFilter] = useState<string | null>(null)
|
||||
|
||||
@@ -98,7 +99,12 @@ export function NoteList({ entries, selection, selectedNote, onSelectNote }: Not
|
||||
<div className="note-list">
|
||||
<div className="note-list__header">
|
||||
<h3>Notes</h3>
|
||||
<span className="note-list__count">{displayed.length}</span>
|
||||
<div className="note-list__header-right">
|
||||
<span className="note-list__count">{displayed.length}</span>
|
||||
<button className="note-list__add-btn" onClick={onCreateNote} title="Create new note">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="note-list__search">
|
||||
<input
|
||||
|
||||
@@ -532,6 +532,11 @@ export function isTauri(): boolean {
|
||||
return typeof window !== 'undefined' && '__TAURI__' in window
|
||||
}
|
||||
|
||||
/** Register content for a new entry in mock mode (for get_note_content calls) */
|
||||
export function addMockEntry(_entry: VaultEntry, content: string) {
|
||||
MOCK_CONTENT[_entry.path] = content
|
||||
}
|
||||
|
||||
export async function mockInvoke<T>(cmd: string, args?: any): Promise<T> {
|
||||
const handler = mockHandlers[cmd]
|
||||
if (handler) {
|
||||
|
||||
Reference in New Issue
Block a user