feat: show source entries in todo view

This commit is contained in:
mesalogo
2026-09-08 21:38:33 +08:00
parent c1f4fd0580
commit fbc0f5e9f3
5 changed files with 221 additions and 1 deletions
+11 -1
View File
@@ -463,6 +463,11 @@ describe('MagicNotesWorkspace', () => {
expect(
screen.getByRole('button', { name: '打开原笔记修改' })
).toBeInTheDocument()
const sourceEntry = screen.getByRole('region', {
name: '对应的笔记记录'
})
expect(within(sourceEntry).getByText('发布笔记')).toBeInTheDocument()
expect(within(sourceEntry).getByText('记录正文')).toBeInTheDocument()
})
it('keeps history editing contained and de-emphasizes note deletion', async () => {
@@ -1169,7 +1174,7 @@ describe('MagicNotesWorkspace', () => {
expect(
screen.queryByRole('group', { name: '待办列表方式' })
).not.toBeInTheDocument()
expect(screen.getByText('发布笔记')).toBeInTheDocument()
expect(screen.getAllByText('发布笔记')).not.toHaveLength(0)
expect(screen.getByText('演示笔记')).toBeInTheDocument()
expect(screen.getByText('准备演示')).toBeInTheDocument()
})
@@ -1203,6 +1208,11 @@ describe('MagicNotesWorkspace', () => {
message: '待办已完成'
})
)
expect(
within(
screen.getByRole('region', { name: '对应的笔记记录' })
).getByTestId('magic-note-checklist-state')
).toHaveTextContent('checked')
fireEvent.click(screen.getByRole('tab', { name: '笔记' }))
expect(
+132
View File
@@ -483,6 +483,12 @@ export function MagicNotesWorkspace({
const [selectedNoteId, setSelectedNoteId] = useState('')
const [selectedTodoId, setSelectedTodoId] = useState('')
const [detail, setDetail] = useState<MagicNoteDetail>()
const [todoSourceDetail, setTodoSourceDetail] =
useState<MagicNoteDetail>()
const [todoSourceError, setTodoSourceError] = useState<{
message: string
noteId: string
}>()
const [loadStatus, setLoadStatus] = useState<LoadStatus>('loading')
const [loadError, setLoadError] = useState('')
const [refreshError, setRefreshError] = useState('')
@@ -537,6 +543,7 @@ export function MagicNotesWorkspace({
const [pendingDraftSwitch, setPendingDraftSwitch] =
useState<DraftSwitchTarget>()
const detailRequestRef = useRef(0)
const todoSourceRequestRef = useRef(0)
const requestedNoteIdRef = useRef('')
const refreshRequestRef = useRef(0)
const hasLoadedRef = useRef(false)
@@ -1398,6 +1405,77 @@ export function MagicNotesWorkspace({
() => todos.find((todo) => todo.id === selectedTodoId),
[selectedTodoId, todos]
)
const selectedTodoNoteId = selectedTodo?.noteId
const selectedTodoEntryId = selectedTodo?.entryId
const loadTodoSource = useCallback(
async (noteId: string): Promise<void> => {
const requestId = ++todoSourceRequestRef.current
try {
const sourceDetail = await window.goodbuddy.magicNotes.get(noteId)
if (todoSourceRequestRef.current === requestId) {
setTodoSourceDetail(sourceDetail)
setTodoSourceError(undefined)
}
} catch (sourceError) {
if (todoSourceRequestRef.current === requestId) {
setTodoSourceError({
message: errorMessage(
sourceError,
tRef.current('errors.operationFailed')
),
noteId
})
}
}
},
[]
)
useEffect(() => {
if (libraryView !== 'todos' || !selectedTodoNoteId) {
return
}
const requestId = ++todoSourceRequestRef.current
void window.goodbuddy.magicNotes
.get(selectedTodoNoteId)
.then((sourceDetail) => {
if (todoSourceRequestRef.current === requestId) {
setTodoSourceDetail(sourceDetail)
setTodoSourceError(undefined)
}
})
.catch((sourceError: unknown) => {
if (todoSourceRequestRef.current === requestId) {
setTodoSourceError({
message: errorMessage(
sourceError,
tRef.current('errors.operationFailed')
),
noteId: selectedTodoNoteId
})
}
})
return () => {
todoSourceRequestRef.current += 1
}
}, [libraryView, selectedTodoNoteId])
const selectedTodoSourceEntry = useMemo(
() =>
todoSourceDetail?.entries.find(
(entry) => entry.id === selectedTodoEntryId
),
[selectedTodoEntryId, todoSourceDetail]
)
const selectedTodoSourceError =
todoSourceError && todoSourceError.noteId === selectedTodoNoteId
? todoSourceError.message
: ''
const todoSourceLoading =
Boolean(selectedTodoNoteId) &&
todoSourceDetail?.id !== selectedTodoNoteId &&
!selectedTodoSourceError
const reloadTodos = useCallback(async (): Promise<void> => {
const snapshot = await window.goodbuddy.magicNotes.listTodos()
@@ -1493,6 +1571,11 @@ export function MagicNotesWorkspace({
expectedRevision: todo.revision
})
applyTodo(result.todo)
if (selectedTodoId === todo.id) {
todoSourceRequestRef.current += 1
setTodoSourceDetail(result.note)
setTodoSourceError(undefined)
}
if (requestedNoteIdRef.current === result.note.id) {
applyDetail(result.note)
} else {
@@ -2755,6 +2838,55 @@ export function MagicNotesWorkspace({
<PanelRightClose aria-hidden="true" size={15} />
</button>
</div>
{libraryView === 'todos' && selectedTodo && (
<section
aria-label={t('todos.sourceEntryLabel')}
className="magic-todo-source-entry"
>
<header>
<div>
<strong>{t('todos.sourceEntryHeading')}</strong>
<small>{selectedTodo.noteTitle}</small>
</div>
{selectedTodoSourceEntry && (
<time dateTime={selectedTodoSourceEntry.updatedAt}>
{dateFormatter.format(
new Date(selectedTodoSourceEntry.updatedAt)
)}
</time>
)}
</header>
{todoSourceLoading ? (
<p className="magic-notes-muted">
{t('todos.loadingSourceEntry')}
</p>
) : selectedTodoSourceError ? (
<div className="magic-todo-source-entry__error" role="alert">
<span>
{t('todos.sourceEntryLoadFailed', {
error: selectedTodoSourceError
})}
</span>
<button
className="secondary-button"
onClick={() => {
setTodoSourceError(undefined)
void loadTodoSource(selectedTodo.noteId)
}}
type="button"
>
{t('actions.retry')}
</button>
</div>
) : selectedTodoSourceEntry ? (
<MagicNoteContent content={selectedTodoSourceEntry.content} />
) : (
<p className="magic-notes-muted">
{t('todos.sourceEntryMissing')}
</p>
)}
</section>
)}
<div className="magic-notes-ai-controls">
<label>
<span>{t('comments.directionLabel')}</span>
@@ -115,6 +115,11 @@ export const magicNotes = {
emptySelectionDescription:
'Select a to-do on the left. All to-dos come from note checklists.',
sourceNote: 'From note: {{title}}',
sourceEntryLabel: 'Related note entry',
sourceEntryHeading: 'Related entry',
loadingSourceEntry: 'Loading the related entry…',
sourceEntryMissing: 'The note entry for this to-do could not be found.',
sourceEntryLoadFailed: 'Could not load the related entry: {{error}}',
defaultInstructions: 'This to-do comes from a checklist in a note.',
markComplete: 'Mark complete: {{title}}',
markIncomplete: 'Mark incomplete: {{title}}'
@@ -108,6 +108,11 @@ export const magicNotes = {
emptySelectionTitle: '还没有选择待办',
emptySelectionDescription: '从左侧选择待办;待办统一来自笔记中的清单。',
sourceNote: '来自笔记:{{title}}',
sourceEntryLabel: '对应的笔记记录',
sourceEntryHeading: '对应记录',
loadingSourceEntry: '正在加载对应记录…',
sourceEntryMissing: '未找到这条待办对应的笔记记录。',
sourceEntryLoadFailed: '对应记录加载失败:{{error}}',
defaultInstructions: '此待办来自笔记正文中的待办清单。',
markComplete: '标记为已完成:{{title}}',
markIncomplete: '标记为未完成:{{title}}'
+68
View File
@@ -1397,6 +1397,74 @@
padding: var(--space-4);
}
.magic-todo-source-entry {
display: grid;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--border-subtle);
gap: var(--space-2);
}
.magic-todo-source-entry > header {
display: flex;
min-width: 0;
align-items: start;
justify-content: space-between;
color: var(--text-muted);
font-size: var(--font-caption);
gap: var(--space-2);
}
.magic-todo-source-entry > header > div {
display: grid;
min-width: 0;
gap: var(--space-1);
}
.magic-todo-source-entry > header strong {
color: var(--text-primary);
font-size: var(--font-body);
}
.magic-todo-source-entry > header small {
overflow: hidden;
color: var(--text-muted);
font-size: var(--font-caption);
text-overflow: ellipsis;
white-space: nowrap;
}
.magic-todo-source-entry > header time {
flex: none;
}
.magic-todo-source-entry > .magic-note-content {
max-height: 240px;
overflow: auto;
border: 1px solid var(--border-default);
border-radius: var(--radius-control);
background: var(--surface-raised);
}
.magic-todo-source-entry > .magic-note-content .ql-editor {
padding: var(--space-3);
}
.magic-todo-source-entry > .magic-notes-muted {
padding-inline: 0;
}
.magic-todo-source-entry__error {
display: grid;
color: var(--danger);
font-size: var(--font-caption);
line-height: 1.5;
gap: var(--space-2);
}
.magic-todo-source-entry__error .secondary-button {
width: fit-content;
}
.magic-notes-ai-controls {
display: grid;
padding: var(--space-3) 0;