dorfteich/packages/shared/src/editor-schema/tasks.ts
Claude Fable 5 e164370691 #154: Aufgabenübersicht als Kern-Block (Seite + Unterseiten)
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
2026-07-20 01:20:30 +02:00

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[];
}