feat: make magic todo status editable
This commit is contained in:
@@ -1932,6 +1932,50 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('updates a derived todo and its source checklist together', async () => {
|
||||
const database = await createDatabase()
|
||||
const note = database.createMagicNote({ title: '发布笔记' })
|
||||
database.createMagicNoteEntry({
|
||||
noteId: note.id,
|
||||
content: {
|
||||
version: 1,
|
||||
ops: [
|
||||
{ insert: '核对发布材料' },
|
||||
{ insert: '\n', attributes: { list: 'unchecked' } }
|
||||
]
|
||||
},
|
||||
plainText: '核对发布材料'
|
||||
})
|
||||
const todo = database.listMagicTodos()[0]!
|
||||
|
||||
const updated = database.updateMagicTodo({
|
||||
todoId: todo.id,
|
||||
completed: true,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
|
||||
expect(updated).toMatchObject({
|
||||
id: todo.id,
|
||||
completed: true,
|
||||
revision: todo.revision + 1
|
||||
})
|
||||
expect(
|
||||
database.getMagicNote(note.id).entries[0]!.content.ops
|
||||
).toEqual([
|
||||
{ insert: '核对发布材料' },
|
||||
{ insert: '\n', attributes: { list: 'checked' } }
|
||||
])
|
||||
expect(() =>
|
||||
database.updateMagicTodo({
|
||||
todoId: todo.id,
|
||||
completed: false,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
).toThrow('待办已被更新,请刷新后重试')
|
||||
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('protects magic note records from stale revisions', async () => {
|
||||
const database = await createDatabase()
|
||||
const note = database.createMagicNote({ title: '并发笔记' })
|
||||
|
||||
@@ -51,13 +51,16 @@ import {
|
||||
type MagicNoteRichContent,
|
||||
type MagicNoteSearchResult,
|
||||
type MagicNoteSummary,
|
||||
type MagicTodoItem
|
||||
type MagicTodoItem,
|
||||
type MagicTodoUpdateInput
|
||||
} from '../../shared/magic-notes-contracts'
|
||||
import type { ComputerControlAuditEvent } from '../computer-control/audit'
|
||||
import {
|
||||
magicNoteChecklistItems,
|
||||
magicNoteImageBytes,
|
||||
magicNotePreview
|
||||
magicNotePlainText,
|
||||
magicNotePreview,
|
||||
setMagicNoteChecklistCompletion
|
||||
} from '../magic-notes/rich-content'
|
||||
import { computeNextHeartbeatRun } from './heartbeat-recurrence'
|
||||
|
||||
@@ -2164,6 +2167,86 @@ export class AssistantDatabase {
|
||||
return toMagicTodo(row)
|
||||
}
|
||||
|
||||
updateMagicTodo(input: MagicTodoUpdateInput): MagicTodoItem {
|
||||
const database = this.requireDatabase()
|
||||
const now = new Date().toISOString()
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
const existing = database
|
||||
.prepare(
|
||||
`SELECT t.note_id, t.entry_id, t.source_index, t.completed,
|
||||
t.revision AS todo_revision,
|
||||
e.content_json, e.revision AS entry_revision
|
||||
FROM magic_todos t
|
||||
INNER JOIN magic_note_entries e ON e.id = t.entry_id
|
||||
WHERE t.id = ? AND t.source = 'note'`
|
||||
)
|
||||
.get(input.todoId) as
|
||||
| {
|
||||
note_id: string
|
||||
entry_id: string
|
||||
source_index: number
|
||||
completed: number
|
||||
todo_revision: number
|
||||
content_json: string
|
||||
entry_revision: number
|
||||
}
|
||||
| undefined
|
||||
if (!existing) {
|
||||
throw new Error('待办不存在')
|
||||
}
|
||||
if (existing.todo_revision !== input.expectedRevision) {
|
||||
throw new Error('待办已被更新,请刷新后重试')
|
||||
}
|
||||
if (Boolean(existing.completed) === input.completed) {
|
||||
database.exec('COMMIT')
|
||||
return this.getMagicTodo(input.todoId)
|
||||
}
|
||||
|
||||
const content = setMagicNoteChecklistCompletion(
|
||||
JSON.parse(existing.content_json) as MagicNoteRichContent,
|
||||
existing.source_index,
|
||||
input.completed
|
||||
)
|
||||
const result = database
|
||||
.prepare(
|
||||
`UPDATE magic_note_entries
|
||||
SET content_json = ?, plain_text = ?, comments_json = '[]',
|
||||
analyzed_at = NULL, revision = revision + 1, updated_at = ?
|
||||
WHERE id = ? AND revision = ?`
|
||||
)
|
||||
.run(
|
||||
JSON.stringify(content),
|
||||
magicNotePlainText(content),
|
||||
now,
|
||||
existing.entry_id,
|
||||
existing.entry_revision
|
||||
)
|
||||
if (result.changes !== 1) {
|
||||
throw new Error('记录已被更新,请刷新后重试')
|
||||
}
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE magic_notes
|
||||
SET revision = revision + 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run(now, existing.note_id)
|
||||
this.syncMagicNoteTodos(
|
||||
database,
|
||||
existing.note_id,
|
||||
existing.entry_id,
|
||||
content,
|
||||
now
|
||||
)
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicTodo(input.todoId)
|
||||
}
|
||||
|
||||
saveMagicTodoAnalysis(input: {
|
||||
todoId: string
|
||||
expectedRevision: number
|
||||
|
||||
+12
-1
@@ -86,7 +86,8 @@ import {
|
||||
magicNoteEntryDeleteSchema,
|
||||
magicNoteEntryUpdateSchema,
|
||||
magicNoteUpdateSchema,
|
||||
magicTodoIdSchema
|
||||
magicTodoIdSchema,
|
||||
magicTodoUpdateSchema
|
||||
} from '../shared/magic-notes-contracts'
|
||||
import {
|
||||
assistantIdSchema,
|
||||
@@ -3495,6 +3496,16 @@ export function registerIpcHandlers(
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
ipcChannels.magicTodosUpdate,
|
||||
(event, input: unknown) => {
|
||||
assertTrustedSender(event, window)
|
||||
return assistantDatabase.updateMagicTodo(
|
||||
magicTodoUpdateSchema.parse(input)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
ipcChannels.magicTodosAnalyze,
|
||||
async (event, input: unknown) => {
|
||||
|
||||
@@ -785,6 +785,11 @@ const desktopApi: DesktopApi = {
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.magicTodosList
|
||||
) as Promise<MagicTodosSnapshot>,
|
||||
updateTodo: (input) =>
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.magicTodosUpdate,
|
||||
input
|
||||
) as Promise<MagicTodoItem>,
|
||||
analyzeTodo: (todoId, options) =>
|
||||
ipcRenderer.invoke(ipcChannels.magicTodosAnalyze, {
|
||||
todoId,
|
||||
|
||||
@@ -479,6 +479,9 @@ const api: DesktopApi = {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
listTodos: vi.fn(async () => ({ todos: [] })),
|
||||
updateTodo: vi.fn(async () => {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
analyzeTodo: vi.fn(async () => {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
|
||||
@@ -152,6 +152,7 @@ const listTodos = vi.fn<() => Promise<MagicTodosSnapshot>>()
|
||||
const remove = vi.fn<DesktopApi['magicNotes']['remove']>()
|
||||
const createEntry = vi.fn<DesktopApi['magicNotes']['createEntry']>()
|
||||
const analyze = vi.fn<DesktopApi['magicNotes']['analyze']>()
|
||||
const updateTodo = vi.fn<DesktopApi['magicNotes']['updateTodo']>()
|
||||
const analyzeTodo = vi.fn<DesktopApi['magicNotes']['analyzeTodo']>()
|
||||
const analyzeDraft = vi.fn<DesktopApi['magicNotes']['analyzeDraft']>()
|
||||
let analysisEventListener:
|
||||
@@ -206,6 +207,11 @@ beforeEach(() => {
|
||||
]
|
||||
}
|
||||
createEntry.mockResolvedValue(createdDetail)
|
||||
updateTodo.mockImplementation(async (input) => ({
|
||||
...noteTodo,
|
||||
completed: input.completed,
|
||||
revision: noteTodo.revision + 1
|
||||
}))
|
||||
analyze.mockResolvedValue({
|
||||
...createdDetail,
|
||||
entries: createdDetail.entries.map((entry) =>
|
||||
@@ -258,6 +264,7 @@ beforeEach(() => {
|
||||
remove,
|
||||
createEntry,
|
||||
analyze,
|
||||
updateTodo,
|
||||
analyzeTodo,
|
||||
analyzeDraft,
|
||||
onAnalysisEvent
|
||||
@@ -319,8 +326,12 @@ describe('MagicNotesWorkspace', () => {
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
fireEvent.click(screen.getByRole('tab', { name: '待办' }))
|
||||
expect(screen.getByText('准备演示')).toBeInTheDocument()
|
||||
const selectedTodoButton = screen
|
||||
.getAllByText('核对发布材料')
|
||||
.find((element) => element.tagName === 'STRONG')
|
||||
?.closest('button')
|
||||
expect(
|
||||
screen.getByRole('button', { name: /核对发布材料/ })
|
||||
selectedTodoButton
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(onNotify).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -355,7 +366,11 @@ describe('MagicNotesWorkspace', () => {
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy()
|
||||
|
||||
expect(screen.getByLabelText('未完成')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getAllByRole('button', {
|
||||
name: `标记为已完成:${noteTodo.title}`
|
||||
})
|
||||
).toHaveLength(2)
|
||||
expect(
|
||||
screen.getByRole('button', { name: '打开原笔记修改' })
|
||||
).toBeInTheDocument()
|
||||
@@ -571,7 +586,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('groups note-backed todos in a directory view', async () => {
|
||||
it('only shows note-backed todos in a directory view', async () => {
|
||||
render(
|
||||
<MagicNotesWorkspace onNotify={onNotify} />
|
||||
)
|
||||
@@ -581,12 +596,45 @@ describe('MagicNotesWorkspace', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '新建待办' })
|
||||
).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: '目录视图' }))
|
||||
expect(
|
||||
screen.queryByRole('group', { name: '待办列表方式' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByText('发布笔记')).toBeInTheDocument()
|
||||
expect(screen.getByText('演示笔记')).toBeInTheDocument()
|
||||
expect(screen.getByText('准备演示')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('marks a todo completed from the standalone todo tab', async () => {
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
|
||||
await screen.findByText('记录正文')
|
||||
fireEvent.click(screen.getByRole('tab', { name: '待办' }))
|
||||
fireEvent.click(
|
||||
screen.getAllByRole('button', {
|
||||
name: `标记为已完成:${noteTodo.title}`
|
||||
})[0]!
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateTodo).toHaveBeenCalledWith({
|
||||
todoId: noteTodo.id,
|
||||
completed: true,
|
||||
expectedRevision: noteTodo.revision
|
||||
})
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: `标记为未完成:${noteTodo.title}`
|
||||
})
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(onNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tone: 'success',
|
||||
message: '待办已完成'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('reuses the AI comments pane for selected todos', async () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
|
||||
@@ -54,7 +54,6 @@ export type MagicNotesWorkspaceProps = {
|
||||
|
||||
type LibraryView = 'notes' | 'todos'
|
||||
type TodoFilter = 'active' | 'completed' | 'all'
|
||||
type TodoListMode = 'list' | 'directory'
|
||||
type LoadStatus = 'loading' | 'ready' | 'error'
|
||||
type ValidationTarget =
|
||||
| 'create-note'
|
||||
@@ -73,11 +72,6 @@ const todoFilters = [
|
||||
{ value: 'all', label: '全部' }
|
||||
] as const
|
||||
|
||||
const todoListModes = [
|
||||
{ value: 'list', label: '待办视图' },
|
||||
{ value: 'directory', label: '目录视图' }
|
||||
] as const
|
||||
|
||||
const commentDirections: ReadonlyArray<{
|
||||
value: MagicNoteCommentDirection
|
||||
label: string
|
||||
@@ -211,35 +205,50 @@ function AiComment({
|
||||
}
|
||||
|
||||
function TodoListItem({
|
||||
disabled,
|
||||
onSelect,
|
||||
onToggle,
|
||||
selected,
|
||||
todo
|
||||
}: {
|
||||
disabled: boolean
|
||||
onSelect: () => void
|
||||
onToggle: () => void
|
||||
selected: boolean
|
||||
todo: MagicTodoItem
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<button
|
||||
aria-pressed={selected}
|
||||
<div
|
||||
className={`magic-todo-list-item ${
|
||||
selected ? 'magic-todo-list-item--active' : ''
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
<span aria-hidden="true" className="magic-todo-list-item__check">
|
||||
<button
|
||||
aria-label={`${
|
||||
todo.completed ? '标记为未完成' : '标记为已完成'
|
||||
}:${todo.title}`}
|
||||
aria-pressed={todo.completed}
|
||||
className="magic-todo-list-item__check"
|
||||
disabled={disabled}
|
||||
onClick={onToggle}
|
||||
type="button"
|
||||
>
|
||||
{todo.completed ? (
|
||||
<CheckCircle2 size={16} />
|
||||
) : (
|
||||
<Circle size={16} />
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
</button>
|
||||
<button
|
||||
aria-pressed={selected}
|
||||
className="magic-todo-list-item__content"
|
||||
onClick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
<strong>{todo.title}</strong>
|
||||
<small>来自笔记:{todo.noteTitle}</small>
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -250,8 +259,6 @@ export function MagicNotesWorkspace({
|
||||
const [todos, setTodos] = useState<MagicTodoItem[]>([])
|
||||
const [libraryView, setLibraryView] = useState<LibraryView>('notes')
|
||||
const [todoFilter, setTodoFilter] = useState<TodoFilter>('active')
|
||||
const [todoListMode, setTodoListMode] =
|
||||
useState<TodoListMode>('list')
|
||||
const [commentMode, setCommentMode] =
|
||||
useState<MagicNoteCommentMode>('immediate')
|
||||
const [commentDirection, setCommentDirection] =
|
||||
@@ -873,6 +880,30 @@ export function MagicNotesWorkspace({
|
||||
}
|
||||
}
|
||||
|
||||
const updateTodoCompletion = async (
|
||||
todo: MagicTodoItem
|
||||
): Promise<void> => {
|
||||
const operation = `update-todo-${todo.id}`
|
||||
if (!beginBusy(operation)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const completed = !todo.completed
|
||||
applyTodo(
|
||||
await window.goodbuddy.magicNotes.updateTodo({
|
||||
todoId: todo.id,
|
||||
completed,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
)
|
||||
notifySuccess(completed ? '待办已完成' : '待办已恢复为未完成')
|
||||
} catch (updateError) {
|
||||
notifyError(updateError)
|
||||
} finally {
|
||||
endBusy(operation)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTitle = async (): Promise<void> => {
|
||||
if (!detail || titleDraft.trim() === detail.title) {
|
||||
return
|
||||
@@ -1308,12 +1339,6 @@ export function MagicNotesWorkspace({
|
||||
value={search}
|
||||
/>
|
||||
</label>
|
||||
<SegmentedControl
|
||||
ariaLabel="待办列表方式"
|
||||
onChange={setTodoListMode}
|
||||
options={todoListModes}
|
||||
value={todoListMode}
|
||||
/>
|
||||
<SegmentedControl
|
||||
ariaLabel="筛选待办"
|
||||
onChange={setTodoFilter}
|
||||
@@ -1347,18 +1372,6 @@ export function MagicNotesWorkspace({
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : todoListMode === 'list' ? (
|
||||
visibleTodos.map((todo) => (
|
||||
<TodoListItem
|
||||
key={todo.id}
|
||||
onSelect={() => {
|
||||
setValidation(undefined)
|
||||
setSelectedTodoId(todo.id)
|
||||
}}
|
||||
selected={selectedTodoId === todo.id}
|
||||
todo={todo}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
todoDirectories.map((directory) => (
|
||||
<section
|
||||
@@ -1373,11 +1386,15 @@ export function MagicNotesWorkspace({
|
||||
<div className="magic-todo-directory__items">
|
||||
{directory.todos.map((todo) => (
|
||||
<TodoListItem
|
||||
disabled={busy === `update-todo-${todo.id}`}
|
||||
key={todo.id}
|
||||
onSelect={() => {
|
||||
setValidation(undefined)
|
||||
setSelectedTodoId(todo.id)
|
||||
}}
|
||||
onToggle={() =>
|
||||
void updateTodoCompletion(todo)
|
||||
}
|
||||
selected={selectedTodoId === todo.id}
|
||||
todo={todo}
|
||||
/>
|
||||
@@ -1780,18 +1797,28 @@ export function MagicNotesWorkspace({
|
||||
) : (
|
||||
<section className="magic-todo-detail">
|
||||
<header>
|
||||
<span
|
||||
aria-label={
|
||||
selectedTodo.completed ? '已完成' : '未完成'
|
||||
}
|
||||
<button
|
||||
aria-label={`${
|
||||
selectedTodo.completed
|
||||
? '标记为未完成'
|
||||
: '标记为已完成'
|
||||
}:${selectedTodo.title}`}
|
||||
aria-pressed={selectedTodo.completed}
|
||||
className="magic-todo-detail__check"
|
||||
disabled={
|
||||
busy === `update-todo-${selectedTodo.id}`
|
||||
}
|
||||
onClick={() =>
|
||||
void updateTodoCompletion(selectedTodo)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{selectedTodo.completed ? (
|
||||
<CheckCircle2 size={24} />
|
||||
) : (
|
||||
<Circle size={24} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<div>
|
||||
<h2>{selectedTodo.title}</h2>
|
||||
<span>来自笔记:{selectedTodo.noteTitle}</span>
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
border-radius: var(--radius-control);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
gap: var(--space-2);
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
text-align: left;
|
||||
@@ -333,18 +332,34 @@
|
||||
|
||||
.magic-todo-list-item__check {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding-top: 1px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-control);
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.magic-todo-list-item > span:last-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: var(--space-1);
|
||||
.magic-todo-list-item__check:hover {
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.magic-todo-list-item strong {
|
||||
.magic-todo-list-item__content {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
gap: var(--space-1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.magic-todo-list-item__content strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-body);
|
||||
@@ -352,7 +367,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.magic-todo-list-item small {
|
||||
.magic-todo-list-item__content small {
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
@@ -476,12 +491,19 @@
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.magic-todo-detail__check:hover {
|
||||
background: var(--accent-selected);
|
||||
}
|
||||
|
||||
.magic-todo-detail > header > div:nth-child(2) {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
@@ -48,6 +48,7 @@ import type {
|
||||
MagicNotesSnapshot,
|
||||
MagicNoteUpdateInput,
|
||||
MagicTodoItem,
|
||||
MagicTodoUpdateInput,
|
||||
MagicTodosSnapshot
|
||||
} from './magic-notes-contracts'
|
||||
import type {
|
||||
@@ -1223,6 +1224,9 @@ export type DesktopApi = {
|
||||
options: MagicNoteAnalysisOptions
|
||||
) => Promise<MagicNoteDraftAnalysis>
|
||||
listTodos: () => Promise<MagicTodosSnapshot>
|
||||
updateTodo: (
|
||||
input: MagicTodoUpdateInput
|
||||
) => Promise<MagicTodoItem>
|
||||
analyzeTodo: (
|
||||
todoId: string,
|
||||
options: MagicNoteAnalysisOptions
|
||||
|
||||
@@ -130,6 +130,7 @@ export const ipcChannels = {
|
||||
magicNotesAnalyzeDraft: 'magic-notes:analyze-draft',
|
||||
magicNotesAnalysisEvent: 'magic-notes:analysis-event',
|
||||
magicTodosList: 'magic-todos:list',
|
||||
magicTodosUpdate: 'magic-todos:update',
|
||||
magicTodosAnalyze: 'magic-todos:analyze',
|
||||
knowledgeSnapshot: 'knowledge:snapshot',
|
||||
knowledgeCreateLibrary: 'knowledge:library:create',
|
||||
|
||||
@@ -205,6 +205,17 @@ export const magicTodoIdSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicTodoUpdateSchema = z
|
||||
.object({
|
||||
todoId: magicNoteIdSchema,
|
||||
completed: z.boolean(),
|
||||
expectedRevision: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
export type MagicTodoUpdateInput = z.infer<
|
||||
typeof magicTodoUpdateSchema
|
||||
>
|
||||
|
||||
export type MagicNoteCommentKind =
|
||||
| 'narrative'
|
||||
| 'summary'
|
||||
|
||||
Reference in New Issue
Block a user