feat(magic-notes): improve tag management UI
This commit is contained in:
@@ -7805,8 +7805,8 @@ describe('AssistantDatabase', () => {
|
|||||||
const retagged = database.updateMagicNote({ noteId: work.id, tags: ['Work'], expectedRevision: work.revision })
|
const retagged = database.updateMagicNote({ noteId: work.id, tags: ['Work'], expectedRevision: work.revision })
|
||||||
expect(retagged.tags).toEqual(['Work'])
|
expect(retagged.tags).toEqual(['Work'])
|
||||||
expect(retagged.revision).toBe(work.revision + 1)
|
expect(retagged.revision).toBe(work.revision + 1)
|
||||||
// "Project A" lost its last note and is removed.
|
// Tags remain available after their last note link is removed.
|
||||||
expect(database.listMagicNoteTags().map((tag) => tag.name)).not.toContain('Project A')
|
expect(database.listMagicNoteTags().map((tag) => tag.name)).toContain('Project A')
|
||||||
expect(() => database.updateMagicNote({ noteId: work.id, tags: [], expectedRevision: work.revision })).toThrow()
|
expect(() => database.updateMagicNote({ noteId: work.id, tags: [], expectedRevision: work.revision })).toThrow()
|
||||||
expect(database.getMagicNote(work.id).tags).toEqual(['Work'])
|
expect(database.getMagicNote(work.id).tags).toEqual(['Work'])
|
||||||
|
|
||||||
@@ -7820,19 +7820,23 @@ describe('AssistantDatabase', () => {
|
|||||||
const merged = database.renameMagicNoteTag({ tagId: workTag.id, name: 'Personal' })
|
const merged = database.renameMagicNoteTag({ tagId: workTag.id, name: 'Personal' })
|
||||||
expect(merged).toMatchObject({ merged: true, tag: { name: 'personal', noteCount: 3 } })
|
expect(merged).toMatchObject({ merged: true, tag: { name: 'personal', noteCount: 3 } })
|
||||||
expect(database.getMagicNote(both.id).tags).toEqual(['personal'])
|
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
|
const calls = onMagicNotesChanged.mock.calls.length
|
||||||
database.deleteMagicNoteTag(merged.tag.id)
|
database.deleteMagicNoteTag(merged.tag.id)
|
||||||
expect(onMagicNotesChanged).toHaveBeenCalledTimes(calls + 1)
|
expect(onMagicNotesChanged).toHaveBeenCalledTimes(calls + 1)
|
||||||
expect(database.listMagicNoteTags()).toEqual([])
|
expect(database.listMagicNoteTags().map((tag) => tag.name)).toEqual(['Project A'])
|
||||||
expect(database.getMagicNote(personal.id)).toMatchObject({ title: 'Personal', tags: [] })
|
expect(database.getMagicNote(personal.id)).toMatchObject({ title: 'Personal', tags: [] })
|
||||||
expect(() => database.deleteMagicNoteTag(merged.tag.id)).toThrow('标签不存在')
|
expect(() => database.deleteMagicNoteTag(merged.tag.id)).toThrow('标签不存在')
|
||||||
expect(() => database.renameMagicNoteTag({ tagId: merged.tag.id, name: 'x' })).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)
|
database.deleteMagicNote(personal.id)
|
||||||
expect(database.listMagicNoteTags()).toEqual([])
|
expect(database.listMagicNoteTags().map((tag) => tag.name)).toEqual(['Ideas', 'Project A', 'Solo'])
|
||||||
} finally {
|
} finally {
|
||||||
database.close()
|
database.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4169,13 +4169,30 @@ export class AssistantDatabase {
|
|||||||
FROM magic_note_tags t
|
FROM magic_note_tags t
|
||||||
LEFT JOIN magic_note_tag_links l ON l.tag_id = t.id
|
LEFT JOIN magic_note_tag_links l ON l.tag_id = t.id
|
||||||
GROUP BY t.id
|
GROUP BY t.id
|
||||||
HAVING note_count > 0
|
|
||||||
ORDER BY note_count DESC, t.name_key ASC`
|
ORDER BY note_count DESC, t.name_key ASC`
|
||||||
)
|
)
|
||||||
.all() as MagicNoteTagRow[]
|
.all() as MagicNoteTagRow[]
|
||||||
return rows.map(toMagicNoteTag)
|
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,
|
* 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.
|
* 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)
|
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 {
|
getMagicNote(noteId: string): MagicNoteDetail {
|
||||||
@@ -4474,7 +4483,6 @@ export class AssistantDatabase {
|
|||||||
if (result.changes !== 1) {
|
if (result.changes !== 1) {
|
||||||
throw new Error('笔记不存在')
|
throw new Error('笔记不存在')
|
||||||
}
|
}
|
||||||
this.deleteOrphanMagicNoteTags(database)
|
|
||||||
database.exec('COMMIT')
|
database.exec('COMMIT')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
database.exec('ROLLBACK')
|
database.exec('ROLLBACK')
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const assistantStorageMethods = [
|
|||||||
'deleteLocalConversation', 'getOrCreateRemoteConversation', 'appendConversationMessage',
|
'deleteLocalConversation', 'getOrCreateRemoteConversation', 'appendConversationMessage',
|
||||||
'appendRemoteConversationMessage', 'claimChannelEvent', 'releaseChannelEvent',
|
'appendRemoteConversationMessage', 'claimChannelEvent', 'releaseChannelEvent',
|
||||||
'enqueueChannelResult', 'markChannelResult', 'listUndeliveredChannelResults',
|
'enqueueChannelResult', 'markChannelResult', 'listUndeliveredChannelResults',
|
||||||
'listMagicNotes', 'listMagicNoteTags', 'renameMagicNoteTag', 'deleteMagicNoteTag',
|
'listMagicNotes', 'listMagicNoteTags', 'createMagicNoteTag', 'renameMagicNoteTag', 'deleteMagicNoteTag',
|
||||||
'getMagicNote', 'getMagicNoteContext', 'createMagicNote', 'updateMagicNote',
|
'getMagicNote', 'getMagicNoteContext', 'createMagicNote', 'updateMagicNote',
|
||||||
'deleteMagicNote', 'createMagicNoteEntry', 'updateMagicNoteEntry', 'deleteMagicNoteEntry',
|
'deleteMagicNote', 'createMagicNoteEntry', 'updateMagicNoteEntry', 'deleteMagicNoteEntry',
|
||||||
'getMagicNoteEntry', 'saveMagicNoteAnalysis', 'listMagicTodos', 'getMagicTodoStatus',
|
'getMagicNoteEntry', 'saveMagicNoteAnalysis', 'listMagicTodos', 'getMagicTodoStatus',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
magicNoteEntryCreateSchema,
|
magicNoteEntryCreateSchema,
|
||||||
magicNoteEntryDeleteSchema,
|
magicNoteEntryDeleteSchema,
|
||||||
magicNoteEntryUpdateSchema,
|
magicNoteEntryUpdateSchema,
|
||||||
|
magicNoteTagCreateSchema,
|
||||||
magicNoteTagDeleteSchema,
|
magicNoteTagDeleteSchema,
|
||||||
magicNoteTagRenameSchema,
|
magicNoteTagRenameSchema,
|
||||||
magicNoteUpdateSchema,
|
magicNoteUpdateSchema,
|
||||||
@@ -62,6 +63,11 @@ export function registerMagicNotesIpcHandlers(
|
|||||||
return assistantDatabase.renameMagicNoteTag(magicNoteTagRenameSchema.parse(input))
|
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) => {
|
registerHandler(ipcChannels.magicNotesDeleteTag, (event, input: unknown) => {
|
||||||
assertTrustedSender(event, window)
|
assertTrustedSender(event, window)
|
||||||
const { tagId } = magicNoteTagDeleteSchema.parse(input)
|
const { tagId } = magicNoteTagDeleteSchema.parse(input)
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ import type {
|
|||||||
MagicNoteCreateResult,
|
MagicNoteCreateResult,
|
||||||
MagicNoteSummary,
|
MagicNoteSummary,
|
||||||
MagicNoteEntryCreateResult,
|
MagicNoteEntryCreateResult,
|
||||||
|
MagicNoteTag,
|
||||||
MagicNoteTagRenameResult,
|
MagicNoteTagRenameResult,
|
||||||
MagicNotesSnapshot,
|
MagicNotesSnapshot,
|
||||||
MagicTodoItem,
|
MagicTodoItem,
|
||||||
@@ -1707,6 +1708,11 @@ const desktopApi: DesktopApi = {
|
|||||||
remove: async (noteId: string) => {
|
remove: async (noteId: string) => {
|
||||||
await ipcRenderer.invoke(ipcChannels.magicNotesDelete, { noteId })
|
await ipcRenderer.invoke(ipcChannels.magicNotesDelete, { noteId })
|
||||||
},
|
},
|
||||||
|
createTag: (input) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
ipcChannels.magicNotesCreateTag,
|
||||||
|
input
|
||||||
|
) as Promise<MagicNoteTag>,
|
||||||
renameTag: (input) =>
|
renameTag: (input) =>
|
||||||
ipcRenderer.invoke(
|
ipcRenderer.invoke(
|
||||||
ipcChannels.magicNotesRenameTag,
|
ipcChannels.magicNotesRenameTag,
|
||||||
|
|||||||
@@ -963,6 +963,7 @@ const api: DesktopApi & RuntimeNativeClientApi = {
|
|||||||
magicNotes: {
|
magicNotes: {
|
||||||
search: vi.fn(async () => []),
|
search: vi.fn(async () => []),
|
||||||
list: vi.fn(async () => ({ notes: [], tags: [] })),
|
list: vi.fn(async () => ({ notes: [], tags: [] })),
|
||||||
|
createTag: vi.fn(async () => ({ id: 'unused-tag', name: 'unused', noteCount: 0 })),
|
||||||
renameTag: vi.fn(async () => {
|
renameTag: vi.fn(async () => {
|
||||||
throw new Error("not used");
|
throw new Error("not used");
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -317,11 +317,13 @@ export function MagicNoteTagManagerButton({
|
|||||||
export function MagicNoteTagManager({
|
export function MagicNoteTagManager({
|
||||||
tags,
|
tags,
|
||||||
onClose,
|
onClose,
|
||||||
|
onCreate,
|
||||||
onRename,
|
onRename,
|
||||||
onDelete
|
onDelete
|
||||||
}: {
|
}: {
|
||||||
tags: MagicNoteTag[]
|
tags: MagicNoteTag[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onCreate: (name: string) => Promise<MagicNoteTag | undefined>
|
||||||
onRename: (tag: MagicNoteTag, name: string) => Promise<MagicNoteTagRenameResult | undefined>
|
onRename: (tag: MagicNoteTag, name: string) => Promise<MagicNoteTagRenameResult | undefined>
|
||||||
onDelete: (tag: MagicNoteTag) => Promise<boolean>
|
onDelete: (tag: MagicNoteTag) => Promise<boolean>
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
@@ -332,6 +334,8 @@ export function MagicNoteTagManager({
|
|||||||
const descriptionId = useId()
|
const descriptionId = useId()
|
||||||
const [editingId, setEditingId] = useState('')
|
const [editingId, setEditingId] = useState('')
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [createDraft, setCreateDraft] = useState('')
|
||||||
const [deletingId, setDeletingId] = useState('')
|
const [deletingId, setDeletingId] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
@@ -383,6 +387,21 @@ export function MagicNoteTagManager({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitCreate = async (): Promise<void> => {
|
||||||
|
if (busy) return
|
||||||
|
const problem = tagNameError(createDraft)
|
||||||
|
if (problem) { setError(problem); return }
|
||||||
|
setBusy(true)
|
||||||
|
const created = await onCreate(createDraft.trim())
|
||||||
|
setBusy(false)
|
||||||
|
if (created) {
|
||||||
|
setCreateDraft('')
|
||||||
|
setCreating(false)
|
||||||
|
setError('')
|
||||||
|
requestAnimationFrame(() => document.getElementById(`magic-note-tag-rename-${created.id}`)?.focus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="custom-task-dialog">
|
<div className="custom-task-dialog">
|
||||||
<section
|
<section
|
||||||
@@ -411,6 +430,27 @@ export function MagicNoteTagManager({
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="magic-note-tag-manager__body" aria-busy={busy}>
|
<div className="magic-note-tag-manager__body" aria-busy={busy}>
|
||||||
|
{creating ? (
|
||||||
|
<form className="magic-note-tag-manager__create" onSubmit={(event) => { event.preventDefault(); void submitCreate() }}>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
aria-label={t('tags.inputLabel')}
|
||||||
|
aria-invalid={Boolean(error)}
|
||||||
|
maxLength={MAGIC_NOTE_TAG_MAX_LENGTH}
|
||||||
|
placeholder={t('tags.inputPlaceholder')}
|
||||||
|
value={createDraft}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(event) => { setCreateDraft(event.target.value); setError('') }}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="primary-button" disabled={busy}>{t('tags.createButton')}</button>
|
||||||
|
<button type="button" className="secondary-button" disabled={busy} onClick={() => { setCreating(false); setCreateDraft(''); setError('') }}>{t('tags.cancel')}</button>
|
||||||
|
{error && <small className="magic-notes-field-error" role="alert">{error}</small>}
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="secondary-button magic-note-tag-manager__create-button" disabled={busy} onClick={() => { setCreating(true); setError('') }}>
|
||||||
|
<Plus aria-hidden="true" size={14} />{t('tags.createButton')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{tags.length === 0 ? (
|
{tags.length === 0 ? (
|
||||||
<p className="magic-notes-muted">{t('tags.managerEmpty')}</p>
|
<p className="magic-notes-muted">{t('tags.managerEmpty')}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1776,6 +1776,22 @@ function MagicNotesWorkspaceView({
|
|||||||
setNarrowNotesOpen(false)
|
setNarrowNotesOpen(false)
|
||||||
document.getElementById('magic-notes-list-toggle')?.focus({ preventScroll: true })
|
document.getElementById('magic-notes-list-toggle')?.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createNoteTag = async (name: string): Promise<MagicNoteTag | undefined> => {
|
||||||
|
const operation = 'create-tag'
|
||||||
|
if (!beginBusy(operation)) return undefined
|
||||||
|
try {
|
||||||
|
const tag = await window.goodbuddy.magicNotes.createTag({ name })
|
||||||
|
notifySuccess(t('tags.created'))
|
||||||
|
await refreshNotes(requestedNoteIdRef.current, true)
|
||||||
|
return tag
|
||||||
|
} catch (error) {
|
||||||
|
notifyError(error)
|
||||||
|
return undefined
|
||||||
|
} finally {
|
||||||
|
endBusy(operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (notesDrawerOpen) document.querySelector<HTMLInputElement>('#magic-library-panel-notes input[type="search"]')?.focus({ preventScroll: true })
|
if (notesDrawerOpen) document.querySelector<HTMLInputElement>('#magic-library-panel-notes input[type="search"]')?.focus({ preventScroll: true })
|
||||||
}, [notesDrawerOpen])
|
}, [notesDrawerOpen])
|
||||||
@@ -3393,6 +3409,7 @@ function MagicNotesWorkspaceView({
|
|||||||
<MagicNoteTagManager
|
<MagicNoteTagManager
|
||||||
tags={noteTags}
|
tags={noteTags}
|
||||||
onClose={() => setTagManagerOpen(false)}
|
onClose={() => setTagManagerOpen(false)}
|
||||||
|
onCreate={createNoteTag}
|
||||||
onRename={renameNoteTag}
|
onRename={renameNoteTag}
|
||||||
onDelete={deleteNoteTag}
|
onDelete={deleteNoteTag}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -135,12 +135,13 @@ export const magicNotes = {
|
|||||||
inputPlaceholder: 'Type and press Enter',
|
inputPlaceholder: 'Type and press Enter',
|
||||||
suggestionsLabel: 'Existing tags',
|
suggestionsLabel: 'Existing tags',
|
||||||
create: 'Create "{{name}}"',
|
create: 'Create "{{name}}"',
|
||||||
|
createButton: 'New tag',
|
||||||
remove: 'Remove tag {{name}}',
|
remove: 'Remove tag {{name}}',
|
||||||
limitReached: 'A note can have at most {{count}} tags',
|
limitReached: 'A note can have at most {{count}} tags',
|
||||||
manage: 'Manage tags',
|
manage: 'Manage tags',
|
||||||
managerTitle: 'Manage tags',
|
managerTitle: 'Manage tags',
|
||||||
managerDescription: 'Renaming or deleting a tag updates every note. Notes themselves are never deleted.',
|
managerDescription: 'Renaming or deleting a tag updates every note. Notes themselves are never deleted.',
|
||||||
managerEmpty: 'No tags yet. Open a note and add one below its title.',
|
managerEmpty: 'No tags yet. Create one here or add one below a note title.',
|
||||||
noteCountOne: '{{count}} note',
|
noteCountOne: '{{count}} note',
|
||||||
noteCountOther: '{{count}} notes',
|
noteCountOther: '{{count}} notes',
|
||||||
rename: 'Rename',
|
rename: 'Rename',
|
||||||
@@ -154,7 +155,8 @@ export const magicNotes = {
|
|||||||
mergeHint: 'A tag named "{{name}}" already exists. Saving merges the two tags.',
|
mergeHint: 'A tag named "{{name}}" already exists. Saving merges the two tags.',
|
||||||
renamed: 'Tag renamed',
|
renamed: 'Tag renamed',
|
||||||
merged: 'Tag merged into "{{name}}"',
|
merged: 'Tag merged into "{{name}}"',
|
||||||
deleted: 'Tag deleted'
|
deleted: 'Tag deleted',
|
||||||
|
created: 'Tag created'
|
||||||
},
|
},
|
||||||
todos: {
|
todos: {
|
||||||
listLabel: 'To-do list',
|
listLabel: 'To-do list',
|
||||||
|
|||||||
@@ -130,12 +130,13 @@ export const magicNotes = {
|
|||||||
inputPlaceholder: '输入后按回车',
|
inputPlaceholder: '输入后按回车',
|
||||||
suggestionsLabel: '已有标签',
|
suggestionsLabel: '已有标签',
|
||||||
create: '新建“{{name}}”',
|
create: '新建“{{name}}”',
|
||||||
|
createButton: '新建标签',
|
||||||
remove: '移除标签 {{name}}',
|
remove: '移除标签 {{name}}',
|
||||||
limitReached: '每篇笔记最多 {{count}} 个标签',
|
limitReached: '每篇笔记最多 {{count}} 个标签',
|
||||||
manage: '管理标签',
|
manage: '管理标签',
|
||||||
managerTitle: '管理标签',
|
managerTitle: '管理标签',
|
||||||
managerDescription: '重命名或删除标签会同步到所有笔记,笔记本身不会被删除。',
|
managerDescription: '重命名或删除标签会同步到所有笔记,笔记本身不会被删除。',
|
||||||
managerEmpty: '还没有标签。打开一篇笔记,在标题下方添加。',
|
managerEmpty: '还没有标签。可以在这里新建,也可以在笔记标题下方添加。',
|
||||||
noteCountOne: '{{count}} 篇笔记',
|
noteCountOne: '{{count}} 篇笔记',
|
||||||
noteCountOther: '{{count}} 篇笔记',
|
noteCountOther: '{{count}} 篇笔记',
|
||||||
rename: '重命名',
|
rename: '重命名',
|
||||||
@@ -149,7 +150,8 @@ export const magicNotes = {
|
|||||||
mergeHint: '已有同名标签“{{name}}”,保存后两个标签会合并。',
|
mergeHint: '已有同名标签“{{name}}”,保存后两个标签会合并。',
|
||||||
renamed: '标签已重命名',
|
renamed: '标签已重命名',
|
||||||
merged: '标签已合并到“{{name}}”',
|
merged: '标签已合并到“{{name}}”',
|
||||||
deleted: '标签已删除'
|
deleted: '标签已删除',
|
||||||
|
created: '标签已创建'
|
||||||
},
|
},
|
||||||
todos: {
|
todos: {
|
||||||
listLabel: '待办列表',
|
listLabel: '待办列表',
|
||||||
|
|||||||
@@ -1673,6 +1673,46 @@
|
|||||||
padding: var(--space-2) var(--space-5) var(--space-5);
|
padding: var(--space-2) var(--space-5) var(--space-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.magic-note-tag-manager__create {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-block: var(--space-2) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-note-tag-manager__create > input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: var(--control-height);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
border: 1px solid var(--border-control);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-note-tag-manager__create > button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: var(--control-height);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-note-tag-manager__create > small {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.magic-note-tag-manager__create {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-note-tag-manager__create > input {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.magic-note-tag-manager__list {
|
.magic-note-tag-manager__list {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -2393,7 +2433,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--space-2);
|
top: var(--space-2);
|
||||||
right: var(--space-2);
|
right: var(--space-2);
|
||||||
opacity: 0;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.magic-note-row:hover .magic-note-more,
|
.magic-note-row:hover .magic-note-more,
|
||||||
|
|||||||
@@ -2363,6 +2363,9 @@ export type DesktopApi = {
|
|||||||
create: (input: MagicNoteCreateInput) => Promise<MagicNoteCreateResult>
|
create: (input: MagicNoteCreateInput) => Promise<MagicNoteCreateResult>
|
||||||
update: (input: MagicNoteUpdateInput) => Promise<MagicNoteDetail>
|
update: (input: MagicNoteUpdateInput) => Promise<MagicNoteDetail>
|
||||||
remove: (noteId: string) => Promise<void>
|
remove: (noteId: string) => Promise<void>
|
||||||
|
createTag: (
|
||||||
|
input: import('./magic-notes-contracts').MagicNoteTagCreateInput
|
||||||
|
) => Promise<import('./magic-notes-contracts').MagicNoteTag>
|
||||||
renameTag: (
|
renameTag: (
|
||||||
input: import('./magic-notes-contracts').MagicNoteTagRenameInput
|
input: import('./magic-notes-contracts').MagicNoteTagRenameInput
|
||||||
) => Promise<import('./magic-notes-contracts').MagicNoteTagRenameResult>
|
) => Promise<import('./magic-notes-contracts').MagicNoteTagRenameResult>
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export const ipcChannels = {
|
|||||||
magicNotesCreate: 'magic-notes:create',
|
magicNotesCreate: 'magic-notes:create',
|
||||||
magicNotesUpdate: 'magic-notes:update',
|
magicNotesUpdate: 'magic-notes:update',
|
||||||
magicNotesDelete: 'magic-notes:delete',
|
magicNotesDelete: 'magic-notes:delete',
|
||||||
|
magicNotesCreateTag: 'magic-notes:create-tag',
|
||||||
magicNotesRenameTag: 'magic-notes:rename-tag',
|
magicNotesRenameTag: 'magic-notes:rename-tag',
|
||||||
magicNotesDeleteTag: 'magic-notes:delete-tag',
|
magicNotesDeleteTag: 'magic-notes:delete-tag',
|
||||||
magicNotesCreateEntry: 'magic-notes:create-entry',
|
magicNotesCreateEntry: 'magic-notes:create-entry',
|
||||||
|
|||||||
@@ -369,6 +369,11 @@ export type MagicNoteUpdateInput = z.infer<typeof magicNoteUpdateSchema>
|
|||||||
|
|
||||||
export const magicNoteTagIdSchema = magicNoteIdSchema
|
export const magicNoteTagIdSchema = magicNoteIdSchema
|
||||||
|
|
||||||
|
export const magicNoteTagCreateSchema = z
|
||||||
|
.object({ name: magicNoteTagNameSchema })
|
||||||
|
.strict()
|
||||||
|
export type MagicNoteTagCreateInput = z.infer<typeof magicNoteTagCreateSchema>
|
||||||
|
|
||||||
export const magicNoteTagRenameSchema = z
|
export const magicNoteTagRenameSchema = z
|
||||||
.object({
|
.object({
|
||||||
tagId: magicNoteTagIdSchema,
|
tagId: magicNoteTagIdSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user