From 6825e1e2cbf8caf94cdb17ef4f069524252a611b Mon Sep 17 00:00:00 2001 From: root Date: Sat, 27 Jun 2026 02:28:16 +0000 Subject: [PATCH] fix(mdtodo): restore projected report access on web --- internal/project-management/mdtodo.test.ts | 3 ++ internal/project-management/mdtodo.ts | 20 +++++----- internal/project-management/store.ts | 4 +- .../src/api/projectManagement.ts | 10 +++++ .../src/components/mdtodo/MdtodoPageShell.vue | 40 ++++++++++++++++--- .../mdtodo/useMdtodoReportPreview.ts | 26 +++++++++++- 6 files changed, 87 insertions(+), 16 deletions(-) diff --git a/internal/project-management/mdtodo.test.ts b/internal/project-management/mdtodo.test.ts index d984ced0..d695c73f 100644 --- a/internal/project-management/mdtodo.test.ts +++ b/internal/project-management/mdtodo.test.ts @@ -27,6 +27,9 @@ Root notes with [context](https://example.test/context). expect(document.tasks[0].depth).toBe(0); expect(document.tasks[1].depth).toBe(1); expect(document.tasks[0].linkCount).toBe(1); + expect(document.tasks[0].links).toEqual([ + expect.objectContaining({ label: "context", kind: "external", relativePath: null }) + ]); expect(document.tasks[0].bodyPreview).toBe("Root notes with [context](https://example.test/context)."); expect(document.tasks[0].body).toBeUndefined(); expect(document.tasks[0].rawContent).toBeUndefined(); diff --git a/internal/project-management/mdtodo.ts b/internal/project-management/mdtodo.ts index 7471096a..9a4f079d 100644 --- a/internal/project-management/mdtodo.ts +++ b/internal/project-management/mdtodo.ts @@ -10,7 +10,6 @@ import path from "node:path"; const taskLinePattern = /^(\s*)(?:[-*+]|\d+[.)])\s+\[([ xX-])\]\s+(.*)$/u; const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu; const rxxIdPattern = /^R\d+(?:\.\d+)*$/iu; -const markdownLinkPattern = /!?\[[^\]]*\]\([^)]+\)|https?:\/\/\S+/giu; const statusAliases = new Map([ ["open", "open"], @@ -220,6 +219,7 @@ function parseRxxTasks(lines, context) { if (parentRxxId && !parentTaskRef) diagnostics.push({ code: "missing_parent_rxx_id", rxxId: block.rxxId, parentRxxId, lineNumber: block.lineNumber }); const bodyText = lines.slice(block.lineIndex + 1, block.endLine).join("\n"); const bodyPreview = bodyPreviewFromLines(lines, block.lineIndex + 1, block.endLine); + const links = markdownLinksFromBody(bodyText, context.relativePath); const title = cleanTaskTitle(block.title || bodyPreview || block.rxxId); return { taskRef: taskRefForRxx(context.sourceId, context.fileRef, block.rxxId), @@ -236,12 +236,13 @@ function parseRxxTasks(lines, context) { depth: rxxDepth(block.rxxId), lineNumber: block.lineNumber, ordinal: index + 1, - linkCount: countMarkdownLinks(bodyText), + linkCount: links.length, sourceFingerprint: context.fingerprint, parserMode: "rxx", headingLevel: block.hashes.length, statusMarker: block.statusMarker, - bodyPreview + bodyPreview, + links }; }); @@ -535,15 +536,14 @@ function stripStatusMarkers(value) { return String(value ?? "").replace(/\s*\[([^\]]+)\]/gu, (match, label) => normalizeStatus(label) ? "" : match).trim(); } -function countMarkdownLinks(value) { - return [...String(value ?? "").matchAll(markdownLinkPattern)].length; -} - function markdownLinksFromBody(body, relativePath) { + const text = String(body ?? ""); const links = []; const markdownPattern = /!?\[([^\]]*)\]\(([^)]+)\)/gu; + const markdownRanges = []; let ordinal = 0; - for (const match of String(body ?? "").matchAll(markdownPattern)) { + for (const match of text.matchAll(markdownPattern)) { + markdownRanges.push([match.index ?? 0, (match.index ?? 0) + String(match[0] ?? "").length]); ordinal += 1; const label = String(match[1] ?? "").trim(); const href = cleanMarkdownHref(match[2]); @@ -559,7 +559,9 @@ function markdownLinksFromBody(body, relativePath) { }); } - for (const match of String(body ?? "").matchAll(/https?:\/\/\S+/giu)) { + for (const match of text.matchAll(/https?:\/\/\S+/giu)) { + const index = match.index ?? 0; + if (markdownRanges.some(([start, end]) => index >= start && index < end)) continue; ordinal += 1; const href = String(match[0] ?? "").replace(/[),.;]+$/u, ""); links.push({ diff --git a/internal/project-management/store.ts b/internal/project-management/store.ts index 9525b91f..38954aa8 100644 --- a/internal/project-management/store.ts +++ b/internal/project-management/store.ts @@ -469,7 +469,9 @@ function appendTaskFilterClauses(params, clauses, filters = {}) { } function taskFromRow(row) { + const taskJson = row.task_json && typeof row.task_json === "object" && !Array.isArray(row.task_json) ? row.task_json : {}; return { + ...taskJson, taskRef: row.task_ref, projectId: row.project_id, sourceId: row.source_id, @@ -484,7 +486,7 @@ function taskFromRow(row) { linkCount: row.link_count, sourceFingerprint: row.source_fingerprint, updatedAt: row.updated_at, - task: row.task_json ?? {} + task: taskJson }; } diff --git a/web/hwlab-cloud-web/src/api/projectManagement.ts b/web/hwlab-cloud-web/src/api/projectManagement.ts index d1227b05..6feef78a 100644 --- a/web/hwlab-cloud-web/src/api/projectManagement.ts +++ b/web/hwlab-cloud-web/src/api/projectManagement.ts @@ -114,6 +114,14 @@ export interface MdtodoTaskRecord { sourceFingerprint?: string; bodyPreview?: string; parserMode?: string; + links?: MdtodoTaskLinkRecord[]; + task?: { + bodyPreview?: string | null; + links?: MdtodoTaskLinkRecord[]; + linkCount?: number | null; + title?: string | null; + [key: string]: unknown; + }; updatedAt?: string; } @@ -124,6 +132,8 @@ export interface MdtodoTaskLinkRecord { kind?: string; relativePath?: string | null; ordinal?: number; + availability?: string; + message?: string; valuesRedacted?: boolean; } diff --git a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue index d2f498b4..c947f03c 100644 --- a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue +++ b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue @@ -4,7 +4,7 @@