Neuer Block-Atom task_overview (Markdown-Fence dorfteich-tasks, HTML-Placeholder). Shared extractTaskRows liest Task-Zeilen mit Text, Mentions (#150) und Start-/Zieldaten (#152); TasksService sammelt zur Lesezeit den Teilbaum (rekursiv via collectSubtreeIds, canAccessPage- Filter je Quellseite) aus Basis-State + page_updates-Log — KEINE abgeleitete Tabelle nötig (Teilbäume sind klein, kein Drift). Neuer auth-Endpoint GET /read/:pond/:slug/tasks; die öffentliche Ansicht expandiert den Placeholder serverseitig zur statischen Tabelle (Instanz-Sprache). NodeView mit Live-Tabelle und Rückschreib-Checkboxen (optimistisch, Override bis der debounced Collab-Persist nachzieht); Einfügen über die Block-Auswahl (eingebauter Eintrag). Unit- + DB-Tests, neuer CI-Pack tasks.spec (voller Loop inkl. Rückschreiben end-to-end), User-Guide-Doku en+de. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import { Node } from 'prosemirror-model';
|
|
|
|
/**
|
|
* Task extraction for the task overview block (issue #154): every
|
|
* `task_item` of a document as a flat row — its stable id (#153, null for
|
|
* lines not yet opened in an editor), checked state, the line's text
|
|
* (mentions and date markers excluded — they become their own columns),
|
|
* the mentioned users (#150), and the start/due dates (#152).
|
|
*/
|
|
export interface TaskRow {
|
|
id: string | null;
|
|
checked: boolean;
|
|
text: string;
|
|
mentions: { userId: string; username: string }[];
|
|
startDate: string | null;
|
|
dueDate: string | null;
|
|
}
|
|
|
|
export function extractTaskRows(doc: Node): TaskRow[] {
|
|
const rows: TaskRow[] = [];
|
|
doc.descendants((node) => {
|
|
if (node.type.name !== 'task_item') return;
|
|
const row: TaskRow = {
|
|
id: (node.attrs.id as string | null) ?? null,
|
|
checked: node.attrs.checked === true,
|
|
text: '',
|
|
mentions: [],
|
|
startDate: null,
|
|
dueDate: null,
|
|
};
|
|
// The line's own content is its first paragraph; nested task lists
|
|
// produce their own rows via the outer descendants walk.
|
|
const paragraph = node.firstChild;
|
|
if (paragraph && paragraph.type.name === 'paragraph') {
|
|
paragraph.forEach((child) => {
|
|
if (child.isText) {
|
|
row.text += child.text ?? '';
|
|
} else if (child.type.name === 'mention') {
|
|
row.mentions.push({
|
|
userId: child.attrs.userId as string,
|
|
username: child.attrs.username as string,
|
|
});
|
|
} else if (child.type.name === 'date_marker') {
|
|
if (child.attrs.kind === 'due') row.dueDate = child.attrs.date as string;
|
|
else row.startDate = child.attrs.date as string;
|
|
} else if (child.type.name === 'wikilink') {
|
|
row.text +=
|
|
(child.attrs.displayText as string | null) ?? (child.attrs.targetSlug as string);
|
|
}
|
|
});
|
|
}
|
|
row.text = row.text.replace(/\s+/g, ' ').trim();
|
|
rows.push(row);
|
|
});
|
|
return rows;
|
|
}
|
|
|
|
/** Wire shapes of `GET /read/:pond/:slug/tasks` (issue #154). */
|
|
export interface TaskOverviewMention {
|
|
id: string;
|
|
username: string;
|
|
displayName: string;
|
|
}
|
|
|
|
export interface TaskOverviewRow {
|
|
/** Null for lines that never got an id — shown read-only. */
|
|
id: string | null;
|
|
checked: boolean;
|
|
text: string;
|
|
mentions: TaskOverviewMention[];
|
|
startDate: string | null;
|
|
dueDate: string | null;
|
|
}
|
|
|
|
export interface TaskOverviewPage {
|
|
pageId: string;
|
|
slug: string;
|
|
title: string;
|
|
tasks: TaskOverviewRow[];
|
|
}
|