fix: sync todo completion with source notes

Completing a Magic Notes todo updated the stored checklist but left the cached
source note stale until the workspace was remounted. Todo updates now return
the refreshed source note and update the active note cache immediately for
both completion and reopening.

Release note: 修复在待办视图勾选或取消待办后,源笔记必须切换页面才显示最新状态的问题;现在会即时同步。
This commit is contained in:
mesalogo
2026-08-21 23:27:41 +08:00
parent 63f0f7f0b0
commit fee4686750
7 changed files with 114 additions and 20 deletions
+1
View File
@@ -613,6 +613,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
+5 -1
View File
@@ -5686,9 +5686,13 @@ export function registerIpcHandlers(
ipcChannels.magicTodosUpdate,
(event, input: unknown) => {
assertTrustedSender(event, window)
return assistantDatabase.updateMagicTodo(
const todo = assistantDatabase.updateMagicTodo(
magicTodoUpdateSchema.parse(input)
)
return {
todo,
note: assistantDatabase.getMagicNote(todo.noteId)
}
}
)
+2 -1
View File
@@ -107,6 +107,7 @@ import type {
MagicNoteDetail,
MagicNotesSnapshot,
MagicTodoItem,
MagicTodoUpdateResult,
MagicTodosSnapshot
} from '../shared/magic-notes-contracts'
import type {
@@ -1113,7 +1114,7 @@ const desktopApi: DesktopApi = {
ipcRenderer.invoke(
ipcChannels.magicTodosUpdate,
input
) as Promise<MagicTodoItem>,
) as Promise<MagicTodoUpdateResult>,
analyzeTodo: (todoId, options) =>
ipcRenderer.invoke(ipcChannels.magicTodosAnalyze, {
todoId,
+78 -7
View File
@@ -14,7 +14,9 @@ import type { ApplicationSettings } from '../../shared/application-settings-cont
import type {
MagicNoteDetail,
MagicNotesSnapshot,
MagicNoteRichContent,
MagicTodoItem,
MagicTodoUpdateResult,
MagicTodosSnapshot
} from '../../shared/magic-notes-contracts'
import { MagicNotesWorkspace } from './MagicNotesWorkspace'
@@ -47,7 +49,22 @@ vi.mock('./MagicNoteEditor', () => ({
}))
vi.mock('./MagicNoteContent', () => ({
MagicNoteContent: () => <div>记录正文</div>
MagicNoteContent: ({ content }: { content: MagicNoteRichContent }) => {
const checklistState =
content.ops.find(
(operation) =>
operation.attributes?.list === 'checked' ||
operation.attributes?.list === 'unchecked'
)?.attributes?.list ?? 'none'
return (
<div>
<span>记录正文</span>
<output data-testid="magic-note-checklist-state">
{checklistState}
</output>
</div>
)
}
}))
const noteId = '00000000-0000-4000-8000-000000000601'
@@ -229,11 +246,41 @@ beforeEach(() => {
revision: entry.revision + 1
}))
})
updateTodo.mockImplementation(async (input) => ({
...noteTodo,
completed: input.completed,
revision: noteTodo.revision + 1
}))
updateTodo.mockImplementation(
async (input): Promise<MagicTodoUpdateResult> => ({
todo: {
...noteTodo,
completed: input.completed,
revision: input.expectedRevision + 1
},
note: {
...detail,
revision: detail.revision + 1,
entries: detail.entries.map((entry) =>
entry.id === noteTodo.entryId
? {
...entry,
content: {
version: 1,
ops: [
{ insert: noteTodo.title },
{
insert: '\n',
attributes: {
list: input.completed ? 'checked' : 'unchecked'
}
}
]
},
comments: [],
analyzedAt: undefined,
revision: entry.revision + 1
}
: entry
)
}
})
)
analyze.mockResolvedValue({
...createdDetail,
entries: createdDetail.entries.map((entry) =>
@@ -957,7 +1004,7 @@ describe('MagicNotesWorkspace', () => {
expect(screen.getByText('准备演示')).toBeInTheDocument()
})
it('marks a todo completed from the standalone todo tab', async () => {
it('synchronizes todo completion with its source note immediately', async () => {
render(<MagicNotesWorkspace onNotify={onNotify} />)
await screen.findByText('记录正文')
@@ -986,6 +1033,30 @@ describe('MagicNotesWorkspace', () => {
message: '待办已完成'
})
)
fireEvent.click(screen.getByRole('tab', { name: '笔记' }))
expect(
screen.getByTestId('magic-note-checklist-state')
).toHaveTextContent('checked')
fireEvent.click(screen.getByRole('tab', { name: '待办' }))
fireEvent.click(
screen.getByRole('button', {
name: `标记为未完成:${noteTodo.title}`
})
)
await waitFor(() =>
expect(updateTodo).toHaveBeenNthCalledWith(2, {
todoId: noteTodo.id,
completed: false,
expectedRevision: noteTodo.revision + 1
})
)
fireEvent.click(screen.getByRole('tab', { name: '笔记' }))
expect(
screen.getByTestId('magic-note-checklist-state')
).toHaveTextContent('unchecked')
})
it('reuses the AI comments pane for selected todos', async () => {
+21 -10
View File
@@ -693,9 +693,7 @@ export function MagicNotesWorkspace({
[]
)
const applyDetail = useCallback((next: MagicNoteDetail) => {
setDetail(next)
setTitleDraft(next.title)
const applyNoteSummary = useCallback((next: MagicNoteDetail) => {
setNotes((current) => {
const summary = noteSummary(next)
const existing = current.some((note) => note.id === next.id)
@@ -710,6 +708,15 @@ export function MagicNotesWorkspace({
})
}, [])
const applyDetail = useCallback(
(next: MagicNoteDetail) => {
setDetail(next)
setTitleDraft(next.title)
applyNoteSummary(next)
},
[applyNoteSummary]
)
const applyTodo = useCallback((next: MagicTodoItem) => {
setTodos((current) =>
[
@@ -1184,13 +1191,17 @@ export function MagicNotesWorkspace({
}
try {
const completed = !todo.completed
applyTodo(
await window.goodbuddy.magicNotes.updateTodo({
todoId: todo.id,
completed,
expectedRevision: todo.revision
})
)
const result = await window.goodbuddy.magicNotes.updateTodo({
todoId: todo.id,
completed,
expectedRevision: todo.revision
})
applyTodo(result.todo)
if (requestedNoteIdRef.current === result.note.id) {
applyDetail(result.note)
} else {
applyNoteSummary(result.note)
}
notifySuccess(
t(
completed
+2 -1
View File
@@ -64,6 +64,7 @@ import type {
MagicNoteUpdateInput,
MagicTodoItem,
MagicTodoUpdateInput,
MagicTodoUpdateResult,
MagicTodosSnapshot
} from './magic-notes-contracts'
import type {
@@ -1704,7 +1705,7 @@ export type DesktopApi = {
listTodos: () => Promise<MagicTodosSnapshot>
updateTodo: (
input: MagicTodoUpdateInput
) => Promise<MagicTodoItem>
) => Promise<MagicTodoUpdateResult>
analyzeTodo: (
todoId: string,
options: MagicNoteAnalysisOptions
+5
View File
@@ -433,6 +433,11 @@ export type MagicTodoItem = {
updatedAt: string
}
export type MagicTodoUpdateResult = {
todo: MagicTodoItem
note: MagicNoteDetail
}
export type MagicTodosSnapshot = {
todos: MagicTodoItem[]
}