mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
feat: add screen flow view for flow diagrams
This commit is contained in:
parent
3e5f0a5b5c
commit
54a4287360
13 changed files with 736 additions and 55 deletions
|
|
@ -93,7 +93,8 @@ Current Color Scheme application:
|
|||
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
|
||||
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` matching Domain kind rows |
|
||||
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
|
||||
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> matching Domain kind rows |
|
||||
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
|
||||
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ kind: screen_communication
|
|||
| `id` | yes | Unique model id |
|
||||
| `name` | yes | Human-readable name |
|
||||
| `kind` | yes | `screen_communication` for the MVP |
|
||||
| `flow_view` | no | `detail` (default) or `screen` |
|
||||
|
||||
|
||||
## Domain Sources / Domains
|
||||
|
|
@ -89,6 +90,13 @@ Supported MVP object kinds:
|
|||
| `store` | `lin-cyl` | `store` |
|
||||
| `datastore` | `lin-cyl` | `store` |
|
||||
| `external` | `rect` | `external` |
|
||||
| `actor` | `rect` | `external` |
|
||||
| `user` | `rect` | `external` |
|
||||
| `message` | `rect` | `context` |
|
||||
| `data` | `rect` | `process` |
|
||||
| `api` | `rect` | `process` |
|
||||
| `service` | `rect` | `process` |
|
||||
| `handler` | `rect` | `process` |
|
||||
| unknown values | `rect` | fallback |
|
||||
|
||||
`Objects.ref` may point to `screen`, `app_process`, `data_object`, `dfd_object`, or another model asset. Unknown object kinds should not block rendering.
|
||||
|
|
@ -109,9 +117,17 @@ Mermaid edge labels are assembled compactly from `trigger`, `kind`, and `data`.
|
|||
|
||||
## Rendering
|
||||
|
||||
The MVP uses Internal Detail View only and renders the raw Objects / Flows graph without projection, folding, or view selectors.
|
||||
Flow Diagram supports two MVP view modes.
|
||||
|
||||
Surface View, Communication View, projection, context/process/store folding, transition coverage, and automatic generation from screen or app_process models are future work.
|
||||
`detail` is the default view and renders the raw Objects / Flows graph. It keeps internal processes, contexts, stores, external systems, and all declared flow edges visible.
|
||||
|
||||
`screen` is a Screen Flow projection. It keeps user-visible objects with kind `screen`, `external`, `actor`, `user`, `context`, or `message`, and folds internal objects such as `work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, and `handler`. Edges are projected as reachability between visible objects. If a diagram has only folded/internal objects, rendering falls back to the Detail view so the graph is not empty.
|
||||
|
||||
Screen Flow edge labels are taken from the incoming flow to the visible target, using this priority: `condition`, then `trigger`, then `kind`. When multiple projected paths produce the same visible source/target pair, labels are merged with ` / `.
|
||||
|
||||
Screen Flow is a derived rendering projection. It does not change the source Markdown, does not create data nodes from `Flows.data`, and does not replace the detailed communication model.
|
||||
|
||||
Toolbar switching is not part of the MVP; use `flow_view` frontmatter to choose the default view for a file.
|
||||
|
||||
## AI Generation Notes
|
||||
|
||||
|
|
@ -120,4 +136,5 @@ Surface View, Communication View, projection, context/process/store folding, tra
|
|||
- Keep table headers exactly as documented.
|
||||
- Use local `Objects.id` values in `Flows.from` and `Flows.to`.
|
||||
- Put edge semantics in `Flows.kind`, user/system events in `Flows.trigger`, payloads or data references in `Flows.data`, and guards in `Flows.condition`.
|
||||
- Use `flow_view: screen` only when the same detailed graph should default to Screen Flow projection.
|
||||
- Do not add generated state matrices, folding rules, or automatic screen/process derivations.
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ ref が解決できない場合、または対象ファイルが `type: color_sc
|
|||
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
|
||||
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` に対応する Domain kind rows |
|
||||
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
|
||||
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> に対応する Domain kind rows |
|
||||
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
|
||||
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ kind: screen_communication
|
|||
| `id` | yes | 一意な model id |
|
||||
| `name` | yes | 表示名 |
|
||||
| `kind` | yes | MVPでは `screen_communication` |
|
||||
| `flow_view` | no | `detail` (default) または `screen` |
|
||||
|
||||
|
||||
## Domain Sources / Domains
|
||||
|
|
@ -89,6 +90,13 @@ MVPで対応する object kind:
|
|||
| `store` | `lin-cyl` | `store` |
|
||||
| `datastore` | `lin-cyl` | `store` |
|
||||
| `external` | `rect` | `external` |
|
||||
| `actor` | `rect` | `external` |
|
||||
| `user` | `rect` | `external` |
|
||||
| `message` | `rect` | `context` |
|
||||
| `data` | `rect` | `process` |
|
||||
| `api` | `rect` | `process` |
|
||||
| `service` | `rect` | `process` |
|
||||
| `handler` | `rect` | `process` |
|
||||
| unknown values | `rect` | fallback |
|
||||
|
||||
`Objects.ref` は `screen`, `app_process`, `data_object`, `dfd_object` または他の model asset を参照できます。未知の object kind はレンダリングを止めません。
|
||||
|
|
@ -109,9 +117,17 @@ Mermaid edge label は `trigger`、`kind`、`data` から compact に組み立
|
|||
|
||||
## Rendering
|
||||
|
||||
MVPでは Internal Detail View のみを使います。Objects / Flows の raw graph を表示し、projection、folding、view selector は実装しません。
|
||||
Flow Diagram は MVP として2つの view mode を持ちます。
|
||||
|
||||
Surface View、Communication View、projection、context/process/store folding、transition coverage、screen や app_process からの自動生成は将来対応です。
|
||||
`detail` は既定の view です。Objects / Flows の raw graph を表示し、内部 process、context、store、external system、宣言された flow edge をそのまま表示します。
|
||||
|
||||
`screen` は Screen Flow projection です。`screen`, `external`, `actor`, `user`, `context`, `message` のような user-visible object を残し、`work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, `handler` などの内部 object を折りたたみます。Edge は表示対象 object 間の到達関係として投影されます。折りたたみ対象の内部 object しか無い diagram では、空表示にせず Detail view に fallback します。
|
||||
|
||||
Screen Flow の edge label は、表示対象 target へ入る flow から `condition`、`trigger`、`kind` の優先順で作ります。同じ visible source / target に複数 path が畳み込まれる場合、label は ` / ` で結合されます。
|
||||
|
||||
Screen Flow は派生レンダリング projection です。source Markdown は変更せず、`Flows.data` から data node を作らず、詳細な communication model を置き換えるものではありません。
|
||||
|
||||
Toolbar 切り替えは MVP には含めません。ファイルの既定 view を選ぶ場合は frontmatter の `flow_view` を使います。
|
||||
|
||||
## AI生成時の注意
|
||||
|
||||
|
|
@ -120,4 +136,5 @@ Surface View、Communication View、projection、context/process/store folding
|
|||
- テーブルヘッダーを仕様通りに保つ。
|
||||
- `Flows.from` と `Flows.to` にはローカル `Objects.id` を使う。
|
||||
- edge semantics は `Flows.kind`、user/system event は `Flows.trigger`、payload や data reference は `Flows.data`、guard は `Flows.condition` に書く。
|
||||
- 同じ詳細 graph を Screen Flow projection で既定表示したい場合だけ `flow_view: screen` を使う。
|
||||
- state matrix、folding rule、自動生成された screen/process derivation は追加しない。
|
||||
|
|
|
|||
224
main.js
224
main.js
|
|
@ -3062,6 +3062,7 @@ function localizeDiagnosticMessage(message, language) {
|
|||
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `\u4E88\u7D04\u6E08\u307F kind "${kind}" \u304C\u4F7F\u308F\u308C\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} \u3067\u306F ${renderer} renderer \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
||||
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" \u306F\u4E0D\u660E\u3067\u3059\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
||||
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view \u306F "detail" \u307E\u305F\u306F "screen" \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'],
|
||||
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" \u306F\u901A\u5E38\u3068\u7570\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`],
|
||||
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" \u306F\u81EA\u5DF1\u30EB\u30FC\u30D7\u3067\u3059\u3002`],
|
||||
[/^Domain id is required\.$/, "Domain \u306E id \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
||||
|
|
@ -11721,6 +11722,13 @@ var DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"];
|
|||
var LEGACY_OBJECT_HEADERS = ["ref", "notes"];
|
||||
var FLOW_OBJECT_KINDS = /* @__PURE__ */ new Set([
|
||||
"screen",
|
||||
"actor",
|
||||
"user",
|
||||
"message",
|
||||
"data",
|
||||
"api",
|
||||
"service",
|
||||
"handler",
|
||||
"process",
|
||||
"app_process",
|
||||
"context",
|
||||
|
|
@ -11765,6 +11773,7 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
||||
const level = typeof frontmatter.level === "string" || typeof frontmatter.level === "number" ? String(frontmatter.level).trim() : void 0;
|
||||
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim() ? frontmatter.kind.trim() : options.defaultKind;
|
||||
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
|
||||
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
|
||||
const isAcceptedType = rawType === options.type || options.type === "flow_diagram" && rawType === "flow-diagram";
|
||||
if (!isAcceptedType) {
|
||||
|
|
@ -11779,6 +11788,9 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
|
||||
warnings.push(createWarning8(path2, "kind", 'expected kind "screen_communication"'));
|
||||
}
|
||||
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
|
||||
warnings.push(createWarning8(path2, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
|
||||
}
|
||||
const objectsTable = parseDfdObjectsTable(sections.Objects, path2, {
|
||||
schema: options.schema,
|
||||
allowLegacyObjects: options.allowLegacyObjects,
|
||||
|
|
@ -11898,6 +11910,7 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
name: name || fallbackTitle,
|
||||
kind: "screen_communication",
|
||||
description: joinSectionLines2(sections.Summary),
|
||||
flowView,
|
||||
domainSources: domainSourcesTable.rows,
|
||||
domains: domainsTable.rows,
|
||||
objectRefs,
|
||||
|
|
@ -11934,6 +11947,12 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
warnings
|
||||
};
|
||||
}
|
||||
function parseFlowDiagramViewMode(value) {
|
||||
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
|
||||
}
|
||||
function isUnknownFlowDiagramViewMode(value) {
|
||||
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
|
||||
}
|
||||
function getFileStem4(path2) {
|
||||
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
@ -15023,6 +15042,7 @@ function createShallowModel(path2, fileType, frontmatter) {
|
|||
id,
|
||||
name,
|
||||
kind: "screen_communication",
|
||||
flowView: "detail",
|
||||
domainSources: [],
|
||||
domains: [],
|
||||
objectRefs: [],
|
||||
|
|
@ -18731,22 +18751,23 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
|
||||
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
||||
});
|
||||
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram);
|
||||
if (!options?.hideDetails) {
|
||||
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
|
||||
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
|
||||
if (domainDetails) {
|
||||
shell3.root.appendChild(domainDetails);
|
||||
}
|
||||
shell3.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram) ? buildFlowDiagramHoverMetadata(diagram, sourcePath) : null;
|
||||
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram) ? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath) : null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
renderedDiagram,
|
||||
sourcePath
|
||||
);
|
||||
const ready = renderMermaidSourceIntoShell(shell3, {
|
||||
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
||||
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
|
||||
renderIdPrefix: "model_weave_dfd",
|
||||
fitVerticalAlign: options?.fitVerticalAlign,
|
||||
viewportState: options?.viewportState,
|
||||
|
|
@ -18775,7 +18796,7 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext,
|
||||
openLinkText: isFlowDiagramModel(diagram.diagram) ? (target, event) => {
|
||||
const linktext = getFlowDiagramOpenLinkText(target);
|
||||
|
|
@ -19010,7 +19031,7 @@ function buildDfdMermaidInteractionTargets(diagram, sourcePath) {
|
|||
}
|
||||
function buildDfdMermaidSource(diagram, colorScheme) {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return buildFlowDiagramMermaidSource(diagram, colorScheme);
|
||||
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram), colorScheme);
|
||||
}
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines = ["flowchart LR"];
|
||||
|
|
@ -19081,21 +19102,146 @@ function buildDfdMermaidSource(diagram, colorScheme) {
|
|||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function getFlowDiagramRenderedDiagram(diagram) {
|
||||
if (!isFlowDiagramModel(diagram.diagram) || resolveFlowDiagramViewMode(diagram.diagram) !== "screen") {
|
||||
return diagram;
|
||||
}
|
||||
return buildFlowDiagramScreenFlowProjection(diagram);
|
||||
}
|
||||
function resolveFlowDiagramViewMode(diagram, toolbarOverride) {
|
||||
return toolbarOverride ?? diagram.flowView ?? "detail";
|
||||
}
|
||||
function buildFlowDiagramScreenFlowProjection(diagram) {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return diagram;
|
||||
}
|
||||
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
|
||||
if (visibleNodes.length === 0) {
|
||||
return diagram;
|
||||
}
|
||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const outgoing = /* @__PURE__ */ new Map();
|
||||
for (const edge of diagram.edges) {
|
||||
const edges = outgoing.get(edge.source) ?? [];
|
||||
edges.push(edge);
|
||||
outgoing.set(edge.source, edges);
|
||||
}
|
||||
const projectedByPair = /* @__PURE__ */ new Map();
|
||||
for (const sourceNode of visibleNodes) {
|
||||
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
|
||||
edge,
|
||||
visitedEdges: /* @__PURE__ */ new Set([getProjectionEdgeVisitKey(edge)])
|
||||
}));
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
const targetId = current.edge.target;
|
||||
if (visibleIds.has(targetId)) {
|
||||
if (targetId !== sourceNode.id) {
|
||||
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const nextEdge of outgoing.get(targetId) ?? []) {
|
||||
const visitKey = getProjectionEdgeVisitKey(nextEdge);
|
||||
if (current.visitedEdges.has(visitKey)) {
|
||||
continue;
|
||||
}
|
||||
const nextVisitedEdges = new Set(current.visitedEdges);
|
||||
nextVisitedEdges.add(visitKey);
|
||||
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...diagram,
|
||||
diagram: {
|
||||
...diagram.diagram,
|
||||
flowView: "detail"
|
||||
},
|
||||
nodes: visibleNodes,
|
||||
edges: [...projectedByPair.values()]
|
||||
};
|
||||
}
|
||||
function isScreenFlowVisibleNode(node) {
|
||||
switch (node.kind) {
|
||||
case "screen":
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "context":
|
||||
case "message":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function addProjectedScreenFlowEdge(projectedByPair, source, target, incomingEdge) {
|
||||
const key = `${source}->${target}`;
|
||||
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
|
||||
const existing = projectedByPair.get(key);
|
||||
if (!existing) {
|
||||
projectedByPair.set(key, {
|
||||
id: `screen_flow:${source}->${target}`,
|
||||
source,
|
||||
target,
|
||||
kind: "flow",
|
||||
label,
|
||||
metadata: {
|
||||
...incomingEdge.metadata,
|
||||
projected: true,
|
||||
sourceFlowId: incomingEdge.id,
|
||||
dataRaw: void 0
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
existing.label = mergeScreenFlowLabels(existing.label, label);
|
||||
}
|
||||
function buildScreenFlowProjectionEdgeLabel(edge) {
|
||||
return getStringMetadata(edge.metadata, "condition")?.trim() || getStringMetadata(edge.metadata, "trigger")?.trim() || getStringMetadata(edge.metadata, "flowKind")?.trim() || void 0;
|
||||
}
|
||||
function mergeScreenFlowLabels(left, right) {
|
||||
const values = [
|
||||
...(left ?? "").split(" / "),
|
||||
...(right ?? "").split(" / ")
|
||||
].map((value) => value.trim()).filter(Boolean);
|
||||
const unique = [...new Set(values)];
|
||||
if (unique.length === 0) {
|
||||
return void 0;
|
||||
}
|
||||
const merged = unique.join(" / ");
|
||||
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
|
||||
}
|
||||
function getProjectionEdgeVisitKey(edge) {
|
||||
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
|
||||
}
|
||||
function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines = [
|
||||
"flowchart LR",
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
];
|
||||
const lines = ["flowchart LR"];
|
||||
const colorClasses = /* @__PURE__ */ new Map();
|
||||
if (!colorScheme) {
|
||||
lines.push(
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
);
|
||||
}
|
||||
const nodeIds = /* @__PURE__ */ new Map();
|
||||
const domainStyles = [];
|
||||
const flowDomains = getFlowDiagramDomains(diagram);
|
||||
|
|
@ -19126,11 +19272,12 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
|||
nodeIds,
|
||||
1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
for (const node of ungroupedNodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
|
||||
}
|
||||
for (const edge of diagram.edges) {
|
||||
const from = nodeIds.get(edge.source);
|
||||
|
|
@ -19145,12 +19292,18 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
|||
lines.push(` ${from} --> ${to}`);
|
||||
}
|
||||
}
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, domainStyles) {
|
||||
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, colorClasses, domainStyles) {
|
||||
const childLines = [];
|
||||
for (const child of domainNode.children) {
|
||||
appendFlowDiagramDomainSubgraph(
|
||||
|
|
@ -19160,6 +19313,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
nodeIds,
|
||||
depth + 1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
|
@ -19172,7 +19326,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
|
||||
lines.push(...childLines);
|
||||
for (const node of nodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
|
||||
}
|
||||
lines.push(`${indent}end`);
|
||||
if (colorScheme) {
|
||||
|
|
@ -19184,10 +19338,10 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
}
|
||||
return true;
|
||||
}
|
||||
function appendFlowDiagramNode(lines, node, nodeIds, depth) {
|
||||
function appendFlowDiagramNode(lines, node, nodeIds, depth, colorScheme, colorClasses) {
|
||||
const mermaidId = toMermaidNodeId(node.id);
|
||||
const shape = toFlowDiagramMermaidShape(node.kind);
|
||||
const className = toFlowDiagramClassName(node.kind);
|
||||
const className = colorScheme ? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses) : toFlowDiagramClassName(node.kind);
|
||||
const indent = " ".repeat(depth);
|
||||
nodeIds.set(node.id, mermaidId);
|
||||
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel2(node.label ?? node.ref ?? node.id)}" }`);
|
||||
|
|
@ -19217,7 +19371,7 @@ function getFlowDomainColorKind(domain) {
|
|||
}
|
||||
function getDfdMermaidColorSchemeTargets(diagram) {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) ? ["domain"] : [];
|
||||
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
|
||||
}
|
||||
if (isDfdDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0 ? ["dfd", "domain"] : ["dfd"];
|
||||
|
|
@ -19239,12 +19393,31 @@ function toFlowDiagramMermaidShape(kind) {
|
|||
case "app_process":
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "message":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
case "external":
|
||||
case "unknown":
|
||||
default:
|
||||
return "rect";
|
||||
}
|
||||
}
|
||||
function registerFlowDiagramColorClass(kind, colorScheme, colorClasses) {
|
||||
const className = toFlowDiagramColorClassName(kind);
|
||||
colorClasses?.set(
|
||||
className,
|
||||
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : void 0)
|
||||
);
|
||||
return className;
|
||||
}
|
||||
function toFlowDiagramColorClassName(kind) {
|
||||
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
|
||||
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
|
||||
}
|
||||
function toFlowDiagramClassName(kind) {
|
||||
switch (kind) {
|
||||
case "screen":
|
||||
|
|
@ -19255,11 +19428,18 @@ function toFlowDiagramClassName(kind) {
|
|||
return "store";
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "message":
|
||||
return "context";
|
||||
case "process":
|
||||
case "app_process":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
return "process";
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
return "external";
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ The samples are organized by format and purpose rather than by historical versio
|
|||
- [relations/REL-WMS-CORE.md](relations/REL-WMS-CORE.md)
|
||||
- [domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md](domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md)
|
||||
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md)
|
||||
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md)
|
||||
- [color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md](color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md)
|
||||
|
|
|
|||
53
samples/flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md
Normal file
53
samples/flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
type: flow_diagram
|
||||
id: FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW
|
||||
name: Sample Checkout Screen Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
tags:
|
||||
- Sample
|
||||
- FlowDiagram
|
||||
- DomainSources
|
||||
---
|
||||
|
||||
# Sample Checkout Screen Flow
|
||||
|
||||
## Summary
|
||||
|
||||
Checkout screen submitから、内部処理を折りたたんだScreen Flow projectionを確認するFlow Diagramサンプルです。
|
||||
|
||||
このサンプルでは、同じ詳細Objects / Flowsから、user / screen / external / message中心の簡略表示、Domain Sources、parent-child Domain group、投影edge labelを確認します。
|
||||
|
||||
## Domain Sources
|
||||
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
| [[DOMAINS-SAMPLE-CHECKOUT-FLOW]] | Sample Domains for Flow Diagram group rendering |
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| user | Customer | external | | actor_area | User who submits the checkout form |
|
||||
| checkout_screen | Checkout Screen | screen | | frontend_area | Checkout screen |
|
||||
| submit_action | Submit Order Action | work_object | | frontend_area | UI action on the checkout screen |
|
||||
| checkout_process | Checkout Process | app_process | | application_area | Server-side checkout handling |
|
||||
| session_store | Session Store | session | | data_area | Temporary checkout session |
|
||||
| order_store | Order Store | datastore | | data_area | Persisted order data |
|
||||
| payment_gateway | Payment Gateway | external | | external_area | External payment service |
|
||||
| completion_screen | Completion Screen | screen | | frontend_area | Final screen after successful checkout |
|
||||
| error_message | Error Message | context | | frontend_area | Validation or payment error display |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | user | checkout_screen | event | open | Checkout page request | | User opens checkout screen |
|
||||
| F02 | checkout_screen | submit_action | event | click submit | Checkout form values | | UI submit action |
|
||||
| F03 | submit_action | checkout_process | command | submit | DATA-CHECKOUT-SUBMIT-REQUEST | | Submit request from screen to process |
|
||||
| F04 | checkout_process | session_store | read | load session | Checkout session | | Plain text data label |
|
||||
| F05 | checkout_process | payment_gateway | call | authorize payment | DATA-PAYMENT-AUTHORIZATION-REQUEST | payment required | External payment authorization |
|
||||
| F06 | payment_gateway | checkout_process | response | authorization result | DATA-PAYMENT-AUTHORIZATION-RESULT | | Payment result response |
|
||||
| F07 | checkout_process | order_store | command | save order | DATA-ORDER-CREATE-COMMAND | authorized | Persist order |
|
||||
| F08 | checkout_process | completion_screen | navigation | show complete | DATA-CHECKOUT-RESULT | success | Navigate to completion screen |
|
||||
| F09 | checkout_process | error_message | event | show error | Checkout error | failed | Error branch |
|
||||
|
|
@ -1522,6 +1522,7 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
|
|||
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `予約済み kind "${kind}" が使われています。`],
|
||||
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} では ${renderer} renderer はサポートされていません。format の既定 renderer を使います。`],
|
||||
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" は不明です。format の既定 renderer を使います。`],
|
||||
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view は "detail" または "screen" を指定してください。'],
|
||||
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" は通常と異なる可能性があります。`],
|
||||
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" は自己ループです。`],
|
||||
[/^Domain id is required\.$/, "Domain の id が必要です。"],
|
||||
|
|
|
|||
|
|
@ -943,6 +943,7 @@ function createShallowModel(
|
|||
id,
|
||||
name,
|
||||
kind: "screen_communication",
|
||||
flowView: "detail",
|
||||
domainSources: [],
|
||||
domains: [],
|
||||
objectRefs: [],
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ const DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"
|
|||
const LEGACY_OBJECT_HEADERS = ["ref", "notes"];
|
||||
const FLOW_OBJECT_KINDS = new Set<string>([
|
||||
"screen",
|
||||
"actor",
|
||||
"user",
|
||||
"message",
|
||||
"data",
|
||||
"api",
|
||||
"service",
|
||||
"handler",
|
||||
"process",
|
||||
"app_process",
|
||||
"context",
|
||||
|
|
@ -103,6 +110,7 @@ function parseDfdLikeDiagramFile(
|
|||
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim()
|
||||
? frontmatter.kind.trim()
|
||||
: options.defaultKind;
|
||||
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
|
||||
|
||||
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
|
||||
const isAcceptedType = rawType === options.type ||
|
||||
|
|
@ -119,6 +127,9 @@ function parseDfdLikeDiagramFile(
|
|||
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
|
||||
warnings.push(createWarning(path, "kind", 'expected kind "screen_communication"'));
|
||||
}
|
||||
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
|
||||
warnings.push(createWarning(path, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
|
||||
}
|
||||
|
||||
const objectsTable = parseDfdObjectsTable(sections.Objects, path, {
|
||||
schema: options.schema,
|
||||
|
|
@ -248,6 +259,7 @@ function parseDfdLikeDiagramFile(
|
|||
name: name || fallbackTitle,
|
||||
kind: "screen_communication",
|
||||
description: joinSectionLines(sections.Summary),
|
||||
flowView,
|
||||
domainSources: domainSourcesTable.rows,
|
||||
domains: domainsTable.rows,
|
||||
objectRefs,
|
||||
|
|
@ -286,6 +298,14 @@ function parseDfdLikeDiagramFile(
|
|||
};
|
||||
}
|
||||
|
||||
function parseFlowDiagramViewMode(value: unknown): "detail" | "screen" {
|
||||
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
|
||||
}
|
||||
|
||||
function isUnknownFlowDiagramViewMode(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
|
||||
}
|
||||
|
||||
function getFileStem(path: string): string {
|
||||
return path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type {
|
|||
DiagramEdge,
|
||||
DfdDiagramModel,
|
||||
DomainEntry,
|
||||
FlowDiagramModel,
|
||||
FlowDiagramViewMode,
|
||||
ResolvedColorScheme,
|
||||
ResolvedColorStyle,
|
||||
ResolvedDiagram
|
||||
|
|
@ -106,26 +108,28 @@ export function renderDfdMermaidDiagram(
|
|||
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
||||
});
|
||||
|
||||
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram);
|
||||
|
||||
if (!options?.hideDetails) {
|
||||
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
|
||||
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
|
||||
if (domainDetails) {
|
||||
shell.root.appendChild(domainDetails);
|
||||
}
|
||||
shell.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram)
|
||||
? buildFlowDiagramHoverMetadata(diagram, sourcePath)
|
||||
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram)
|
||||
? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath)
|
||||
: null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
renderedDiagram,
|
||||
sourcePath
|
||||
);
|
||||
|
||||
const ready = renderMermaidSourceIntoShell(shell, {
|
||||
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
||||
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
|
||||
renderIdPrefix: "model_weave_dfd",
|
||||
fitVerticalAlign: options?.fitVerticalAlign,
|
||||
viewportState: options?.viewportState,
|
||||
|
|
@ -155,7 +159,7 @@ export function renderDfdMermaidDiagram(
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label
|
||||
? `${target.label} (${target.targetType ?? "model"})`
|
||||
: target.linktext,
|
||||
|
|
@ -440,7 +444,7 @@ export function buildDfdMermaidSource(
|
|||
colorScheme?: ResolvedColorScheme
|
||||
): string {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return buildFlowDiagramMermaidSource(diagram, colorScheme);
|
||||
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram), colorScheme);
|
||||
}
|
||||
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
|
|
@ -520,6 +524,13 @@ export function buildDfdMermaidSource(
|
|||
}
|
||||
}
|
||||
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
|
|
@ -528,19 +539,168 @@ export function buildDfdMermaidSource(
|
|||
}
|
||||
|
||||
|
||||
function getFlowDiagramRenderedDiagram(diagram: ResolvedDiagram): ResolvedDiagram {
|
||||
if (!isFlowDiagramModel(diagram.diagram) || resolveFlowDiagramViewMode(diagram.diagram) !== "screen") {
|
||||
return diagram;
|
||||
}
|
||||
return buildFlowDiagramScreenFlowProjection(diagram);
|
||||
}
|
||||
|
||||
export function resolveFlowDiagramViewMode(
|
||||
diagram: FlowDiagramModel,
|
||||
toolbarOverride?: FlowDiagramViewMode
|
||||
): FlowDiagramViewMode {
|
||||
return toolbarOverride ?? diagram.flowView ?? "detail";
|
||||
}
|
||||
|
||||
export function buildFlowDiagramScreenFlowProjection(diagram: ResolvedDiagram): ResolvedDiagram {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return diagram;
|
||||
}
|
||||
|
||||
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
|
||||
if (visibleNodes.length === 0) {
|
||||
return diagram;
|
||||
}
|
||||
|
||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const outgoing = new Map<string, DiagramEdge[]>();
|
||||
for (const edge of diagram.edges) {
|
||||
const edges = outgoing.get(edge.source) ?? [];
|
||||
edges.push(edge);
|
||||
outgoing.set(edge.source, edges);
|
||||
}
|
||||
|
||||
const projectedByPair = new Map<string, DiagramEdge>();
|
||||
for (const sourceNode of visibleNodes) {
|
||||
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
|
||||
edge,
|
||||
visitedEdges: new Set<string>([getProjectionEdgeVisitKey(edge)])
|
||||
}));
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetId = current.edge.target;
|
||||
if (visibleIds.has(targetId)) {
|
||||
if (targetId !== sourceNode.id) {
|
||||
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const nextEdge of outgoing.get(targetId) ?? []) {
|
||||
const visitKey = getProjectionEdgeVisitKey(nextEdge);
|
||||
if (current.visitedEdges.has(visitKey)) {
|
||||
continue;
|
||||
}
|
||||
const nextVisitedEdges = new Set(current.visitedEdges);
|
||||
nextVisitedEdges.add(visitKey);
|
||||
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
diagram: {
|
||||
...diagram.diagram,
|
||||
flowView: "detail"
|
||||
},
|
||||
nodes: visibleNodes,
|
||||
edges: [...projectedByPair.values()]
|
||||
};
|
||||
}
|
||||
|
||||
function isScreenFlowVisibleNode(node: DiagramNode): boolean {
|
||||
switch (node.kind) {
|
||||
case "screen":
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "context":
|
||||
case "message":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function addProjectedScreenFlowEdge(
|
||||
projectedByPair: Map<string, DiagramEdge>,
|
||||
source: string,
|
||||
target: string,
|
||||
incomingEdge: DiagramEdge
|
||||
): void {
|
||||
const key = `${source}->${target}`;
|
||||
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
|
||||
const existing = projectedByPair.get(key);
|
||||
if (!existing) {
|
||||
projectedByPair.set(key, {
|
||||
id: `screen_flow:${source}->${target}`,
|
||||
source,
|
||||
target,
|
||||
kind: "flow",
|
||||
label,
|
||||
metadata: {
|
||||
...incomingEdge.metadata,
|
||||
projected: true,
|
||||
sourceFlowId: incomingEdge.id,
|
||||
dataRaw: undefined
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
existing.label = mergeScreenFlowLabels(existing.label, label);
|
||||
}
|
||||
|
||||
function buildScreenFlowProjectionEdgeLabel(edge: DiagramEdge): string | undefined {
|
||||
return getStringMetadata(edge.metadata, "condition")?.trim() ||
|
||||
getStringMetadata(edge.metadata, "trigger")?.trim() ||
|
||||
getStringMetadata(edge.metadata, "flowKind")?.trim() ||
|
||||
undefined;
|
||||
}
|
||||
|
||||
function mergeScreenFlowLabels(left: string | undefined, right: string | undefined): string | undefined {
|
||||
const values = [
|
||||
...(left ?? "").split(" / "),
|
||||
...(right ?? "").split(" / ")
|
||||
]
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const unique = [...new Set(values)];
|
||||
if (unique.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const merged = unique.join(" / ");
|
||||
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
|
||||
}
|
||||
|
||||
function getProjectionEdgeVisitKey(edge: DiagramEdge): string {
|
||||
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
|
||||
}
|
||||
|
||||
|
||||
function buildFlowDiagramMermaidSource(
|
||||
diagram: ResolvedDiagram,
|
||||
colorScheme?: ResolvedColorScheme
|
||||
): string {
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines: string[] = [
|
||||
"flowchart LR",
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
];
|
||||
const lines: string[] = ["flowchart LR"];
|
||||
const colorClasses = new Map<string, ResolvedColorStyle>();
|
||||
if (!colorScheme) {
|
||||
lines.push(
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
);
|
||||
}
|
||||
const nodeIds = new Map<string, string>();
|
||||
const domainStyles: string[] = [];
|
||||
const flowDomains = getFlowDiagramDomains(diagram);
|
||||
|
|
@ -573,12 +733,13 @@ function buildFlowDiagramMermaidSource(
|
|||
nodeIds,
|
||||
1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
||||
for (const node of ungroupedNodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
|
||||
}
|
||||
|
||||
for (const edge of diagram.edges) {
|
||||
|
|
@ -596,6 +757,13 @@ function buildFlowDiagramMermaidSource(
|
|||
}
|
||||
}
|
||||
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
|
|
@ -610,6 +778,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
nodeIds: Map<string, string>,
|
||||
depth: number,
|
||||
colorScheme: ResolvedColorScheme | undefined,
|
||||
colorClasses: Map<string, ResolvedColorStyle>,
|
||||
domainStyles: string[]
|
||||
): boolean {
|
||||
const childLines: string[] = [];
|
||||
|
|
@ -621,6 +790,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
nodeIds,
|
||||
depth + 1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
|
@ -635,7 +805,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
|
||||
lines.push(...childLines);
|
||||
for (const node of nodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
|
||||
}
|
||||
lines.push(`${indent}end`);
|
||||
|
||||
|
|
@ -653,11 +823,15 @@ function appendFlowDiagramNode(
|
|||
lines: string[],
|
||||
node: DiagramNode,
|
||||
nodeIds: Map<string, string>,
|
||||
depth: number
|
||||
depth: number,
|
||||
colorScheme?: ResolvedColorScheme,
|
||||
colorClasses?: Map<string, ResolvedColorStyle>
|
||||
): void {
|
||||
const mermaidId = toMermaidNodeId(node.id);
|
||||
const shape = toFlowDiagramMermaidShape(node.kind);
|
||||
const className = toFlowDiagramClassName(node.kind);
|
||||
const className = colorScheme
|
||||
? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses)
|
||||
: toFlowDiagramClassName(node.kind);
|
||||
const indent = " ".repeat(depth);
|
||||
nodeIds.set(node.id, mermaidId);
|
||||
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel(node.label ?? node.ref ?? node.id)}" }`);
|
||||
|
|
@ -695,7 +869,7 @@ function getFlowDomainColorKind(domain: DomainEntry): string {
|
|||
|
||||
export function getDfdMermaidColorSchemeTargets(diagram: ResolvedDiagram): string[] {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) ? ["domain"] : [];
|
||||
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
|
||||
}
|
||||
if (isDfdDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0
|
||||
|
|
@ -721,6 +895,13 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
|
|||
case "app_process":
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "message":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
case "external":
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
@ -728,6 +909,24 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
|
|||
}
|
||||
}
|
||||
|
||||
function registerFlowDiagramColorClass(
|
||||
kind: unknown,
|
||||
colorScheme: ResolvedColorScheme,
|
||||
colorClasses: Map<string, ResolvedColorStyle> | undefined
|
||||
): string {
|
||||
const className = toFlowDiagramColorClassName(kind);
|
||||
colorClasses?.set(
|
||||
className,
|
||||
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : undefined)
|
||||
);
|
||||
return className;
|
||||
}
|
||||
|
||||
function toFlowDiagramColorClassName(kind: unknown): string {
|
||||
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
|
||||
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
|
||||
}
|
||||
|
||||
function toFlowDiagramClassName(kind: unknown): string {
|
||||
switch (kind) {
|
||||
case "screen":
|
||||
|
|
@ -738,11 +937,18 @@ function toFlowDiagramClassName(kind: unknown): string {
|
|||
return "store";
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "message":
|
||||
return "context";
|
||||
case "process":
|
||||
case "app_process":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
return "process";
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
return "external";
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
@ -812,7 +1018,7 @@ function isDfdDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is DfdD
|
|||
return diagram.schema === "dfd_diagram";
|
||||
}
|
||||
|
||||
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): boolean {
|
||||
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is FlowDiagramModel {
|
||||
return diagram.schema === "flow_diagram";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,17 @@ import type { AppProcessBusinessFlowDirection } from "../core/app-process-busine
|
|||
export type FileType = (typeof FILE_TYPES)[number];
|
||||
export type DfdObjectKind = (typeof DFD_OBJECT_KINDS)[number];
|
||||
export type DfdDiagramObjectKind = DfdObjectKind | "other";
|
||||
export type FlowDiagramViewMode = "detail" | "screen";
|
||||
|
||||
export type FlowDiagramObjectKind =
|
||||
| "screen"
|
||||
| "actor"
|
||||
| "user"
|
||||
| "message"
|
||||
| "data"
|
||||
| "api"
|
||||
| "service"
|
||||
| "handler"
|
||||
| "process"
|
||||
| "app_process"
|
||||
| "context"
|
||||
|
|
@ -694,6 +703,7 @@ export interface FlowDiagramModel extends BaseFileModel<"flow-diagram"> {
|
|||
name: string;
|
||||
kind: "screen_communication";
|
||||
description?: string;
|
||||
flowView: FlowDiagramViewMode;
|
||||
domainSources: DomainSourceRef[];
|
||||
domainSourceSummaries?: DomainDiagramSourceSummary[];
|
||||
domains?: DomainEntry[];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ await build({
|
|||
'export { isDiagramPreviewRouteFileType, isDfdLikeDiagramPreviewFileType } from "./src/core/preview-routing";',
|
||||
'export { isModelWeavePreviewSupportedFileType } from "./src/core/supported-formats";',
|
||||
'export { resolveDiagramRelations } from "./src/core/relation-resolver";',
|
||||
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
|
||||
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, buildFlowDiagramScreenFlowProjection, getDfdMermaidColorSchemeTargets, resolveFlowDiagramViewMode } from "./src/renderers/dfd-mermaid";',
|
||||
'export { getAppliedColorSchemeRowsForTargets } from "./src/core/color-scheme";',
|
||||
'export { buildCurrentDiagramDiagnostics } from "./src/core/current-file-diagnostics";',
|
||||
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
|
||||
|
|
@ -82,6 +82,7 @@ const {
|
|||
buildCurrentDiagramDiagnostics,
|
||||
buildDfdMermaidSource,
|
||||
buildFlowDiagramHoverMetadata,
|
||||
buildFlowDiagramScreenFlowProjection,
|
||||
createModelWeaveTranslator,
|
||||
getAppliedColorSchemeLowerPaneSlot,
|
||||
getAppliedColorSchemeRowsForTargets,
|
||||
|
|
@ -95,7 +96,8 @@ const {
|
|||
isDiagramPreviewRouteFileType,
|
||||
isModelWeavePreviewSupportedFileType,
|
||||
parseFlowDiagramFile,
|
||||
resolveDiagramRelations
|
||||
resolveDiagramRelations,
|
||||
resolveFlowDiagramViewMode
|
||||
} = await import(`../${outputFile}?t=${Date.now()}`);
|
||||
|
||||
globalThis.activeDocument = {
|
||||
|
|
@ -162,6 +164,34 @@ test("flow_diagram is detected and parsed as a distinct model type", () => {
|
|||
});
|
||||
|
||||
|
||||
test("flow_diagram defaults flow_view to detail", () => {
|
||||
const { file } = parseFlow();
|
||||
|
||||
assert.equal(file.flowView, "detail");
|
||||
assert.equal(resolveFlowDiagramViewMode(file), "detail");
|
||||
});
|
||||
|
||||
test("flow_diagram parses and resolves screen flow_view", () => {
|
||||
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen");
|
||||
const { file, warnings } = parseFlow(markdown);
|
||||
|
||||
assert.equal(file.flowView, "screen");
|
||||
assert.equal(resolveFlowDiagramViewMode(file), "screen");
|
||||
assert.equal(resolveFlowDiagramViewMode(file, "detail"), "detail");
|
||||
assert.equal(warnings.some((warning) => warning.field === "flow_view"), false);
|
||||
});
|
||||
|
||||
test("flow_diagram unknown flow_view warns and falls back to detail", () => {
|
||||
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: storyboard");
|
||||
const { file, warnings } = parseFlow(markdown);
|
||||
|
||||
assert.equal(file.flowView, "detail");
|
||||
assert.equal(resolveFlowDiagramViewMode(file), "detail");
|
||||
assert.equal(
|
||||
warnings.some((warning) => warning.field === "flow_view" && /unknown flow_view/.test(warning.message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
test("flow_diagram follows the same preview support and diagram route checks", () => {
|
||||
const fileType = detectFileType({ type: "flow_diagram" });
|
||||
|
||||
|
|
@ -258,12 +288,117 @@ test("flow_diagram Mermaid edge labels use trigger, data, and kind", () => {
|
|||
assert.match(source, /mystery -->\|fallback \/ Unknown handoff\| order_screen/);
|
||||
});
|
||||
|
||||
const screenFlowMarkdown = `---
|
||||
type: flow_diagram
|
||||
id: FLOW-CHECKOUT-SCREEN-FLOW
|
||||
name: Checkout Screen Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
---
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| checkout_screen | Checkout Screen | screen | | frontend_area | Start screen |
|
||||
| submit_action | Submit Action | work_object | | frontend_area | User action detail |
|
||||
| checkout_process | Checkout Process | app_process | | application_area | Hidden submit process |
|
||||
| completion_screen | Completion Screen | screen | | frontend_area | Success screen |
|
||||
| error_message | Error Message | message | | frontend_area | User-visible error |
|
||||
| session_store | Session Store | session | | data_area | Hidden session store |
|
||||
| retry_process | Retry Process | process | | application_area | Alternate hidden route |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| SF01 | checkout_screen | submit_action | action | click:Submit | Checkout input | | Submit action |
|
||||
| SF02 | submit_action | checkout_process | command | | Checkout request | | Invoke process |
|
||||
| SF03 | checkout_process | completion_screen | navigate | showComplete | Completion payload | success | Show completion |
|
||||
| SF04 | checkout_process | error_message | message | showError | Error details | failed | Show error |
|
||||
| SF05 | checkout_process | session_store | store | save | Session data | | Hidden persistence |
|
||||
| SF06 | checkout_screen | retry_process | action | click:Retry | Retry input | | Retry action |
|
||||
| SF07 | retry_process | completion_screen | navigate | showComplete | Retry payload | alternate | Alternate success |
|
||||
`;
|
||||
function getFlowDiagramDataWarnings(markdown, extraFiles = []) {
|
||||
const { model, resolved } = resolveFlow(markdown, extraFiles);
|
||||
return buildCurrentDiagramDiagnostics({ diagram: model }, resolved.warnings)
|
||||
.filter((warning) => warning.code === "unresolved-reference" && /flow data reference/.test(warning.message));
|
||||
}
|
||||
|
||||
test("flow_diagram screen view projects internal paths into visible nodes", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
|
||||
assert.deepEqual(projected.nodes.map((node) => node.id), [
|
||||
"checkout_screen",
|
||||
"completion_screen",
|
||||
"error_message"
|
||||
]);
|
||||
assert.equal(projected.edges.length, 2);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "session_store"), false);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "completion_screen"), true);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "error_message"), true);
|
||||
});
|
||||
|
||||
test("flow_diagram screen view deduplicates projected edges and merges labels", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
const completionEdges = projected.edges.filter((edge) =>
|
||||
edge.source === "checkout_screen" && edge.target === "completion_screen"
|
||||
);
|
||||
|
||||
assert.equal(completionEdges.length, 1);
|
||||
assert.match(completionEdges[0].label ?? "", /^(success \/ alternate|alternate \/ success)$/);
|
||||
assert.equal(completionEdges[0].metadata.projected, true);
|
||||
});
|
||||
|
||||
test("flow_diagram screen flow Mermaid hides collapsed nodes and uses projected labels", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
||||
assert.match(source, /checkout_screen@\{ shape: curv-trap, label: "Checkout Screen" \}/);
|
||||
assert.match(source, /completion_screen@\{ shape: curv-trap, label: "Completion Screen" \}/);
|
||||
assert.match(source, /error_message@\{ shape: rect, label: "Error Message" \}/);
|
||||
assert.doesNotMatch(source, /submit_action@/);
|
||||
assert.doesNotMatch(source, /checkout_process@/);
|
||||
assert.doesNotMatch(source, /session_store@/);
|
||||
assert.match(source, /checkout_screen -->\|(success \/ alternate|alternate \/ success)\| completion_screen/);
|
||||
assert.match(source, /checkout_screen -->\|failed\| error_message/);
|
||||
assert.doesNotMatch(source, /Checkout input|Session data/);
|
||||
});
|
||||
test("flow_diagram screen view falls back to detail when no visible nodes exist", () => {
|
||||
const markdown = `---
|
||||
type: flow_diagram
|
||||
id: FLOW-INTERNAL-ONLY
|
||||
name: Internal Only Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
---
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| process_a | Process A | app_process | | application | Internal process |
|
||||
| store_a | Store A | session | | data | Internal store |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | process_a | store_a | store | save | Session data | | Store state |
|
||||
`;
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
||||
assert.deepEqual(projected.nodes.map((node) => node.id), ["process_a", "store_a"]);
|
||||
assert.equal(projected.edges.length, 1);
|
||||
assert.match(source, /process_a@\{ shape: rect, label: "Process A" \}/);
|
||||
assert.match(source, /store_a@\{ shape: lin-cyl, label: "Store A" \}/);
|
||||
assert.match(source, /process_a -->\|save \/ Session data\| store_a/);
|
||||
});
|
||||
test("flow_diagram Flows.data plain text does not warn", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", "DATA-ORDER-DRAFT"));
|
||||
|
||||
|
|
@ -768,6 +903,41 @@ test("flow_diagram Domain Sources resolve Domain name, kind, and parent for nest
|
|||
assert.match(source, /style DOMAIN_application_area fill:#e9fbe9,stroke:#2f8f46,color:#123d1b/);
|
||||
});
|
||||
|
||||
test("flow_diagram screen view preserves resolved Domain Sources for visible screens", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown
|
||||
.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen")
|
||||
.replace(
|
||||
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |",
|
||||
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |\n| CONFIRM_SCREEN | Confirmation Screen | screen | | frontend_area | Completion screen |"
|
||||
)
|
||||
.replace(
|
||||
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |",
|
||||
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |\n| F03 | ORDER_SUBMIT | CONFIRM_SCREEN | navigate | showComplete | Confirmation data | success | Show completion |"
|
||||
);
|
||||
const { resolved } = resolveFlow(markdown, [
|
||||
{ path: "DOMAINS-FLOW-TEST.md", content: flowDomainSourceMarkdown }
|
||||
]);
|
||||
const source = buildDfdMermaidSource(resolved, {
|
||||
id: "COLOR-FLOW-DOMAIN-TEST",
|
||||
name: "Flow Domain Test Colors",
|
||||
entries: [
|
||||
{ target: "domain", kind: "system", fill: "#f7f7f7", stroke: "#999999", text: "#222222", rowIndex: 0 },
|
||||
{ target: "domain", kind: "ui", fill: "#e8f1ff", stroke: "#3b73d9", text: "#102a5c", rowIndex: 1 },
|
||||
{ target: "domain", kind: "application", fill: "#e9fbe9", stroke: "#2f8f46", text: "#123d1b", rowIndex: 2 },
|
||||
{ target: "domain", kind: "data", fill: "#f3e8ff", stroke: "#7c3bbd", text: "#2d124c", rowIndex: 3 }
|
||||
],
|
||||
defaultStyle: { fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111" }
|
||||
});
|
||||
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system\["Checkout System"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area\["Frontend Area"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_frontend_area[\s\S]*ORDER_ENTRY/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area[\s\S]*CONFIRM_SCREEN/);
|
||||
assert.doesNotMatch(source, /ORDER_SUBMIT@/);
|
||||
assert.doesNotMatch(source, /SESSION_STORE@/);
|
||||
assert.match(source, /ORDER_ENTRY -->\|success\| CONFIRM_SCREEN/);
|
||||
assert.match(source, /style DOMAIN_frontend_area fill:#e8f1ff,stroke:#3b73d9,color:#102a5c/);
|
||||
});
|
||||
test("flow_diagram local Domains resolve names, kinds, and nested parents", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown
|
||||
.replace("## Domain Sources\n\n| ref | notes |\n|---|---|\n| [[DOMAINS-FLOW-TEST]] | test domains |\n\n", "")
|
||||
|
|
@ -861,7 +1031,9 @@ const flowDomainColorScheme = {
|
|||
name: "Flow Domain Colors",
|
||||
entries: [
|
||||
{ target: "domain", kind: "sales", fill: "#7ddea7", stroke: "#000000", text: "#ffffff", rowIndex: 0 },
|
||||
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 }
|
||||
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 },
|
||||
{ target: "flow_diagram", kind: "screen", fill: "#fff4cc", stroke: "#bf9000", text: "#111111", rowIndex: 2 },
|
||||
{ target: "flow_diagram", kind: "app_process", fill: "#e2f0d9", stroke: "#70ad47", text: "#111111", rowIndex: 3 }
|
||||
],
|
||||
defaultStyle: {
|
||||
fill: "#f5f5f5",
|
||||
|
|
@ -887,7 +1059,8 @@ test("flow_diagram Applied Color Scheme targets include used domain rows", () =>
|
|||
const targets = getDfdMermaidColorSchemeTargets(resolved);
|
||||
const rows = getAppliedColorSchemeRowsForTargets(flowDomainColorScheme, targets);
|
||||
|
||||
assert.deepEqual(targets, ["domain"]);
|
||||
assert.deepEqual(targets, ["flow_diagram", "domain"]);
|
||||
assert.equal(rows.some((row) => row.entry.target === "flow_diagram" && row.entry.kind === "screen"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "sales"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "application"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "app_process"), false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue