feat: add magic note tags and a two-pane notes layout
Keep the notes list beside the open note, compact the detail view with a full-width sticky toolbar, and make Back to overview return to a full-width all-notes grid. Add manual note tags with AND filtering, a tag manager for rename, merge and delete, schema 51 tag tables, and tag support in the note Agent tools.
This commit is contained in:
@@ -44,7 +44,7 @@ async function notesAnalysisRegression() {
|
||||
}
|
||||
throw new Error(`Timed out: ${predicate}`)
|
||||
}
|
||||
const button = (label: string, parent: ParentNode = host) => [...parent.querySelectorAll<HTMLButtonElement>('button')].find(node => node.textContent?.trim() === label)!
|
||||
const button = (label: string, parent: ParentNode = host) => [...parent.querySelectorAll<HTMLButtonElement>('button')].find(node => (node.getAttribute('aria-label') ?? node.textContent?.trim()) === label)!
|
||||
const click = async (label: string, parent: ParentNode = host) => {
|
||||
await wait(() => button(label, parent) && !button(label, parent).disabled)
|
||||
button(label, parent).click()
|
||||
@@ -77,7 +77,6 @@ async function notesAnalysisRegression() {
|
||||
await click('Save changes')
|
||||
await wait(async () => (await entry()).revision >= manualRevision + 2)
|
||||
const editedRevision = (await entry()).revision
|
||||
await click('Cancel')
|
||||
await wait(() => host.querySelector('.magic-note-composer'))
|
||||
await click('Canvas', host.querySelector('.magic-note-composer')!)
|
||||
const composer = () => host.querySelector<HTMLElement>('.magic-note-composer')!
|
||||
|
||||
@@ -8,14 +8,24 @@ import '../../src/renderer/src/magic-canvas.css'
|
||||
await i18n.changeLanguage('en-US')
|
||||
const note = {
|
||||
id: 'layout-note', title: 'Layout note', preview: 'Record preview', entryCount: 1,
|
||||
pinned: false, revision: 1, createdAt: '2026-09-19T00:00:00Z', updatedAt: '2026-09-19T00:00:00Z',
|
||||
pinned: false, tags: ['Work', 'Release planning', 'Q4', 'Long tag name here'], revision: 1, createdAt: '2026-09-19T00:00:00Z', updatedAt: '2026-09-19T00:00:00Z',
|
||||
entries: [{ id: 'layout-entry', noteId: 'layout-note', revision: 1,
|
||||
createdAt: '2026-09-19T00:00:00Z', updatedAt: '2026-09-19T00:00:00Z',
|
||||
content: { version: 1, ops: [{ insert: 'Record preview\n' }] }, plainText: 'Record preview', comments: [] }]
|
||||
content: { version: 1, ops: [{ insert: 'Record preview\n'.repeat(60) }] }, plainText: 'Record preview', comments: [] }]
|
||||
}
|
||||
const notes = [note, ...Array.from({ length: 24 }, (_, index) => ({
|
||||
...note, id: `other-note-${index}`, title: `Other note ${index}`, tags: index % 3 === 0 ? ['Work'] : [],
|
||||
entries: note.entries.map(entry => ({ ...entry, id: `other-entry-${index}`, noteId: `other-note-${index}` }))
|
||||
}))]
|
||||
const tags = [
|
||||
{ id: 'tag-work', name: 'Work', noteCount: 9 },
|
||||
{ id: 'tag-release', name: 'Release planning', noteCount: 1 },
|
||||
{ id: 'tag-q4', name: 'Q4', noteCount: 1 },
|
||||
{ id: 'tag-long', name: 'Long tag name here', noteCount: 1 }
|
||||
]
|
||||
window.goodbuddy = {
|
||||
magicNotes: {
|
||||
list: async () => ({ notes: [note] }), get: async () => note, listTodos: async () => ({ todos: [{
|
||||
list: async () => ({ notes, tags }), get: async (id: string) => notes.find(note => note.id === id), listTodos: async () => ({ todos: [{
|
||||
id: 'layout-todo', noteId: note.id, noteTitle: note.title, entryId: 'layout-entry',
|
||||
title: 'Layout task', instructions: 'Task details', completed: false, revision: 1,
|
||||
comments: [], createdAt: note.createdAt, updatedAt: note.updatedAt
|
||||
|
||||
@@ -49,14 +49,14 @@ async function notesNavigationRegression() {
|
||||
}
|
||||
throw new Error('Timed out waiting for notes UI')
|
||||
}
|
||||
const button = (text: string) => [...host.querySelectorAll<HTMLButtonElement>('button')].find(item => item.textContent?.trim() === text)!
|
||||
const button = (text: string) => [...host.querySelectorAll<HTMLButtonElement>('button')].find(item => (item.getAttribute('aria-label') ?? item.textContent?.trim()) === text)!
|
||||
const frame = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
await wait(() => host.querySelector('[id^="magic-note-select-"]'))
|
||||
host.querySelector<HTMLButtonElement>('[id^="magic-note-select-"]')!.click()
|
||||
await wait(() => button('Analyze with AI'))
|
||||
button('Analyze with AI').click()
|
||||
await wait(() => finishAnalysis)
|
||||
const title = host.querySelector<HTMLInputElement>('.magic-note-detail-header input')!
|
||||
const title = host.querySelector<HTMLInputElement>('.magic-note-detail-title')!
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(title, 'Title during AI')
|
||||
title.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await frame()
|
||||
|
||||
Reference in New Issue
Block a user