feat(magic-notes): improve tag management UI

This commit is contained in:
mesalogo
2026-10-07 21:05:14 +08:00
parent e0607a153f
commit 13c29badfb
14 changed files with 160 additions and 25 deletions
+13 -9
View File
@@ -7805,8 +7805,8 @@ describe('AssistantDatabase', () => {
const retagged = database.updateMagicNote({ noteId: work.id, tags: ['Work'], expectedRevision: work.revision })
expect(retagged.tags).toEqual(['Work'])
expect(retagged.revision).toBe(work.revision + 1)
// "Project A" lost its last note and is removed.
expect(database.listMagicNoteTags().map((tag) => tag.name)).not.toContain('Project A')
// Tags remain available after their last note link is removed.
expect(database.listMagicNoteTags().map((tag) => tag.name)).toContain('Project A')
expect(() => database.updateMagicNote({ noteId: work.id, tags: [], expectedRevision: work.revision })).toThrow()
expect(database.getMagicNote(work.id).tags).toEqual(['Work'])
@@ -7820,19 +7820,23 @@ describe('AssistantDatabase', () => {
const merged = database.renameMagicNoteTag({ tagId: workTag.id, name: 'Personal' })
expect(merged).toMatchObject({ merged: true, tag: { name: 'personal', noteCount: 3 } })
expect(database.getMagicNote(both.id).tags).toEqual(['personal'])
expect(database.listMagicNoteTags()).toHaveLength(1)
expect(database.listMagicNoteTags().map((tag) => tag.name)).toEqual(['personal', 'Project A'])
const calls = onMagicNotesChanged.mock.calls.length
database.deleteMagicNoteTag(merged.tag.id)
expect(onMagicNotesChanged).toHaveBeenCalledTimes(calls + 1)
expect(database.listMagicNoteTags()).toEqual([])
expect(database.getMagicNote(personal.id)).toMatchObject({ title: 'Personal', tags: [] })
expect(() => database.deleteMagicNoteTag(merged.tag.id)).toThrow('标签不存在')
expect(() => database.renameMagicNoteTag({ tagId: merged.tag.id, name: 'x' })).toThrow('标签不存在')
expect(database.listMagicNoteTags().map((tag) => tag.name)).toEqual(['Project A'])
expect(database.getMagicNote(personal.id)).toMatchObject({ title: 'Personal', tags: [] })
expect(() => database.deleteMagicNoteTag(merged.tag.id)).toThrow('标签不存在')
expect(() => database.renameMagicNoteTag({ tagId: merged.tag.id, name: 'x' })).toThrow('标签不存在')
database.updateMagicNote({ noteId: personal.id, tags: ['Solo'], expectedRevision: database.getMagicNote(personal.id).revision })
const empty = database.createMagicNoteTag({ name: 'Ideas' })
expect(empty).toEqual(expect.objectContaining({ name: 'Ideas', noteCount: 0 }))
expect(database.listMagicNoteTags().map((tag) => tag.name)).toContain('Ideas')
database.updateMagicNote({ noteId: personal.id, tags: ['Solo'], expectedRevision: database.getMagicNote(personal.id).revision })
database.deleteMagicNote(personal.id)
expect(database.listMagicNoteTags()).toEqual([])
expect(database.listMagicNoteTags().map((tag) => tag.name)).toEqual(['Ideas', 'Project A', 'Solo'])
} finally {
database.close()
}
+18 -10
View File
@@ -4169,13 +4169,30 @@ export class AssistantDatabase {
FROM magic_note_tags t
LEFT JOIN magic_note_tag_links l ON l.tag_id = t.id
GROUP BY t.id
HAVING note_count > 0
ORDER BY note_count DESC, t.name_key ASC`
)
.all() as MagicNoteTagRow[]
return rows.map(toMagicNoteTag)
}
createMagicNoteTag(input: { name: string }): MagicNoteTag {
const database = this.requireDatabase()
const now = new Date().toISOString()
const id = randomUUID()
const key = magicNoteTagKey(input.name)
try {
database.prepare(
`INSERT INTO magic_note_tags (id, name, name_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
).run(id, input.name, key, now, now)
} catch (error) {
if (error instanceof Error && error.message.includes('UNIQUE')) throw new Error('标签已存在')
throw error
}
this.options.onMagicNotesChanged?.()
return { id, name: input.name, noteCount: 0 }
}
/**
* Renames a tag across every note. When the new name matches another tag,
* the two merge: links move to the existing tag and the old tag is removed.
@@ -4286,14 +4303,6 @@ export class AssistantDatabase {
}
insertLink.run(noteId, tag.id, position, now)
})
this.deleteOrphanMagicNoteTags(database)
}
private deleteOrphanMagicNoteTags(database: DatabaseSync): void {
database.exec(
`DELETE FROM magic_note_tags
WHERE NOT EXISTS (SELECT 1 FROM magic_note_tag_links l WHERE l.tag_id = magic_note_tags.id)`
)
}
getMagicNote(noteId: string): MagicNoteDetail {
@@ -4474,7 +4483,6 @@ export class AssistantDatabase {
if (result.changes !== 1) {
throw new Error('笔记不存在')
}
this.deleteOrphanMagicNoteTags(database)
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
+1 -1
View File
@@ -20,7 +20,7 @@ export const assistantStorageMethods = [
'deleteLocalConversation', 'getOrCreateRemoteConversation', 'appendConversationMessage',
'appendRemoteConversationMessage', 'claimChannelEvent', 'releaseChannelEvent',
'enqueueChannelResult', 'markChannelResult', 'listUndeliveredChannelResults',
'listMagicNotes', 'listMagicNoteTags', 'renameMagicNoteTag', 'deleteMagicNoteTag',
'listMagicNotes', 'listMagicNoteTags', 'createMagicNoteTag', 'renameMagicNoteTag', 'deleteMagicNoteTag',
'getMagicNote', 'getMagicNoteContext', 'createMagicNote', 'updateMagicNote',
'deleteMagicNote', 'createMagicNoteEntry', 'updateMagicNoteEntry', 'deleteMagicNoteEntry',
'getMagicNoteEntry', 'saveMagicNoteAnalysis', 'listMagicTodos', 'getMagicTodoStatus',
+6
View File
@@ -8,6 +8,7 @@ import {
magicNoteEntryCreateSchema,
magicNoteEntryDeleteSchema,
magicNoteEntryUpdateSchema,
magicNoteTagCreateSchema,
magicNoteTagDeleteSchema,
magicNoteTagRenameSchema,
magicNoteUpdateSchema,
@@ -62,6 +63,11 @@ export function registerMagicNotesIpcHandlers(
return assistantDatabase.renameMagicNoteTag(magicNoteTagRenameSchema.parse(input))
})
registerHandler(ipcChannels.magicNotesCreateTag, (event, input: unknown) => {
assertTrustedSender(event, window)
return assistantDatabase.createMagicNoteTag(magicNoteTagCreateSchema.parse(input))
})
registerHandler(ipcChannels.magicNotesDeleteTag, (event, input: unknown) => {
assertTrustedSender(event, window)
const { tagId } = magicNoteTagDeleteSchema.parse(input)