feat(decision-center): add文书工作台 three-panel workspace (issue #51)
- Add DocTypeTree left panel with type grouping (DCSN|GOAL|PLAN|...) and tag grouping - Add DocWorkspace with three-column layout: tree (260px) | list | detail (~50%) - Add DocDetailSidebar with read/edit mode toggle, full markdown display - Add DocDetailHeader with doc-no, title, type, priority, status, evidence links - Add DocEditor for editing title, type, level, status, tags, body - Extract doc-no from title prefix (e.g. DCSN-2025-001) and doc-no:* tag - Support collapsible left/right panels for narrow screens - Add decision-center-workspace-contract-test.ts for doc-no extraction, tree grouping, selection, detail display, edit sync - Add .doc-workspace, .doc-sidebar, .doc-type-tree, .doc-tree-* CSS
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
function source(path: string): string {
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
function includesAll(text: string, snippets: string[]): boolean {
|
||||
return snippets.every((snippet) => text.includes(snippet));
|
||||
}
|
||||
|
||||
export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
||||
const frontend = source("src/components/frontend/src/decision-center.tsx");
|
||||
const css = source("src/components/frontend/public/style.css");
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function extractDocNo(record: any): string",
|
||||
"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",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function DocTreeNode({ record, depth, onSelect, selectedId }: AnyRecord)",
|
||||
"function DocTreeGroup({ group, onSelect, selectedId, defaultOpen }: AnyRecord)",
|
||||
"function TagTreeGroup({ group, onSelect, selectedId, defaultOpen }: AnyRecord)",
|
||||
"function DocTypeTree({ records, onSelect, selectedId, activeGrouping }: AnyRecord)",
|
||||
]),
|
||||
"frontend must implement DocTreeNode, DocTreeGroup, TagTreeGroup, and DocTypeTree components",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function DocDetailSidebar({ record, onSelect, onSave, saving, error, saveMsg, onRaw }: AnyRecord)",
|
||||
"function DocEditor({ form, saving, onChange, onSubmit }: AnyRecord)",
|
||||
"function DocViewer({ record }: AnyRecord)",
|
||||
"function DocDetailHeader({ record, onBack }: AnyRecord)",
|
||||
]),
|
||||
"frontend must implement DocDetailSidebar, DocEditor, DocViewer, and DocDetailHeader",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function DocWorkspace({ records, selectedRecord, onSelect, onSave, saving, saveError, saveMsg, onRaw }: AnyRecord)",
|
||||
"h(DocWorkspace, {",
|
||||
"activeView === \"workspace\"",
|
||||
"decision-tab-workspace",
|
||||
]),
|
||||
"frontend must implement DocWorkspace component and workspace tab",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"mode === \"view\"",
|
||||
"mode === \"edit\"",
|
||||
"setMode(\"view\")",
|
||||
"setMode(\"edit\")",
|
||||
"doc-sidebar-mode-tabs",
|
||||
]),
|
||||
"frontend must implement read/edit mode toggle in DocDetailSidebar",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"selectedRecord?.id",
|
||||
"setState((prev: any) => ({ ...prev, selectedDoc: record",
|
||||
"state.selectedDoc",
|
||||
]),
|
||||
"frontend must track selectedDoc in state and sync on selection",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"await onSave(form)",
|
||||
"recordPayloadFromForm(form)",
|
||||
"setRecordSaveState({ saving: false, error: \"\", message:",
|
||||
"setState((prev: any) => ({ ...prev, selectedDoc: saved",
|
||||
]),
|
||||
"frontend must call onSave, payload conversion, error handling, and sync selectedDoc after save",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
css.includes(".doc-workspace"),
|
||||
"CSS must include .doc-workspace layout",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
css.includes(".doc-sidebar"),
|
||||
"CSS must include .doc-sidebar",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
css.includes("grid-template-columns: minmax(200px, 260px) minmax(0, 1fr) minmax(0, 50%)"),
|
||||
"CSS must implement three-column layout with right sidebar at 50%",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(css, [
|
||||
".doc-type-tree",
|
||||
".doc-tree-group",
|
||||
".doc-tree-node",
|
||||
".doc-list-item",
|
||||
".doc-full-markdown",
|
||||
".doc-editor-form",
|
||||
]),
|
||||
"CSS must include all workspace component styles",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"extractDocNo(record)",
|
||||
"title.match(/^([A-Z]{2,5})[-−]?(\\d+)/u)",
|
||||
"doc-no:([A-Z]{2,5})[-−]?(\\d+)",
|
||||
]),
|
||||
"extractDocNo must extract from title prefix and doc-no tag",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"grouping === \"type\"",
|
||||
"grouping === \"tag\"",
|
||||
"setGrouping(\"type\")",
|
||||
"setGrouping(\"tag\")",
|
||||
]),
|
||||
"workspace must support type and tag grouping toggle",
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"doc-no-extraction-functions",
|
||||
"doc-type-tree-components",
|
||||
"doc-detail-sidebar-components",
|
||||
"doc-workspace-component",
|
||||
"read-edit-mode-toggle",
|
||||
"selected-doc-state-sync",
|
||||
"save-and-sync",
|
||||
"workspace-css-layout",
|
||||
"workspace-css-components",
|
||||
"three-column-layout-50-percent",
|
||||
"doc-no-regex-from-title",
|
||||
"type-tag-grouping-toggle",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(runDecisionCenterWorkspaceContract(), null, 2)}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user