fix(mdtodo): restore projected report access on web
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { projectManagementAPI, type MdtodoTaskMutationInput, type MdtodoTaskRecord, type ProjectNavigationResponse, type ProjectSource } from "@/api";
|
||||
import { projectManagementAPI, type MdtodoTaskLinkRecord, type MdtodoTaskMutationInput, type MdtodoTaskRecord, type ProjectNavigationResponse, type ProjectSource } from "@/api";
|
||||
import type { ApiError, ErrorDiagnostic } from "@/types";
|
||||
import EmptyState from "@/components/common/EmptyState.vue";
|
||||
import LoadingState from "@/components/common/LoadingState.vue";
|
||||
@@ -74,6 +74,7 @@ const deleteConfirm = ref(false);
|
||||
const selectedSource = computed(() => source.sources.value.find((s) => s.sourceId === selectedSourceId.value) ?? null);
|
||||
const selectedFile = computed(() => taskData.files.value.find((f) => f.fileRef === selectedFileRef.value) ?? null);
|
||||
const selectedTask = computed(() => taskData.tasks.value.find((t) => t.taskRef === selectedTaskRef.value) ?? null);
|
||||
const selectedTaskProjection = computed(() => selectedTask.value?.task ?? null);
|
||||
const fileScopedTasks = computed(() => selectedFileRef.value ? taskData.tasks.value.filter((t) => t.fileRef === selectedFileRef.value) : taskData.tasks.value);
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
@@ -92,8 +93,18 @@ const taskStatusCounts = computed(() => fileScopedTasks.value.reduce((acc, task)
|
||||
return acc;
|
||||
}, {} as Record<string, number>));
|
||||
|
||||
const selectedTaskBody = computed(() => taskData.taskDetail.value?.body ?? selectedTask.value?.bodyPreview ?? "");
|
||||
const selectedTaskLinks = computed(() => taskData.taskDetail.value?.links ?? []);
|
||||
const selectedTaskBody = computed(() => taskData.taskDetail.value?.body ?? selectedTask.value?.bodyPreview ?? selectedTaskProjection.value?.bodyPreview ?? "");
|
||||
const selectedTaskLinks = computed(() => {
|
||||
const detailLinks = taskData.taskDetail.value?.links ?? [];
|
||||
const detailReportLinks = reportLinksOnly(detailLinks);
|
||||
if (detailReportLinks.length || detailLinks.length) return detailReportLinks;
|
||||
const projectedLinks = selectedTask.value?.links ?? selectedTaskProjection.value?.links ?? [];
|
||||
const projectedReportLinks = reportLinksOnly(projectedLinks);
|
||||
if (projectedReportLinks.length || projectedLinks.length) return projectedReportLinks;
|
||||
const linkCount = Number(selectedTask.value?.linkCount ?? selectedTaskProjection.value?.linkCount ?? 0);
|
||||
if (selectedTask.value && linkCount > 0) return [projectedReportLink(selectedTask.value, linkCount)];
|
||||
return [];
|
||||
});
|
||||
const selectedTaskBodyHtml = computed(() => report.markdownToHtml(selectedTaskBody.value));
|
||||
const reportPreviewHtml = computed(() => report.markdownToHtml(report.reportPreview.value?.content ?? ""));
|
||||
const selectedFileName = computed(() => selectedFile.value ? fileDisplayName(selectedFile.value) : selectedTask.value?.fileRef || "-");
|
||||
@@ -103,6 +114,25 @@ const launchButtonDisabled = computed(() => !selectedTask.value || !workbenchLau
|
||||
const selectedSourceCanReindex = computed(() => Boolean(selectedSourceId.value && !taskData.taskLoading.value && !source.sourceReindexLoading.value));
|
||||
const selectedTaskLabel = computed(() => selectedTask.value ? `${selectedTask.value.taskId || ''} ${selectedTask.value.title || ''}`.trim() : "");
|
||||
|
||||
function reportLinksOnly(links: MdtodoTaskLinkRecord[]): MdtodoTaskLinkRecord[] {
|
||||
return links.filter((link) => link.kind === "markdown-report");
|
||||
}
|
||||
|
||||
function projectedReportLink(task: MdtodoTaskRecord, linkCount: number): MdtodoTaskLinkRecord {
|
||||
const taskId = task.taskId || task.rxxId || "report";
|
||||
return {
|
||||
linkId: `projected-report-unavailable-${task.taskRef}`,
|
||||
label: linkCount === 1 ? `${taskId} 报告` : `${taskId} 报告 (${linkCount})`,
|
||||
href: "",
|
||||
kind: "markdown-report",
|
||||
relativePath: null,
|
||||
ordinal: 1,
|
||||
availability: "projected-link-unavailable",
|
||||
message: "任务投影确认存在报告链接,但当前任务详情读取失败或旧索引缺少链接目标;恢复 HWPOD 详情读取或重建索引后可打开完整报告。",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => void loadPage());
|
||||
|
||||
watch(selectedSourceId, async (sourceId) => {
|
||||
@@ -545,8 +575,8 @@ function setError(err: unknown): void {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; grid-template-rows: 34px auto auto minmax(0, 1fr); gap: 0; overflow: hidden; border: 1px solid #cad6dd; border-radius: 8px; background: #f7fafb; }
|
||||
.mdtodo-framebar { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e1e7; background: #f9fbfc; padding: 0 10px; }
|
||||
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; grid-template-rows: auto auto auto minmax(0, 1fr); gap: 0; overflow: hidden; border: 1px solid #cad6dd; border-radius: 8px; background: #f7fafb; }
|
||||
.mdtodo-framebar { display: flex; min-width: 0; min-height: 34px; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e1e7; background: #f9fbfc; padding: 4px 10px; }
|
||||
.mdtodo-title-lockup { display: inline-flex; min-width: 0; align-items: baseline; gap: 8px; }
|
||||
.mdtodo-title-lockup span { color: #6b7a86; font-size: 10px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
|
||||
.mdtodo-title-lockup strong { color: #111827; font-size: 16px; line-height: 1; }
|
||||
|
||||
@@ -25,8 +25,33 @@ export function useMdtodoReportPreview() {
|
||||
return DOMPurify.sanitize(marked.parse(text, { async: false }) as string);
|
||||
}
|
||||
|
||||
function projectedLinkNeedsRefresh(link: MdtodoTaskLinkRecord): boolean {
|
||||
return link.availability === "projected-link-unavailable";
|
||||
}
|
||||
|
||||
function openProjectedLinkPlaceholder(link: MdtodoTaskLinkRecord): MdtodoReportPreviewRecord {
|
||||
const message = link.message || "任务投影确认存在报告链接,但当前缓存缺少报告目标;恢复 HWPOD 详情读取或重建 MDTODO 索引后可打开完整报告。";
|
||||
const content = `## 报告索引待刷新\n\n${message}\n\n- 报告: ${link.label || link.linkId}\n- 状态: projection-only\n`;
|
||||
const report: MdtodoReportPreviewRecord = {
|
||||
content,
|
||||
fingerprint: `projected:${link.linkId}`,
|
||||
revision: `projected:${link.linkId}`,
|
||||
byteCount: content.length,
|
||||
render: "markdown",
|
||||
linkId: link.linkId,
|
||||
label: link.label,
|
||||
href: link.href,
|
||||
valuesRedacted: true
|
||||
};
|
||||
reportPreview.value = report;
|
||||
activeReportLink.value = link;
|
||||
reportError.value = null;
|
||||
return report;
|
||||
}
|
||||
|
||||
async function openReportPreview(taskRef: string, link: MdtodoTaskLinkRecord): Promise<MdtodoReportPreviewRecord | null> {
|
||||
if (!link.linkId || link.kind !== "markdown-report") return null;
|
||||
if (projectedLinkNeedsRefresh(link)) return openProjectedLinkPlaceholder(link);
|
||||
reportLoading.value = true;
|
||||
reportError.value = null;
|
||||
try {
|
||||
@@ -83,4 +108,3 @@ export function useMdtodoReportPreview() {
|
||||
closeFullscreen
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user