feat(magic-notes): add rich table editing

Add native Quill table insertion and contextual row, column, and table actions. Preserve table-only drafts through composer validation, IPC, and storage, and document the saved behavior.\n\nRelease note: 魔法笔记支持插入和编辑表格,空单元格表格也可保存并在重新打开后恢复。
This commit is contained in:
mesalogo
2026-10-08 02:02:24 +08:00
parent 2a92f8f875
commit 3d063937c3
14 changed files with 699 additions and 11 deletions
@@ -118,6 +118,24 @@ describe('Magic note SQLite and filesystem storage', () => {
expect(database.getMagicNote(created.id)).toEqual(before)
})
it('persists and reopens a rich table whose cells contain no text', () => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Empty table' })
const table: MagicNoteContent = {
version: 1,
ops: [{ insert: '\n', attributes: { table: 'row-1' } }]
}
const saved = database.createMagicNoteEntry({ noteId: note.id, content: table, plainText: 'ignored' })
expect(saved.entries.at(-1)?.content).toEqual(table)
database.close()
const reopened = new AssistantDatabase(join(directory, 'assistant.sqlite'))
connections.push(reopened)
reopened.initialize(directory)
expect(reopened.getMagicNote(note.id).entries.at(-1)?.content).toEqual(table)
})
it.each(['missing', 'invalid-json', 'invalid-media'] as const)('retains all assets when a later retained body has %s corruption', (failure) => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Retained bodies', content: rich })
@@ -16,6 +16,21 @@ function fixture(database: Partial<AssistantStoragePort>) {
}
describe('async magic notes IPC', () => {
it('passes an empty-cell table through schema and content validation', async () => {
const createMagicNoteEntry = vi.fn<AssistantStoragePort['createMagicNoteEntry']>()
createMagicNoteEntry.mockResolvedValue({
id: 'note', title: 'Note', preview: '', entryCount: 1, pinned: false, tags: [],
revision: 1, createdAt: '', updatedAt: '', createdEntryId: 'entry', entries: []
})
const f = fixture({ createMagicNoteEntry })
const content = { version: 1 as const, ops: [{ insert: '\n', attributes: { table: 'row-1' } }] }
await f.invoke(ipcChannels.magicNotesCreateEntry, {
noteId: '00000000-0000-4000-8000-000000000001', content
})
expect(createMagicNoteEntry).toHaveBeenCalledWith(expect.objectContaining({ content, plainText: '' }))
})
it('resolves list DTOs and preserves sender/schema checks before storage', async () => {
const listMagicNotes = vi.fn(async () => [])
const createMagicNote = vi.fn()