fix: parse decision center document metadata
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
buildDocTypeTree,
|
||||
docTypeLabels,
|
||||
extractDocMetadata,
|
||||
extractDocNo,
|
||||
} from "../src/components/frontend/src/decision-center.tsx";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -14,19 +20,73 @@ function includesAll(text: string, snippets: string[]): boolean {
|
||||
return snippets.every((snippet) => text.includes(snippet));
|
||||
}
|
||||
|
||||
function assertEqual<T>(actual: T, expected: T, message: string, detail: JsonRecord = {}): void {
|
||||
assertCondition(Object.is(actual, expected), message, { ...detail, actual, expected });
|
||||
}
|
||||
|
||||
function groupCount(records: JsonRecord[], type: string): number {
|
||||
return buildDocTypeTree(records).find((group) => group.type === type)?.nodes.length || 0;
|
||||
}
|
||||
|
||||
export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
||||
const frontend = source("src/components/frontend/src/decision-center.tsx");
|
||||
const css = source("src/components/frontend/public/style.css");
|
||||
|
||||
const titleDcsn = {
|
||||
id: "title-dcsn",
|
||||
title: "DC-DCSN-P0-2026-001 决策记录",
|
||||
tags: [],
|
||||
};
|
||||
const tagGoal = {
|
||||
id: "tag-goal",
|
||||
title: "目标文书",
|
||||
tags: ["doc-no:DC-GOAL-P0-2026-002"],
|
||||
};
|
||||
const bodyRprt = {
|
||||
id: "body-rprt",
|
||||
title: "报告正文",
|
||||
body: "DC-RPRT-P2-2026-003\n\n报告正文第一段。",
|
||||
tags: [],
|
||||
};
|
||||
const tagRetr = {
|
||||
id: "tag-retr",
|
||||
title: "复盘文书",
|
||||
tags: ["doc-type:RETR"],
|
||||
};
|
||||
const structuredPlan = {
|
||||
id: "structured-plan",
|
||||
title: "结构化计划文书",
|
||||
docType: "PLAN",
|
||||
priority: "P1",
|
||||
year: 2026,
|
||||
sequence: 4,
|
||||
};
|
||||
|
||||
assertEqual(extractDocNo(titleDcsn), "DC-DCSN-P0-2026-001", "title prefix must extract complete DCSN doc number");
|
||||
assertEqual(extractDocMetadata(titleDcsn).docType, "DCSN", "title prefix must extract DCSN doc type");
|
||||
assertEqual(extractDocNo(tagGoal), "DC-GOAL-P0-2026-002", "doc-no tag must extract complete GOAL doc number");
|
||||
assertEqual(extractDocMetadata(tagGoal).docType, "GOAL", "doc-no tag must extract GOAL doc type");
|
||||
assertEqual(extractDocNo(bodyRprt), "DC-RPRT-P2-2026-003", "body first paragraph must fallback extract complete RPRT doc number");
|
||||
assertEqual(extractDocMetadata(bodyRprt).priority, "P2", "body fallback must extract RPRT priority");
|
||||
assertEqual(extractDocMetadata(tagRetr).docType, "RETR", "doc-type tag must extract RETR doc type");
|
||||
assertEqual(docTypeLabels.RETR, "复盘", "RETR label must be 复盘");
|
||||
assertEqual(extractDocNo(structuredPlan), "DC-PLAN-P1-2026-004", "structured fields must compose complete PLAN doc number");
|
||||
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "DCSN"), 1, "DCSN records must be grouped by parsed type");
|
||||
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "GOAL"), 1, "GOAL records must be grouped by parsed type");
|
||||
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "RPRT"), 1, "RPRT records must be grouped by parsed type");
|
||||
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "RETR"), 1, "RETR records must be grouped by parsed type");
|
||||
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "PLAN"), 1, "PLAN records must be grouped by structured type");
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function extractDocNo(record: any): string",
|
||||
"function buildDocTypeTree(records: any[]): Array<{ type: DocTypeCode",
|
||||
"export function extractDocMetadata(record: any): DocMetadata",
|
||||
"export function extractDocNo(record: any): string",
|
||||
"export function buildDocTypeTree(records: any[]): Array<{ type: DocTypeCode",
|
||||
"function buildTagGroups(records: any[]): Array<{ tag: string",
|
||||
"DOC_TYPE_CODES = [\"DCSN\", \"GOAL\", \"PLAN\", \"RPRT\", \"ACTN\", \"ISSU\", \"RETR\", \"RQST\", \"RESP\", \"MINS\"]",
|
||||
"docTypeLabels: Record<DocTypeCode, string>",
|
||||
]),
|
||||
"frontend must implement doc-no extraction, doc-type tree, and tag grouping",
|
||||
"frontend must implement exported doc metadata extraction, doc-type tree, and tag grouping",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
@@ -118,11 +178,11 @@ export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"extractDocNo(record)",
|
||||
"title.match(/^([A-Z]{2,5})[-−]?(\\d+)/u)",
|
||||
"doc-no:([A-Z]{2,5})[-−]?(\\d+)",
|
||||
"extractDocMetadata(record)",
|
||||
"recordTagValues(record, \"doc-no\")",
|
||||
"firstBodyWindow(record)",
|
||||
]),
|
||||
"extractDocNo must extract from title prefix and doc-no tag",
|
||||
"extractDocNo must use metadata extraction from title, doc-no tag, and body fallback",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
@@ -148,7 +208,7 @@ export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
||||
"workspace-css-layout",
|
||||
"workspace-css-components",
|
||||
"three-column-layout-50-percent",
|
||||
"doc-no-regex-from-title",
|
||||
"doc-metadata-fixtures",
|
||||
"type-tag-grouping-toggle",
|
||||
],
|
||||
};
|
||||
@@ -156,4 +216,4 @@ export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(runDecisionCenterWorkspaceContract(), null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,23 +16,65 @@ const recordLevels = ["all", "G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "no
|
||||
const recordStatuses = ["all", "active", "blocked", "parked", "done"];
|
||||
const diaryWeekdays = ["一", "二", "三", "四", "五", "六", "日"];
|
||||
|
||||
const DOC_TYPE_CODES = ["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"] as const;
|
||||
type DocTypeCode = typeof DOC_TYPE_CODES[number];
|
||||
export const DOC_TYPE_CODES = ["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"] as const;
|
||||
export type DocTypeCode = typeof DOC_TYPE_CODES[number];
|
||||
|
||||
const docTypeLabels: Record<DocTypeCode, string> = {
|
||||
export const docTypeLabels: Record<DocTypeCode, string> = {
|
||||
DCSN: "决策/决议",
|
||||
GOAL: "目标",
|
||||
PLAN: "计划",
|
||||
RPRT: "报告",
|
||||
ACTN: "行动",
|
||||
ISSU: "问题",
|
||||
RETR: "检索",
|
||||
RQST: "请求",
|
||||
RESP: "响应",
|
||||
RETR: "复盘",
|
||||
RQST: "请示",
|
||||
RESP: "批复/答复",
|
||||
MINS: "会议纪要",
|
||||
};
|
||||
|
||||
const docTypeOrder: DocTypeCode[] = ["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"];
|
||||
const docTypeAliases: Record<string, DocTypeCode> = {
|
||||
DECISION: "DCSN",
|
||||
DECISIONS: "DCSN",
|
||||
GOALS: "GOAL",
|
||||
PLANS: "PLAN",
|
||||
REPORT: "RPRT",
|
||||
REPORTS: "RPRT",
|
||||
ACTION: "ACTN",
|
||||
ACTIONS: "ACTN",
|
||||
ISSUE: "ISSU",
|
||||
ISSUES: "ISSU",
|
||||
RETROSPECTIVE: "RETR",
|
||||
RETROSPECTIVES: "RETR",
|
||||
RETRO: "RETR",
|
||||
REQUEST: "RQST",
|
||||
REQUESTS: "RQST",
|
||||
RESPONSE: "RESP",
|
||||
RESPONSES: "RESP",
|
||||
REPLY: "RESP",
|
||||
MINUTES: "MINS",
|
||||
MEETINGMINUTES: "MINS",
|
||||
};
|
||||
const DOC_NO_CORE_PATTERN = "DC[-−–—]([A-Z]{2,5})[-−–—]([A-Z][0-9])[-−–—](\\d{4})[-−–—](\\d{1,6})";
|
||||
const DOC_NO_PATTERN = new RegExp(`\\b${DOC_NO_CORE_PATTERN}\\b`, "iu");
|
||||
const DOC_NO_PREFIX_PATTERN = new RegExp(`^\\s*(?:#+\\s*)?${DOC_NO_CORE_PATTERN}(?=\\s|[::,,.;;/\\\\]|$)`, "iu");
|
||||
|
||||
export interface DocMetadata {
|
||||
docNo: string;
|
||||
docType: DocTypeCode | "";
|
||||
docTypeLabel: string;
|
||||
priority: string;
|
||||
year: string;
|
||||
sequence: string;
|
||||
}
|
||||
|
||||
interface ParsedDocNo {
|
||||
docNo: string;
|
||||
docType: DocTypeCode;
|
||||
priority: string;
|
||||
year: string;
|
||||
sequence: string;
|
||||
}
|
||||
const requirementViews = [
|
||||
{ id: "all", label: "全部需求" },
|
||||
{ id: "external-goal", label: "外部目标" },
|
||||
@@ -224,41 +266,129 @@ function tagsText(value: any): string {
|
||||
return Array.isArray(value) ? value.join(", ") : String(value || "");
|
||||
}
|
||||
|
||||
function extractDocNo(record: any): string {
|
||||
const title = String(record?.title || "");
|
||||
const match = title.match(/^([A-Z]{2,5})[-−]?(\d+)/u);
|
||||
if (match) return `${match[1]}-${match[2]}`;
|
||||
const tags = Array.isArray(record?.tags) ? record.tags : [];
|
||||
for (const tag of tags) {
|
||||
const tagStr = String(tag || "");
|
||||
const m = tagStr.match(/^doc-no:([A-Z]{2,5})[-−]?(\d+)/iu);
|
||||
if (m) return `${m[1]}-${m[2]}`;
|
||||
}
|
||||
return "";
|
||||
function normalizeDocToken(value: any): string {
|
||||
return String(value || "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
function buildDocTypeTree(records: any[]): Array<{ type: DocTypeCode; label: string; nodes: any[] }> {
|
||||
function normalizeDocType(value: any): DocTypeCode | "" {
|
||||
const token = normalizeDocToken(value).replace(/[^A-Z0-9]+/gu, "");
|
||||
if (DOC_TYPE_CODES.includes(token as DocTypeCode)) return token as DocTypeCode;
|
||||
return docTypeAliases[token] || "";
|
||||
}
|
||||
|
||||
function normalizePriority(value: any): string {
|
||||
const match = normalizeDocToken(value).match(/\b([PG][0-3])\b/u);
|
||||
return match?.[1] || "";
|
||||
}
|
||||
|
||||
function normalizeYear(value: any): string {
|
||||
const match = String(value || "").match(/\b(20\d{2}|19\d{2})\b/u);
|
||||
return match?.[1] || "";
|
||||
}
|
||||
|
||||
function normalizeSequence(value: any): string {
|
||||
const match = String(value || "").trim().match(/^\D*(\d{1,6})\D*$/u);
|
||||
if (!match) return "";
|
||||
return match[1].padStart(3, "0");
|
||||
}
|
||||
|
||||
function parseDocNo(value: any): ParsedDocNo | null {
|
||||
const match = String(value || "").match(DOC_NO_PATTERN);
|
||||
if (!match) return null;
|
||||
const docType = normalizeDocType(match[1]);
|
||||
const priority = normalizePriority(match[2]);
|
||||
const year = normalizeYear(match[3]);
|
||||
const sequence = normalizeSequence(match[4]);
|
||||
if (!docType || !priority || !year || !sequence) return null;
|
||||
return {
|
||||
docNo: `DC-${docType}-${priority}-${year}-${sequence}`,
|
||||
docType,
|
||||
priority,
|
||||
year,
|
||||
sequence,
|
||||
};
|
||||
}
|
||||
|
||||
function parseDocNoPrefix(value: any): ParsedDocNo | null {
|
||||
const match = String(value || "").match(DOC_NO_PREFIX_PATTERN);
|
||||
return match ? parseDocNo(match[0]) : null;
|
||||
}
|
||||
|
||||
function firstBodyWindow(record: any): string {
|
||||
const body = recordStringField(record, ["body", "content", "markdown", "summary"]);
|
||||
if (!body) return "";
|
||||
const lines = body.split(/\r?\n/u).slice(0, 8).join("\n");
|
||||
const paragraphs = body.split(/\r?\n\s*\r?\n/u).slice(0, 2).join("\n\n");
|
||||
return `${lines}\n${paragraphs}`.slice(0, 1600);
|
||||
}
|
||||
|
||||
function mergeParsedDocNo(metadata: DocMetadata, parsed: ParsedDocNo | null): void {
|
||||
if (!parsed) return;
|
||||
if (!metadata.docNo) metadata.docNo = parsed.docNo;
|
||||
if (!metadata.docType) metadata.docType = parsed.docType;
|
||||
if (!metadata.priority) metadata.priority = parsed.priority;
|
||||
if (!metadata.year) metadata.year = parsed.year;
|
||||
if (!metadata.sequence) metadata.sequence = parsed.sequence;
|
||||
}
|
||||
|
||||
function completeDocNoFromParts(metadata: DocMetadata): string {
|
||||
return metadata.docType && metadata.priority && metadata.year && metadata.sequence
|
||||
? `DC-${metadata.docType}-${metadata.priority}-${metadata.year}-${metadata.sequence}`
|
||||
: "";
|
||||
}
|
||||
|
||||
export function extractDocMetadata(record: any): DocMetadata {
|
||||
const metadata: DocMetadata = {
|
||||
docNo: "",
|
||||
docType: "",
|
||||
docTypeLabel: "",
|
||||
priority: "",
|
||||
year: "",
|
||||
sequence: "",
|
||||
};
|
||||
|
||||
mergeParsedDocNo(metadata, parseDocNo(recordStringField(record, ["docNo", "documentNo", "documentNumber", "documentId"])));
|
||||
const structuredDocType = normalizeDocType(recordStringField(record, ["docType", "documentType", "documentKind"]));
|
||||
const structuredPriority = normalizePriority(recordStringField(record, ["priority", "docPriority", "documentPriority"]));
|
||||
const structuredYear = normalizeYear(recordStringField(record, ["year", "docYear", "documentYear"]));
|
||||
const structuredSequence = normalizeSequence(recordStringField(record, ["sequence", "seq", "docSequence", "documentSequence"]));
|
||||
if (structuredDocType) metadata.docType = structuredDocType;
|
||||
if (structuredPriority) metadata.priority = structuredPriority;
|
||||
if (structuredYear) metadata.year = structuredYear;
|
||||
if (structuredSequence) metadata.sequence = structuredSequence;
|
||||
|
||||
const docNoFromParts = completeDocNoFromParts(metadata);
|
||||
if (!metadata.docNo && docNoFromParts) metadata.docNo = docNoFromParts;
|
||||
|
||||
mergeParsedDocNo(metadata, parseDocNoPrefix(record?.title));
|
||||
mergeParsedDocNo(metadata, parseDocNo(recordStringField(record, ["title", "name"])));
|
||||
|
||||
for (const tagValue of recordTagValues(record, "doc-no")) mergeParsedDocNo(metadata, parseDocNo(tagValue));
|
||||
if (!metadata.docType) metadata.docType = normalizeDocType(firstRecordTagValue(record, "doc-type"));
|
||||
if (!metadata.priority) metadata.priority = normalizePriority(firstRecordTagValue(record, "doc-priority"));
|
||||
if (!metadata.year) metadata.year = normalizeYear(firstRecordTagValue(record, "doc-year"));
|
||||
if (!metadata.sequence) metadata.sequence = normalizeSequence(firstRecordTagValue(record, "doc-sequence"));
|
||||
|
||||
const tagDocNoFromParts = completeDocNoFromParts(metadata);
|
||||
if (!metadata.docNo && tagDocNoFromParts) metadata.docNo = tagDocNoFromParts;
|
||||
|
||||
mergeParsedDocNo(metadata, parseDocNo(firstBodyWindow(record)));
|
||||
|
||||
const finalDocNoFromParts = completeDocNoFromParts(metadata);
|
||||
if (!metadata.docNo && finalDocNoFromParts) metadata.docNo = finalDocNoFromParts;
|
||||
metadata.docTypeLabel = metadata.docType ? docTypeLabels[metadata.docType] : "";
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export function extractDocNo(record: any): string {
|
||||
return extractDocMetadata(record).docNo;
|
||||
}
|
||||
|
||||
export function buildDocTypeTree(records: any[]): Array<{ type: DocTypeCode; label: string; nodes: any[] }> {
|
||||
const byType = new Map<DocTypeCode, any[]>();
|
||||
for (const code of docTypeOrder) byType.set(code, []);
|
||||
for (const record of records) {
|
||||
const title = String(record?.title || "");
|
||||
let code: DocTypeCode | null = null;
|
||||
const match = title.match(/^([A-Z]{2,5})[-−]?/u);
|
||||
if (match) {
|
||||
const candidate = match[1].toUpperCase() as DocTypeCode;
|
||||
if (DOC_TYPE_CODES.includes(candidate)) code = candidate;
|
||||
}
|
||||
if (!code) {
|
||||
const tags = Array.isArray(record?.tags) ? record.tags : [];
|
||||
for (const tag of tags) {
|
||||
const m = String(tag || "").match(/^doc-type:([A-Z]{2,5})/iu);
|
||||
if (m) {
|
||||
const candidate = m[1].toUpperCase() as DocTypeCode;
|
||||
if (DOC_TYPE_CODES.includes(candidate)) { code = candidate; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!code) code = "DCSN";
|
||||
const code = extractDocMetadata(record).docType || "DCSN";
|
||||
byType.get(code)!.push(record);
|
||||
}
|
||||
return docTypeOrder
|
||||
@@ -293,8 +423,8 @@ function buildTagGroups(records: any[]): Array<{ tag: string; nodes: any[] }> {
|
||||
}
|
||||
|
||||
function DocTreeNode({ record, depth, onSelect, selectedId }: AnyRecord) {
|
||||
const docNo = extractDocNo(record);
|
||||
const label = docNo ? `${docNo} ${record.title || ""}` : (record.title || record.id || "--");
|
||||
const metadata = extractDocMetadata(record);
|
||||
const label = metadata.docNo ? `${metadata.docNo} ${record.title || ""}` : (record.title || record.id || "--");
|
||||
return h("div", { className: `doc-tree-node depth-${Math.min(depth, 3)}` },
|
||||
h("button", {
|
||||
type: "button",
|
||||
@@ -302,7 +432,7 @@ function DocTreeNode({ record, depth, onSelect, selectedId }: AnyRecord) {
|
||||
onClick: () => onSelect(record),
|
||||
"data-testid": `doc-tree-item-${stableTestId(record.id)}`,
|
||||
},
|
||||
h("span", { className: "doc-tree-meta" }, record.type),
|
||||
h("span", { className: "doc-tree-meta" }, metadata.docType || record.type || "--"),
|
||||
h("span", { className: "doc-tree-label" }, shortText(label, 80)),
|
||||
),
|
||||
);
|
||||
@@ -358,16 +488,16 @@ function DocTypeTree({ records, onSelect, selectedId, activeGrouping }: AnyRecor
|
||||
}
|
||||
|
||||
function DocDetailHeader({ record, onBack }: AnyRecord) {
|
||||
const docNo = extractDocNo(record);
|
||||
const metadata = extractDocMetadata(record);
|
||||
return h("div", { className: "doc-detail-header" },
|
||||
onBack ? h("button", { type: "button", className: "ghost-btn compact", onClick: onBack, "data-testid": "doc-detail-back" }, "← 返回") : null,
|
||||
docNo ? h("span", { className: "doc-detail-docno", "data-testid": "doc-detail-docno" }, docNo) : null,
|
||||
metadata.docNo ? h("span", { className: "doc-detail-docno", "data-testid": "doc-detail-docno" }, metadata.docNo) : null,
|
||||
h("h2", { className: "doc-detail-title" }, record.title || "--"),
|
||||
h("div", { className: "doc-detail-meta" },
|
||||
h("span", null, record.type || "--"),
|
||||
h(StatusBadge, { status: levelTone(record.level) }, record.level || "none"),
|
||||
h("span", null, metadata.docType ? `${metadata.docType} ${metadata.docTypeLabel}` : record.type || "--"),
|
||||
h(StatusBadge, { status: levelTone(metadata.priority || record.level) }, metadata.priority || record.level || "none"),
|
||||
h(StatusBadge, { status: statusTone(record.status) }, record.status || "--"),
|
||||
h("span", { className: "muted" }, fmtRecordTime(record.updatedAt)),
|
||||
h("span", { className: "muted" }, `更新 ${fmtRecordTime(record.updatedAt)}`),
|
||||
),
|
||||
record.source ? h("a", { href: record.source, target: "_blank", rel: "noreferrer", className: "doc-detail-source" }, `来源: ${record.source}`) : null,
|
||||
Array.isArray(record.evidenceLinks) && record.evidenceLinks.length > 0
|
||||
@@ -537,7 +667,9 @@ function DocWorkspace({ records, selectedRecord, onSelect, onSave, saving, saveE
|
||||
h("div", { className: "doc-record-list", "data-testid": "doc-record-list" },
|
||||
records.length === 0
|
||||
? h(EmptyState, { title: "暂无记录", text: "通过 CLI 上传文书后会显示在这里。" })
|
||||
: records.map((record: any) => h("button", {
|
||||
: records.map((record: any) => {
|
||||
const metadata = extractDocMetadata(record);
|
||||
return h("button", {
|
||||
key: record.id,
|
||||
type: "button",
|
||||
className: `doc-list-item ${selectedRecord?.id === record.id ? "selected" : ""}`,
|
||||
@@ -545,12 +677,13 @@ function DocWorkspace({ records, selectedRecord, onSelect, onSave, saving, saveE
|
||||
"data-testid": `doc-list-item-${stableTestId(record.id)}`,
|
||||
},
|
||||
h("span", { className: "doc-list-meta" },
|
||||
h("span", null, record.type || "--"),
|
||||
h(StatusBadge, { status: levelTone(record.level) }, record.level || "none"),
|
||||
h("span", null, metadata.docType || record.type || "--"),
|
||||
h(StatusBadge, { status: levelTone(metadata.priority || record.level) }, metadata.priority || record.level || "none"),
|
||||
),
|
||||
h("span", { className: "doc-list-title" }, shortText(record.title || "--", 60)),
|
||||
h("span", { className: "doc-list-title" }, shortText(metadata.docNo ? `${metadata.docNo} ${record.title || ""}` : record.title || "--", 60)),
|
||||
h("span", { className: "doc-list-summary" }, shortText(record.summary || record.body, 100)),
|
||||
)),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
h("div", { className: `doc-workspace-right ${rightOpen ? "open" : "collapsed"}` },
|
||||
|
||||
Reference in New Issue
Block a user