Merge pull request #2221 from pikasTech/fix/2216-mdtodo-report-mobile
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[0].depth).toBe(0);
|
||||||
expect(document.tasks[1].depth).toBe(1);
|
expect(document.tasks[1].depth).toBe(1);
|
||||||
expect(document.tasks[0].linkCount).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].bodyPreview).toBe("Root notes with [context](https://example.test/context).");
|
||||||
expect(document.tasks[0].body).toBeUndefined();
|
expect(document.tasks[0].body).toBeUndefined();
|
||||||
expect(document.tasks[0].rawContent).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 taskLinePattern = /^(\s*)(?:[-*+]|\d+[.)])\s+\[([ xX-])\]\s+(.*)$/u;
|
||||||
const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu;
|
const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu;
|
||||||
const rxxIdPattern = /^R\d+(?:\.\d+)*$/iu;
|
const rxxIdPattern = /^R\d+(?:\.\d+)*$/iu;
|
||||||
const markdownLinkPattern = /!?\[[^\]]*\]\([^)]+\)|https?:\/\/\S+/giu;
|
|
||||||
|
|
||||||
const statusAliases = new Map([
|
const statusAliases = new Map([
|
||||||
["open", "open"],
|
["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 });
|
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 bodyText = lines.slice(block.lineIndex + 1, block.endLine).join("\n");
|
||||||
const bodyPreview = bodyPreviewFromLines(lines, block.lineIndex + 1, block.endLine);
|
const bodyPreview = bodyPreviewFromLines(lines, block.lineIndex + 1, block.endLine);
|
||||||
|
const links = markdownLinksFromBody(bodyText, context.relativePath);
|
||||||
const title = cleanTaskTitle(block.title || bodyPreview || block.rxxId);
|
const title = cleanTaskTitle(block.title || bodyPreview || block.rxxId);
|
||||||
return {
|
return {
|
||||||
taskRef: taskRefForRxx(context.sourceId, context.fileRef, block.rxxId),
|
taskRef: taskRefForRxx(context.sourceId, context.fileRef, block.rxxId),
|
||||||
@@ -236,12 +236,13 @@ function parseRxxTasks(lines, context) {
|
|||||||
depth: rxxDepth(block.rxxId),
|
depth: rxxDepth(block.rxxId),
|
||||||
lineNumber: block.lineNumber,
|
lineNumber: block.lineNumber,
|
||||||
ordinal: index + 1,
|
ordinal: index + 1,
|
||||||
linkCount: countMarkdownLinks(bodyText),
|
linkCount: links.length,
|
||||||
sourceFingerprint: context.fingerprint,
|
sourceFingerprint: context.fingerprint,
|
||||||
parserMode: "rxx",
|
parserMode: "rxx",
|
||||||
headingLevel: block.hashes.length,
|
headingLevel: block.hashes.length,
|
||||||
statusMarker: block.statusMarker,
|
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();
|
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) {
|
function markdownLinksFromBody(body, relativePath) {
|
||||||
|
const text = String(body ?? "");
|
||||||
const links = [];
|
const links = [];
|
||||||
const markdownPattern = /!?\[([^\]]*)\]\(([^)]+)\)/gu;
|
const markdownPattern = /!?\[([^\]]*)\]\(([^)]+)\)/gu;
|
||||||
|
const markdownRanges = [];
|
||||||
let ordinal = 0;
|
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;
|
ordinal += 1;
|
||||||
const label = String(match[1] ?? "").trim();
|
const label = String(match[1] ?? "").trim();
|
||||||
const href = cleanMarkdownHref(match[2]);
|
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;
|
ordinal += 1;
|
||||||
const href = String(match[0] ?? "").replace(/[),.;]+$/u, "");
|
const href = String(match[0] ?? "").replace(/[),.;]+$/u, "");
|
||||||
links.push({
|
links.push({
|
||||||
|
|||||||
@@ -469,7 +469,9 @@ function appendTaskFilterClauses(params, clauses, filters = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function taskFromRow(row) {
|
function taskFromRow(row) {
|
||||||
|
const taskJson = row.task_json && typeof row.task_json === "object" && !Array.isArray(row.task_json) ? row.task_json : {};
|
||||||
return {
|
return {
|
||||||
|
...taskJson,
|
||||||
taskRef: row.task_ref,
|
taskRef: row.task_ref,
|
||||||
projectId: row.project_id,
|
projectId: row.project_id,
|
||||||
sourceId: row.source_id,
|
sourceId: row.source_id,
|
||||||
@@ -484,7 +486,7 @@ function taskFromRow(row) {
|
|||||||
linkCount: row.link_count,
|
linkCount: row.link_count,
|
||||||
sourceFingerprint: row.source_fingerprint,
|
sourceFingerprint: row.source_fingerprint,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
task: row.task_json ?? {}
|
task: taskJson
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ export interface MdtodoTaskRecord {
|
|||||||
sourceFingerprint?: string;
|
sourceFingerprint?: string;
|
||||||
bodyPreview?: string;
|
bodyPreview?: string;
|
||||||
parserMode?: string;
|
parserMode?: string;
|
||||||
|
links?: MdtodoTaskLinkRecord[];
|
||||||
|
task?: {
|
||||||
|
bodyPreview?: string | null;
|
||||||
|
links?: MdtodoTaskLinkRecord[];
|
||||||
|
linkCount?: number | null;
|
||||||
|
title?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +132,8 @@ export interface MdtodoTaskLinkRecord {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
relativePath?: string | null;
|
relativePath?: string | null;
|
||||||
ordinal?: number;
|
ordinal?: number;
|
||||||
|
availability?: string;
|
||||||
|
message?: string;
|
||||||
valuesRedacted?: boolean;
|
valuesRedacted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
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 type { ApiError, ErrorDiagnostic } from "@/types";
|
||||||
import EmptyState from "@/components/common/EmptyState.vue";
|
import EmptyState from "@/components/common/EmptyState.vue";
|
||||||
import LoadingState from "@/components/common/LoadingState.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 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 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 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 fileScopedTasks = computed(() => selectedFileRef.value ? taskData.tasks.value.filter((t) => t.fileRef === selectedFileRef.value) : taskData.tasks.value);
|
||||||
|
|
||||||
const filteredTasks = computed(() => {
|
const filteredTasks = computed(() => {
|
||||||
@@ -92,8 +93,18 @@ const taskStatusCounts = computed(() => fileScopedTasks.value.reduce((acc, task)
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, number>));
|
}, {} as Record<string, number>));
|
||||||
|
|
||||||
const selectedTaskBody = computed(() => taskData.taskDetail.value?.body ?? selectedTask.value?.bodyPreview ?? "");
|
const selectedTaskBody = computed(() => taskData.taskDetail.value?.body ?? selectedTask.value?.bodyPreview ?? selectedTaskProjection.value?.bodyPreview ?? "");
|
||||||
const selectedTaskLinks = computed(() => taskData.taskDetail.value?.links ?? []);
|
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 selectedTaskBodyHtml = computed(() => report.markdownToHtml(selectedTaskBody.value));
|
||||||
const reportPreviewHtml = computed(() => report.markdownToHtml(report.reportPreview.value?.content ?? ""));
|
const reportPreviewHtml = computed(() => report.markdownToHtml(report.reportPreview.value?.content ?? ""));
|
||||||
const selectedFileName = computed(() => selectedFile.value ? fileDisplayName(selectedFile.value) : selectedTask.value?.fileRef || "-");
|
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 selectedSourceCanReindex = computed(() => Boolean(selectedSourceId.value && !taskData.taskLoading.value && !source.sourceReindexLoading.value));
|
||||||
const selectedTaskLabel = computed(() => selectedTask.value ? `${selectedTask.value.taskId || ''} ${selectedTask.value.title || ''}`.trim() : "");
|
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());
|
onMounted(() => void loadPage());
|
||||||
|
|
||||||
watch(selectedSourceId, async (sourceId) => {
|
watch(selectedSourceId, async (sourceId) => {
|
||||||
@@ -545,8 +575,8 @@ function setError(err: unknown): void {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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-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; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e1e7; background: #f9fbfc; padding: 0 10px; }
|
.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 { 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 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; }
|
.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);
|
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> {
|
async function openReportPreview(taskRef: string, link: MdtodoTaskLinkRecord): Promise<MdtodoReportPreviewRecord | null> {
|
||||||
if (!link.linkId || link.kind !== "markdown-report") return null;
|
if (!link.linkId || link.kind !== "markdown-report") return null;
|
||||||
|
if (projectedLinkNeedsRefresh(link)) return openProjectedLinkPlaceholder(link);
|
||||||
reportLoading.value = true;
|
reportLoading.value = true;
|
||||||
reportError.value = null;
|
reportError.value = null;
|
||||||
try {
|
try {
|
||||||
@@ -83,4 +108,3 @@ export function useMdtodoReportPreview() {
|
|||||||
closeFullscreen
|
closeFullscreen
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user