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` 视觉变体,与模型设置的分段控件保持同一外观。 - “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。 - 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。 - 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。 - 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。 - 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
+5 -1
View File
@@ -5686,9 +5686,13 @@ export function registerIpcHandlers(
ipcChannels.magicTodosUpdate, ipcChannels.magicTodosUpdate,
(event, input: unknown) => { (event, input: unknown) => {
assertTrustedSender(event, window) assertTrustedSender(event, window)
return assistantDatabase.updateMagicTodo( const todo = assistantDatabase.updateMagicTodo(
magicTodoUpdateSchema.parse(input) magicTodoUpdateSchema.parse(input)
) )
return {
todo,
note: assistantDatabase.getMagicNote(todo.noteId)
}
} }
) )
+2 -1
View File
@@ -107,6 +107,7 @@ import type {
MagicNoteDetail, MagicNoteDetail,
MagicNotesSnapshot, MagicNotesSnapshot,
MagicTodoItem, MagicTodoItem,
MagicTodoUpdateResult,
MagicTodosSnapshot MagicTodosSnapshot
} from '../shared/magic-notes-contracts' } from '../shared/magic-notes-contracts'
import type { import type {
@@ -1113,7 +1114,7 @@ const desktopApi: DesktopApi = {
ipcRenderer.invoke( ipcRenderer.invoke(
ipcChannels.magicTodosUpdate, ipcChannels.magicTodosUpdate,
input input
) as Promise<MagicTodoItem>, ) as Promise<MagicTodoUpdateResult>,
analyzeTodo: (todoId, options) => analyzeTodo: (todoId, options) =>
ipcRenderer.invoke(ipcChannels.magicTodosAnalyze, { ipcRenderer.invoke(ipcChannels.magicTodosAnalyze, {
todoId, todoId,
+76 -5
View File
@@ -14,7 +14,9 @@ import type { ApplicationSettings } from '../../shared/application-settings-cont
import type { import type {
MagicNoteDetail, MagicNoteDetail,
MagicNotesSnapshot, MagicNotesSnapshot,
MagicNoteRichContent,
MagicTodoItem, MagicTodoItem,
MagicTodoUpdateResult,
MagicTodosSnapshot MagicTodosSnapshot
} from '../../shared/magic-notes-contracts' } from '../../shared/magic-notes-contracts'
import { MagicNotesWorkspace } from './MagicNotesWorkspace' import { MagicNotesWorkspace } from './MagicNotesWorkspace'
@@ -47,7 +49,22 @@ vi.mock('./MagicNoteEditor', () => ({
})) }))
vi.mock('./MagicNoteContent', () => ({ 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' const noteId = '00000000-0000-4000-8000-000000000601'
@@ -229,11 +246,41 @@ beforeEach(() => {
revision: entry.revision + 1 revision: entry.revision + 1
})) }))
}) })
updateTodo.mockImplementation(async (input) => ({ updateTodo.mockImplementation(
async (input): Promise<MagicTodoUpdateResult> => ({
todo: {
...noteTodo, ...noteTodo,
completed: input.completed, completed: input.completed,
revision: noteTodo.revision + 1 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({ analyze.mockResolvedValue({
...createdDetail, ...createdDetail,
entries: createdDetail.entries.map((entry) => entries: createdDetail.entries.map((entry) =>
@@ -957,7 +1004,7 @@ describe('MagicNotesWorkspace', () => {
expect(screen.getByText('准备演示')).toBeInTheDocument() 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} />) render(<MagicNotesWorkspace onNotify={onNotify} />)
await screen.findByText('记录正文') await screen.findByText('记录正文')
@@ -986,6 +1033,30 @@ describe('MagicNotesWorkspace', () => {
message: '待办已完成' 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 () => { it('reuses the AI comments pane for selected todos', async () => {
+17 -6
View File
@@ -693,9 +693,7 @@ export function MagicNotesWorkspace({
[] []
) )
const applyDetail = useCallback((next: MagicNoteDetail) => { const applyNoteSummary = useCallback((next: MagicNoteDetail) => {
setDetail(next)
setTitleDraft(next.title)
setNotes((current) => { setNotes((current) => {
const summary = noteSummary(next) const summary = noteSummary(next)
const existing = current.some((note) => note.id === next.id) 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) => { const applyTodo = useCallback((next: MagicTodoItem) => {
setTodos((current) => setTodos((current) =>
[ [
@@ -1184,13 +1191,17 @@ export function MagicNotesWorkspace({
} }
try { try {
const completed = !todo.completed const completed = !todo.completed
applyTodo( const result = await window.goodbuddy.magicNotes.updateTodo({
await window.goodbuddy.magicNotes.updateTodo({
todoId: todo.id, todoId: todo.id,
completed, completed,
expectedRevision: todo.revision expectedRevision: todo.revision
}) })
) applyTodo(result.todo)
if (requestedNoteIdRef.current === result.note.id) {
applyDetail(result.note)
} else {
applyNoteSummary(result.note)
}
notifySuccess( notifySuccess(
t( t(
completed completed
+2 -1
View File
@@ -64,6 +64,7 @@ import type {
MagicNoteUpdateInput, MagicNoteUpdateInput,
MagicTodoItem, MagicTodoItem,
MagicTodoUpdateInput, MagicTodoUpdateInput,
MagicTodoUpdateResult,
MagicTodosSnapshot MagicTodosSnapshot
} from './magic-notes-contracts' } from './magic-notes-contracts'
import type { import type {
@@ -1704,7 +1705,7 @@ export type DesktopApi = {
listTodos: () => Promise<MagicTodosSnapshot> listTodos: () => Promise<MagicTodosSnapshot>
updateTodo: ( updateTodo: (
input: MagicTodoUpdateInput input: MagicTodoUpdateInput
) => Promise<MagicTodoItem> ) => Promise<MagicTodoUpdateResult>
analyzeTodo: ( analyzeTodo: (
todoId: string, todoId: string,
options: MagicNoteAnalysisOptions options: MagicNoteAnalysisOptions
+5
View File
@@ -433,6 +433,11 @@ export type MagicTodoItem = {
updatedAt: string updatedAt: string
} }
export type MagicTodoUpdateResult = {
todo: MagicTodoItem
note: MagicNoteDetail
}
export type MagicTodosSnapshot = { export type MagicTodosSnapshot = {
todos: MagicTodoItem[] todos: MagicTodoItem[]
} }