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
+1
View File
@@ -963,6 +963,7 @@ const api: DesktopApi & RuntimeNativeClientApi = {
magicNotes: {
search: vi.fn(async () => []),
list: vi.fn(async () => ({ notes: [], tags: [] })),
createTag: vi.fn(async () => ({ id: 'unused-tag', name: 'unused', noteCount: 0 })),
renameTag: vi.fn(async () => {
throw new Error("not used");
}),
+40
View File
@@ -317,11 +317,13 @@ export function MagicNoteTagManagerButton({
export function MagicNoteTagManager({
tags,
onClose,
onCreate,
onRename,
onDelete
}: {
tags: MagicNoteTag[]
onClose: () => void
onCreate: (name: string) => Promise<MagicNoteTag | undefined>
onRename: (tag: MagicNoteTag, name: string) => Promise<MagicNoteTagRenameResult | undefined>
onDelete: (tag: MagicNoteTag) => Promise<boolean>
}): React.JSX.Element {
@@ -332,6 +334,8 @@ export function MagicNoteTagManager({
const descriptionId = useId()
const [editingId, setEditingId] = useState('')
const [draft, setDraft] = useState('')
const [creating, setCreating] = useState(false)
const [createDraft, setCreateDraft] = useState('')
const [deletingId, setDeletingId] = useState('')
const [error, setError] = useState('')
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(
<div className="custom-task-dialog">
<section
@@ -411,6 +430,27 @@ export function MagicNoteTagManager({
</button>
</header>
<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 ? (
<p className="magic-notes-muted">{t('tags.managerEmpty')}</p>
) : (
+17
View File
@@ -1776,6 +1776,22 @@ function MagicNotesWorkspaceView({
setNarrowNotesOpen(false)
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(() => {
if (notesDrawerOpen) document.querySelector<HTMLInputElement>('#magic-library-panel-notes input[type="search"]')?.focus({ preventScroll: true })
}, [notesDrawerOpen])
@@ -3393,6 +3409,7 @@ function MagicNotesWorkspaceView({
<MagicNoteTagManager
tags={noteTags}
onClose={() => setTagManagerOpen(false)}
onCreate={createNoteTag}
onRename={renameNoteTag}
onDelete={deleteNoteTag}
/>
@@ -135,12 +135,13 @@ export const magicNotes = {
inputPlaceholder: 'Type and press Enter',
suggestionsLabel: 'Existing tags',
create: 'Create "{{name}}"',
createButton: 'New tag',
remove: 'Remove tag {{name}}',
limitReached: 'A note can have at most {{count}} tags',
manage: 'Manage tags',
managerTitle: 'Manage tags',
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',
noteCountOther: '{{count}} notes',
rename: 'Rename',
@@ -154,7 +155,8 @@ export const magicNotes = {
mergeHint: 'A tag named "{{name}}" already exists. Saving merges the two tags.',
renamed: 'Tag renamed',
merged: 'Tag merged into "{{name}}"',
deleted: 'Tag deleted'
deleted: 'Tag deleted',
created: 'Tag created'
},
todos: {
listLabel: 'To-do list',
@@ -130,12 +130,13 @@ export const magicNotes = {
inputPlaceholder: '输入后按回车',
suggestionsLabel: '已有标签',
create: '新建“{{name}}”',
createButton: '新建标签',
remove: '移除标签 {{name}}',
limitReached: '每篇笔记最多 {{count}} 个标签',
manage: '管理标签',
managerTitle: '管理标签',
managerDescription: '重命名或删除标签会同步到所有笔记,笔记本身不会被删除。',
managerEmpty: '还没有标签。打开一篇笔记,在标题下方添加。',
managerEmpty: '还没有标签。可以在这里新建,也可以在笔记标题下方添加。',
noteCountOne: '{{count}} 篇笔记',
noteCountOther: '{{count}} 篇笔记',
rename: '重命名',
@@ -149,7 +150,8 @@ export const magicNotes = {
mergeHint: '已有同名标签“{{name}}”,保存后两个标签会合并。',
renamed: '标签已重命名',
merged: '标签已合并到“{{name}}”',
deleted: '标签已删除'
deleted: '标签已删除',
created: '标签已创建'
},
todos: {
listLabel: '待办列表',
+41 -1
View File
@@ -1673,6 +1673,46 @@
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 {
display: grid;
margin: 0;
@@ -2393,7 +2433,7 @@
position: absolute;
top: var(--space-2);
right: var(--space-2);
opacity: 0;
opacity: 1;
}
.magic-note-row:hover .magic-note-more,