feat: add business flow direction settings

This commit is contained in:
nejimakibird 2026-06-24 20:21:53 +09:00
parent 6201b1c9a2
commit b7f5cbbaef
15 changed files with 609 additions and 11 deletions

View file

@ -135,6 +135,7 @@ type: app_process
id: PROC-INVENTORY-SEARCH
name: Inventory Search Process
kind: server_process
flow_direction: LR
tags:
- AppProcess
---
@ -177,6 +178,7 @@ type: app_process
id: PROC-INVENTORY-INQUIRY
name: Inventory Inquiry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -212,6 +214,7 @@ type: app_process
id: PROC-ORDER-ENTRY-FLOW
name: Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -265,6 +268,7 @@ type: app_process
id: PROC-SAMPLE-ORDER-ENTRY-FLOW
name: Sample Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -362,6 +366,7 @@ Optional fields:
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | Process kind. Free text such as `server_process`, `batch`, `api`, `job`, `event_handler`, or `business_flow`. |
| `render_mode` | Optional. If specified, it overrides the initial renderer for this file. If omitted, the format-specific default render mode from the settings is used. |
| `flow_direction` | Optional Business Flow preview direction. Supported values are `LR` and `TD`. If omitted, the setting default is used. |
| `tags` | Obsidian / Markdown tags. |
Example:
@ -372,12 +377,31 @@ type: app_process
id: PROC-ORDER-ENTRY-FLOW
name: Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
---
```
## Business Flow direction
`flow_direction` controls the initial Mermaid direction for the app_process Business Flow preview.
Supported values:
* `LR`: left to right.
* `TD`: top down.
Direction resolution priority:
1. Viewer toolbar override.
2. `frontmatter.flow_direction`.
3. The plugin setting default App Process Business Flow direction.
4. Built-in `LR` fallback.
Changing the toolbar direction is temporary viewer state and does not rewrite Markdown frontmatter. `flow_direction` is separate from `render_mode`.
## Sections
Recommended structure:

View file

@ -135,6 +135,7 @@ type: app_process
id: PROC-INVENTORY-SEARCH
name: Inventory Search Process
kind: server_process
flow_direction: LR
tags:
- AppProcess
---
@ -177,6 +178,7 @@ type: app_process
id: PROC-INVENTORY-INQUIRY
name: Inventory Inquiry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -212,6 +214,7 @@ type: app_process
id: PROC-ORDER-ENTRY-FLOW
name: Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -265,6 +268,7 @@ type: app_process
id: PROC-SAMPLE-ORDER-ENTRY-FLOW
name: Sample Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
@ -362,6 +366,7 @@ Demonstrates table-based app_process Steps and Flows for the Business Flow previ
| ------------- | ------------------------------------------------------------------------------------ |
| `kind` | 処理種別です。例: `server_process`, `batch`, `api`, `job`, `event_handler`, `business_flow`。 |
| `render_mode` | 任意です。指定した場合、そのファイルの初期表示レンダラを上書きします。未指定の場合は、設定画面のフォーマット別初期表示モードに従います。 |
| `flow_direction` | 任意です。Business Flow preview の方向です。指定できる値は `LR``TD` です。未指定の場合は設定画面のデフォルトを使います。 |
| `tags` | Obsidian / Markdown のタグです。 |
例:
@ -372,12 +377,31 @@ type: app_process
id: PROC-ORDER-ENTRY-FLOW
name: Order Entry Business Flow
kind: server_process
flow_direction: LR
tags:
- AppProcess
- BusinessFlow
---
```
## Business Flow 方向
`flow_direction` は app_process Business Flow preview の Mermaid 方向を指定します。
指定できる値:
* `LR`: 左から右。
* `TD`: 上から下。
方向の解決優先順位:
1. Viewer toolbar の一時上書き。
2. `frontmatter.flow_direction`
3. 設定画面の App Process Business Flow 初期方向。
4. 組み込みの `LR` fallback。
Toolbar で方向を変更しても Markdown frontmatter は書き換えません。`flow_direction` は `render_mode` とは別の設定です。
## セクション
推奨構成:

165
main.js
View file

@ -9390,6 +9390,21 @@ function waitForAnimationFrame() {
});
}
// src/core/app-process-business-flow-direction.ts
function normalizeAppProcessBusinessFlowDirection(value) {
if (typeof value !== "string") {
return void 0;
}
const normalized = value.trim().toUpperCase();
return normalized === "LR" || normalized === "TD" ? normalized : void 0;
}
function normalizeAppProcessBusinessFlowDirectionWithFallback(value) {
return normalizeAppProcessBusinessFlowDirection(value) ?? "LR";
}
function resolveAppProcessBusinessFlowDirection(input) {
return normalizeAppProcessBusinessFlowDirection(input.toolbarOverride) ?? normalizeAppProcessBusinessFlowDirection(input.frontmatterDirection) ?? normalizeAppProcessBusinessFlowDirection(input.settingsDefaultDirection) ?? "LR";
}
// src/settings/model-weave-settings.ts
var DOMAIN_VIEW_MODE_SETTING_OPTIONS = [
{ value: "mindmap", label: "Mindmap" },
@ -9401,6 +9416,7 @@ var DEFAULT_MODEL_WEAVE_SETTINGS = {
defaultErRenderMode: "custom",
defaultDfdRenderMode: "mermaid",
defaultProcessRenderMode: "custom",
defaultBusinessFlowDirection: "LR",
defaultScreenRenderMode: "custom",
defaultDomainsViewMode: "mindmap",
defaultDomainDiagramViewMode: "mindmap",
@ -9476,6 +9492,9 @@ function normalizeModelWeaveSettings(value) {
PROCESS_RENDER_MODES,
DEFAULT_MODEL_WEAVE_SETTINGS.defaultProcessRenderMode
),
defaultBusinessFlowDirection: normalizeAppProcessBusinessFlowDirectionWithFallback(
raw.defaultBusinessFlowDirection
),
defaultScreenRenderMode: normalizeEnumValue(
raw.defaultScreenRenderMode,
SCREEN_RENDER_MODES,
@ -11787,6 +11806,7 @@ function parseAppProcessFile(markdown, path2) {
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
const flowDirection = normalizeAppProcessBusinessFlowDirection(frontmatter.flow_direction);
if (frontmatter.type !== "app_process") {
warnings.push(createWarning12(path2, "type", 'expected type "app_process"'));
}
@ -11863,6 +11883,7 @@ function parseAppProcessFile(markdown, path2) {
id,
name: fallbackName,
kind: kind || void 0,
flowDirection,
summary: joinSectionLines5(sections.Summary),
inputs: inputsTable.rows.map((row) => ({
id: row.id?.trim() ?? "",
@ -17912,7 +17933,8 @@ function renderAppProcessBusinessFlow(model, options = {}) {
);
const source = buildAppProcessBusinessFlowMermaidSource(
model,
options.colorScheme
options.colorScheme,
options.flowDirection
);
const ready = renderMermaidSourceIntoShell(shell3, {
source,
@ -17974,7 +17996,7 @@ function buildAppProcessBusinessFlowInteractionTargets(model, sourcePath) {
modelType: "app-process"
}));
}
function buildAppProcessBusinessFlowMermaidSource(model, colorScheme) {
function buildAppProcessBusinessFlowMermaidSource(model, colorScheme, flowDirection) {
const stepNodeIds = /* @__PURE__ */ new Map();
const stepNodeIdsByStepId = /* @__PURE__ */ new Map();
model.steps.forEach((step, index) => {
@ -17984,7 +18006,8 @@ function buildAppProcessBusinessFlowMermaidSource(model, colorScheme) {
stepNodeIdsByStepId.set(step.id, nodeId);
}
});
const lines = ["flowchart LR"];
const normalizedDirection = normalizeAppProcessBusinessFlowDirectionWithFallback(flowDirection);
const lines = [`flowchart ${normalizedDirection}`];
const colorClasses = /* @__PURE__ */ new Map();
const domainStyles = [];
const nodeClasses = [];
@ -18676,6 +18699,9 @@ var EN_MESSAGES = {
"domains.preview.area": "Area",
"domains.preview.treeMode": "Tree",
"domains.preview.viewMode": "Domain view mode",
"appProcess.businessFlow.direction": "Business flow direction",
"appProcess.businessFlow.direction.lr": "Left to right",
"appProcess.businessFlow.direction.td": "Top down",
"domains.preview.diagramEmpty": "No domain hierarchy to display.",
"domains.preview.diagramRenderFailed": "Domain hierarchy diagram could not be rendered.",
"domains.preview.empty": "No domains defined.",
@ -18921,6 +18947,10 @@ var EN_MESSAGES = {
"settings.defaultDfdRenderMode.desc": "Used for dfd_diagram files when frontmatter.render_mode is not set.",
"settings.defaultProcessRenderMode.name": "Default process render mode",
"settings.defaultProcessRenderMode.desc": "Used for app_process files when frontmatter.render_mode is not set.",
"settings.defaultBusinessFlowDirection.name": "Default business flow direction",
"settings.defaultBusinessFlowDirection.desc": "Used for app_process business flow previews when frontmatter.flow_direction is not set.",
"settings.defaultBusinessFlowDirection.lr": "Left to right",
"settings.defaultBusinessFlowDirection.td": "Top down",
"settings.defaultScreenRenderMode.name": "Default screen render mode",
"settings.defaultScreenRenderMode.desc": "Used for screen files when frontmatter.render_mode is not set.",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
@ -19036,6 +19066,9 @@ var JA_MESSAGES = {
"domains.preview.area": "\u9818\u57DF",
"domains.preview.treeMode": "\u30C4\u30EA\u30FC",
"domains.preview.viewMode": "Domain \u8868\u793A\u30E2\u30FC\u30C9",
"appProcess.businessFlow.direction": "Business Flow \u65B9\u5411",
"appProcess.businessFlow.direction.lr": "\u5DE6\u304B\u3089\u53F3",
"appProcess.businessFlow.direction.td": "\u4E0A\u304B\u3089\u4E0B",
"domains.preview.diagramEmpty": "\u8868\u793A\u3067\u304D\u308B Domain \u968E\u5C64\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
"domains.preview.diagramRenderFailed": "Domain \u968E\u5C64\u56F3\u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
"domains.preview.empty": "Domain \u306F\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
@ -19241,6 +19274,10 @@ var JA_MESSAGES = {
"settings.defaultDfdRenderMode.desc": "dfd_diagram \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
"settings.defaultProcessRenderMode.name": "Process \u306E\u521D\u671F render_mode",
"settings.defaultProcessRenderMode.desc": "app_process \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
"settings.defaultBusinessFlowDirection.name": "Business Flow \u306E\u521D\u671F\u65B9\u5411",
"settings.defaultBusinessFlowDirection.desc": "app_process Business Flow preview \u3067 frontmatter.flow_direction \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
"settings.defaultBusinessFlowDirection.lr": "\u5DE6\u304B\u3089\u53F3",
"settings.defaultBusinessFlowDirection.td": "\u4E0A\u304B\u3089\u4E0B",
"settings.defaultScreenRenderMode.name": "Screen \u306E\u521D\u671F render_mode",
"settings.defaultScreenRenderMode.desc": "screen \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
"settings.defaultDomainsViewMode.name": "Domains \u306E\u521D\u671F\u8868\u793A\u30E2\u30FC\u30C9",
@ -20282,6 +20319,7 @@ var DEFAULT_VIEWER_PREFERENCES = {
nodeDensity: "normal",
defaultDomainsViewMode: "mindmap",
defaultDomainDiagramViewMode: "mindmap",
defaultBusinessFlowDirection: "LR",
localSourceRoot: "",
uiLanguage: "auto",
showMermaidRenderDebug: false
@ -20335,6 +20373,8 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
this.splitRatioByKey = /* @__PURE__ */ new Map();
this.domainsDiagramMode = "mindmap";
this.domainsDiagramModeFilePath = null;
this.appProcessBusinessFlowDirectionOverride = null;
this.appProcessBusinessFlowDirectionFilePath = null;
this.domainsDiagramModeState = null;
this.activeScrollContainer = null;
this.focusModeEnabled = false;
@ -20492,6 +20532,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
this.persistActiveViewportState();
this.persistCurrentScrollPosition();
this.prepareDomainsDiagramMode(state, nextFilePath);
this.prepareAppProcessBusinessFlowDirection(state, nextFilePath);
this.prepareViewportState(state, reason);
this.state = state;
this.renderCurrentState();
@ -20619,6 +20660,18 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
state.hasUserInteracted = false;
}
}
prepareAppProcessBusinessFlowDirection(state, nextFilePath) {
if (state.mode !== "summary" || (state.businessFlow?.steps.length ?? 0) === 0) {
this.appProcessBusinessFlowDirectionOverride = null;
this.appProcessBusinessFlowDirectionFilePath = null;
return;
}
if (this.appProcessBusinessFlowDirectionFilePath === nextFilePath) {
return;
}
this.appProcessBusinessFlowDirectionOverride = null;
this.appProcessBusinessFlowDirectionFilePath = nextFilePath;
}
prepareDomainsDiagramMode(state, nextFilePath) {
if (state.mode !== "domains" && state.mode !== "domain-diagram") {
this.domainsDiagramModeFilePath = null;
@ -20769,7 +20822,8 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
render: () => renderAppProcessBusinessFlow(state.businessFlow, {
forExport: true,
debug: false,
colorScheme: state.colorScheme
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state)
})
};
}
@ -21668,6 +21722,86 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
rightGroup.appendChild(wrapper);
}
appendAppProcessBusinessFlowDirectionSelector(container, filePath) {
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
if (!toolbar) {
return;
}
toolbar.addClass("model-weave-render-mode-toolbar-host");
toolbar.querySelector(".model-weave-business-flow-direction-select-group")?.remove();
const doc = container.ownerDocument;
const wrapper = doc.createElement("div");
wrapper.className = "model-weave-business-flow-direction-select-group model-weave-render-mode-row";
const label = doc.createElement("span");
label.addClass("model-weave-render-mode-label");
label.textContent = this.t("appProcess.businessFlow.direction");
wrapper.appendChild(label);
const select = doc.createElement("select");
select.addClass("model-weave-business-flow-direction-select");
for (const direction of ["LR", "TD"]) {
const option = doc.createElement("option");
option.value = direction;
option.textContent = this.getAppProcessBusinessFlowDirectionLabel(direction);
option.selected = this.getAppProcessBusinessFlowDirectionForFile(filePath) === direction;
select.appendChild(option);
}
select.addEventListener("change", () => {
this.setAppProcessBusinessFlowDirection(select.value, filePath);
});
wrapper.appendChild(select);
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
rightGroup.appendChild(wrapper);
}
getAppProcessBusinessFlowDirectionLabel(direction) {
return direction === "TD" ? this.t("appProcess.businessFlow.direction.td") : this.t("appProcess.businessFlow.direction.lr");
}
getAppProcessBusinessFlowDirection(state) {
return resolveAppProcessBusinessFlowDirection({
toolbarOverride: this.getAppProcessBusinessFlowDirectionOverride(state.filePath),
frontmatterDirection: state.businessFlowDirection,
settingsDefaultDirection: this.viewerPreferences.defaultBusinessFlowDirection
});
}
getAppProcessBusinessFlowDirectionForFile(filePath) {
const summaryState = this.state.mode === "summary" && this.state.filePath === filePath ? this.state : null;
return resolveAppProcessBusinessFlowDirection({
toolbarOverride: this.getAppProcessBusinessFlowDirectionOverride(filePath),
frontmatterDirection: summaryState?.businessFlowDirection,
settingsDefaultDirection: this.viewerPreferences.defaultBusinessFlowDirection
});
}
getAppProcessBusinessFlowDirectionOverride(filePath) {
return this.appProcessBusinessFlowDirectionFilePath === filePath ? this.appProcessBusinessFlowDirectionOverride : null;
}
setAppProcessBusinessFlowDirection(direction, filePath) {
const nextDirection = normalizeAppProcessBusinessFlowDirection(direction);
if (!nextDirection) {
return;
}
if (this.appProcessBusinessFlowDirectionOverride === nextDirection && this.appProcessBusinessFlowDirectionFilePath === filePath) {
return;
}
const shouldRestoreViewOnly = this.viewOnlyEnabled && this.viewOnlyTarget?.classList.contains("model-weave-app-process-business-flow");
this.appProcessBusinessFlowDirectionOverride = nextDirection;
this.appProcessBusinessFlowDirectionFilePath = filePath;
this.viewportStateCache.delete(filePath);
resetGraphViewportState(this.screenPreviewViewportState);
this.renderCurrentState();
this.restoreCurrentScrollPosition();
if (shouldRestoreViewOnly) {
const view = this.contentEl.ownerDocument.defaultView;
view?.requestAnimationFrame(() => {
view.requestAnimationFrame(() => {
const nextTarget = this.contentEl.querySelector(
".model-weave-app-process-business-flow"
);
if (nextTarget) {
this.setViewOnlyMode(true, { target: nextTarget });
}
});
});
}
}
getDomainDiagramModeLabel(mode) {
if (mode === "mindmap") {
return this.t("domains.preview.mindmap");
@ -21712,6 +21846,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug,
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state),
app: this.app,
interactionSourcePath: state.filePath,
viewportState: this.screenPreviewViewportState,
@ -21721,6 +21856,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
});
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
this.appendViewerToolbarControls(businessFlowRoot);
this.appendAppProcessBusinessFlowDirectionSelector(businessFlowRoot, state.filePath);
shell3.topPane.appendChild(businessFlowRoot);
this.renderSummaryDetails(shell3.bottomPane, state, {
suppressBusinessFlowChart: true
@ -21820,6 +21956,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
const businessFlowRoot = renderAppProcessBusinessFlow(state.businessFlow, {
viewportState: this.screenPreviewViewportState,
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state),
app: this.app,
interactionSourcePath: state.filePath,
...getGraphExportLabels(this.t),
@ -21831,6 +21968,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
});
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
this.appendViewerToolbarControls(businessFlowRoot);
this.appendAppProcessBusinessFlowDirectionSelector(businessFlowRoot, state.filePath);
section.appendChild(businessFlowRoot);
}
if (state.sections.length > 0) {
@ -24462,6 +24600,10 @@ var PROCESS_RENDER_MODE_OPTIONS = [
var SCREEN_RENDER_MODE_OPTIONS = [
"custom"
];
var BUSINESS_FLOW_DIRECTION_OPTIONS = [
"LR",
"TD"
];
var DOMAIN_VIEW_MODE_OPTIONS = [
"mindmap",
"area",
@ -24479,6 +24621,9 @@ function isDfdRenderModeOption(value) {
function isProcessRenderModeOption(value) {
return PROCESS_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
}
function isBusinessFlowDirectionOption(value) {
return BUSINESS_FLOW_DIRECTION_OPTIONS.some((candidate) => candidate === value);
}
function isScreenRenderModeOption(value) {
return SCREEN_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
}
@ -24917,6 +25062,7 @@ var ModelWeavePlugin = class extends import_obsidian8.Plugin {
nodeDensity: this.settings.nodeDensity,
defaultDomainsViewMode: this.settings.defaultDomainsViewMode,
defaultDomainDiagramViewMode: this.settings.defaultDomainDiagramViewMode,
defaultBusinessFlowDirection: this.settings.defaultBusinessFlowDirection,
localSourceRoot: this.settings.localSourceRoot,
uiLanguage: this.settings.uiLanguage,
showMermaidRenderDebug: this.settings.showMermaidRenderDebug
@ -25531,6 +25677,7 @@ var ModelWeavePlugin = class extends import_obsidian8.Plugin {
textSections: this.buildAppProcessTextSections(model),
tables: this.buildAppProcessSummaryTables(model, file.path),
appProcessDomainPlacement: domainPlacement,
businessFlowDirection: model.flowDirection,
businessFlow: (model.steps?.length ?? 0) > 0 ? {
title: model.name || model.id,
steps: model.steps ?? [],
@ -27445,6 +27592,16 @@ var ModelWeaveSettingTab = class extends import_obsidian8.PluginSettingTab {
});
});
});
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultBusinessFlowDirection.name")).setDesc(t("settings.defaultBusinessFlowDirection.desc")).addDropdown((dropdown) => {
dropdown.addOption("LR", t("settings.defaultBusinessFlowDirection.lr")).addOption("TD", t("settings.defaultBusinessFlowDirection.td")).setValue(settings.defaultBusinessFlowDirection).onChange(async (value) => {
if (!isBusinessFlowDirectionOption(value)) {
return;
}
await this.plugin.updateSettings({
defaultBusinessFlowDirection: value
});
});
});
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultScreenRenderMode.name")).setDesc(t("settings.defaultScreenRenderMode.desc")).addDropdown((dropdown) => {
dropdown.addOption("custom", t("settings.option.custom")).setValue(settings.defaultScreenRenderMode).onChange(async (value) => {
if (!isScreenRenderModeOption(value)) {

View file

@ -0,0 +1,37 @@
export type AppProcessBusinessFlowDirection = "LR" | "TD";
export interface ResolveAppProcessBusinessFlowDirectionInput {
toolbarOverride?: unknown;
frontmatterDirection?: unknown;
settingsDefaultDirection?: unknown;
}
export function normalizeAppProcessBusinessFlowDirection(
value: unknown
): AppProcessBusinessFlowDirection | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toUpperCase();
return normalized === "LR" || normalized === "TD"
? normalized
: undefined;
}
export function normalizeAppProcessBusinessFlowDirectionWithFallback(
value: unknown
): AppProcessBusinessFlowDirection {
return normalizeAppProcessBusinessFlowDirection(value) ?? "LR";
}
export function resolveAppProcessBusinessFlowDirection(
input: ResolveAppProcessBusinessFlowDirectionInput
): AppProcessBusinessFlowDirection {
return (
normalizeAppProcessBusinessFlowDirection(input.toolbarOverride) ??
normalizeAppProcessBusinessFlowDirection(input.frontmatterDirection) ??
normalizeAppProcessBusinessFlowDirection(input.settingsDefaultDirection) ??
"LR"
);
}

View file

@ -77,6 +77,9 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = {
"domains.preview.area": "Area",
"domains.preview.treeMode": "Tree",
"domains.preview.viewMode": "Domain view mode",
"appProcess.businessFlow.direction": "Business flow direction",
"appProcess.businessFlow.direction.lr": "Left to right",
"appProcess.businessFlow.direction.td": "Top down",
"domains.preview.diagramEmpty": "No domain hierarchy to display.",
"domains.preview.diagramRenderFailed": "Domain hierarchy diagram could not be rendered.",
"domains.preview.empty": "No domains defined.",
@ -322,6 +325,10 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = {
"settings.defaultDfdRenderMode.desc": "Used for dfd_diagram files when frontmatter.render_mode is not set.",
"settings.defaultProcessRenderMode.name": "Default process render mode",
"settings.defaultProcessRenderMode.desc": "Used for app_process files when frontmatter.render_mode is not set.",
"settings.defaultBusinessFlowDirection.name": "Default business flow direction",
"settings.defaultBusinessFlowDirection.desc": "Used for app_process business flow previews when frontmatter.flow_direction is not set.",
"settings.defaultBusinessFlowDirection.lr": "Left to right",
"settings.defaultBusinessFlowDirection.td": "Top down",
"settings.defaultScreenRenderMode.name": "Default screen render mode",
"settings.defaultScreenRenderMode.desc": "Used for screen files when frontmatter.render_mode is not set.",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module

View file

@ -71,6 +71,9 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = {
"domains.preview.area": "領域",
"domains.preview.treeMode": "ツリー",
"domains.preview.viewMode": "Domain 表示モード",
"appProcess.businessFlow.direction": "Business Flow 方向",
"appProcess.businessFlow.direction.lr": "左から右",
"appProcess.businessFlow.direction.td": "上から下",
"domains.preview.diagramEmpty": "表示できる Domain 階層がありません。",
"domains.preview.diagramRenderFailed": "Domain 階層図を描画できませんでした。",
"domains.preview.empty": "Domain は定義されていません。",
@ -276,6 +279,10 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = {
"settings.defaultDfdRenderMode.desc": "dfd_diagram ファイルで frontmatter.render_mode が未設定の場合に使用します。",
"settings.defaultProcessRenderMode.name": "Process の初期 render_mode",
"settings.defaultProcessRenderMode.desc": "app_process ファイルで frontmatter.render_mode が未設定の場合に使用します。",
"settings.defaultBusinessFlowDirection.name": "Business Flow の初期方向",
"settings.defaultBusinessFlowDirection.desc": "app_process Business Flow preview で frontmatter.flow_direction が未設定の場合に使用します。",
"settings.defaultBusinessFlowDirection.lr": "左から右",
"settings.defaultBusinessFlowDirection.td": "上から下",
"settings.defaultScreenRenderMode.name": "Screen の初期 render_mode",
"settings.defaultScreenRenderMode.desc": "screen ファイルで frontmatter.render_mode が未設定の場合に使用します。",
"settings.defaultDomainsViewMode.name": "Domains の初期表示モード",

View file

@ -161,6 +161,10 @@ const PROCESS_RENDER_MODE_OPTIONS: readonly ModelWeaveSettings["defaultProcessRe
const SCREEN_RENDER_MODE_OPTIONS: readonly ModelWeaveSettings["defaultScreenRenderMode"][] = [
"custom"
];
const BUSINESS_FLOW_DIRECTION_OPTIONS: readonly ModelWeaveSettings["defaultBusinessFlowDirection"][] = [
"LR",
"TD"
];
const DOMAIN_VIEW_MODE_OPTIONS: readonly ModelWeaveSettings["defaultDomainsViewMode"][] = [
"mindmap",
"area",
@ -191,6 +195,12 @@ function isProcessRenderModeOption(
return PROCESS_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
}
function isBusinessFlowDirectionOption(
value: string
): value is ModelWeaveSettings["defaultBusinessFlowDirection"] {
return BUSINESS_FLOW_DIRECTION_OPTIONS.some((candidate) => candidate === value);
}
function isScreenRenderModeOption(
value: string
): value is ModelWeaveSettings["defaultScreenRenderMode"] {
@ -719,6 +729,7 @@ export default class ModelWeavePlugin extends Plugin {
nodeDensity: this.settings.nodeDensity,
defaultDomainsViewMode: this.settings.defaultDomainsViewMode,
defaultDomainDiagramViewMode: this.settings.defaultDomainDiagramViewMode,
defaultBusinessFlowDirection: this.settings.defaultBusinessFlowDirection,
localSourceRoot: this.settings.localSourceRoot,
uiLanguage: this.settings.uiLanguage,
showMermaidRenderDebug: this.settings.showMermaidRenderDebug
@ -1470,6 +1481,7 @@ export default class ModelWeavePlugin extends Plugin {
textSections: this.buildAppProcessTextSections(model),
tables: this.buildAppProcessSummaryTables(model, file.path),
appProcessDomainPlacement: domainPlacement,
businessFlowDirection: model.flowDirection,
businessFlow:
(model.steps?.length ?? 0) > 0
? {
@ -3992,6 +4004,25 @@ class ModelWeaveSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
.setName(t("settings.defaultBusinessFlowDirection.name"))
.setDesc(t("settings.defaultBusinessFlowDirection.desc"))
.addDropdown((dropdown) => {
dropdown
.addOption("LR", t("settings.defaultBusinessFlowDirection.lr"))
.addOption("TD", t("settings.defaultBusinessFlowDirection.td"))
.setValue(settings.defaultBusinessFlowDirection)
.onChange(async (value) => {
if (!isBusinessFlowDirectionOption(value)) {
return;
}
await this.plugin.updateSettings({
defaultBusinessFlowDirection: value
});
});
});
new Setting(containerEl)
.setName(t("settings.defaultScreenRenderMode.name"))
.setDesc(t("settings.defaultScreenRenderMode.desc"))

View file

@ -4,6 +4,7 @@ import { extractMarkdownSections } from "./markdown-sections";
import { parseSourceLinks } from "./source-links-parser";
import { parseDomainEntries, validateDomainEntries } from "./domains-parser";
import { parseDomainSourcesTable } from "./domain-sources-parser";
import { normalizeAppProcessBusinessFlowDirection } from "../core/app-process-business-flow-direction";
import type {
AppProcessModel,
ValidationWarning
@ -36,6 +37,7 @@ export function parseAppProcessFile(
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
const flowDirection = normalizeAppProcessBusinessFlowDirection(frontmatter.flow_direction);
if (frontmatter.type !== "app_process") {
warnings.push(createWarning(path, "type", 'expected type "app_process"'));
@ -128,6 +130,7 @@ export function parseAppProcessFile(
id,
name: fallbackName,
kind: kind || undefined,
flowDirection,
summary: joinSectionLines(sections.Summary),
inputs: inputsTable.rows
.map((row) => ({

View file

@ -23,6 +23,10 @@ import {
} from "./mermaid-helpers";
import { decodeEscapedDisplayText } from "../utils/display-text";
import { modelWeaveText } from "../i18n/language";
import {
normalizeAppProcessBusinessFlowDirectionWithFallback,
type AppProcessBusinessFlowDirection
} from "../core/app-process-business-flow-direction";
import { attachMermaidNodeInteractions, type GraphInteractionTarget } from "../views/mermaid-node-interactions";
export interface AppProcessBusinessFlowModel {
@ -50,6 +54,7 @@ export interface AppProcessBusinessFlowRenderOptions {
exportAndOpenPngLabel?: string;
exportAndOpenPngTitle?: string;
colorScheme?: ResolvedColorScheme;
flowDirection?: AppProcessBusinessFlowDirection;
app?: App;
interactionSourcePath?: string;
}
@ -77,7 +82,8 @@ export function renderAppProcessBusinessFlow(
);
const source = buildAppProcessBusinessFlowMermaidSource(
model,
options.colorScheme
options.colorScheme,
options.flowDirection
);
const ready = renderMermaidSourceIntoShell(shell, {
source,
@ -155,7 +161,8 @@ function buildAppProcessBusinessFlowInteractionTargets(
export function buildAppProcessBusinessFlowMermaidSource(
model: AppProcessBusinessFlowModel,
colorScheme?: ResolvedColorScheme
colorScheme?: ResolvedColorScheme,
flowDirection?: unknown
): string {
const stepNodeIds = new Map<AppProcessStep, string>();
const stepNodeIdsByStepId = new Map<string, string>();
@ -167,7 +174,8 @@ export function buildAppProcessBusinessFlowMermaidSource(
}
});
const lines = ["flowchart LR"];
const normalizedDirection = normalizeAppProcessBusinessFlowDirectionWithFallback(flowDirection);
const lines = [`flowchart ${normalizedDirection}`];
const colorClasses = new Map<string, ResolvedColorStyle>();
const domainStyles: string[] = [];
const nodeClasses: string[] = [];

View file

@ -1,4 +1,8 @@
import type { RenderMode } from "../core/render-mode";
import {
normalizeAppProcessBusinessFlowDirectionWithFallback,
type AppProcessBusinessFlowDirection
} from "../core/app-process-business-flow-direction";
import type { ModelWeaveUiLanguage } from "../i18n/messages";
export type ModelWeaveDefaultZoom = "fit" | "100";
@ -20,6 +24,7 @@ export interface ModelWeaveSettings {
defaultErRenderMode: RenderMode;
defaultDfdRenderMode: RenderMode;
defaultProcessRenderMode: RenderMode;
defaultBusinessFlowDirection: AppProcessBusinessFlowDirection;
defaultScreenRenderMode: RenderMode;
defaultDomainsViewMode: ModelWeaveDomainViewMode;
defaultDomainDiagramViewMode: ModelWeaveDomainViewMode;
@ -36,6 +41,7 @@ export interface ModelWeaveSettings {
export type ModelWeaveViewerPreferences = Pick<
ModelWeaveSettings,
| "defaultZoom"
| "defaultBusinessFlowDirection"
| "defaultDomainsViewMode"
| "defaultDomainDiagramViewMode"
| "fontSize"
@ -51,6 +57,7 @@ export const DEFAULT_MODEL_WEAVE_SETTINGS: ModelWeaveSettings = {
defaultErRenderMode: "custom",
defaultDfdRenderMode: "mermaid",
defaultProcessRenderMode: "custom",
defaultBusinessFlowDirection: "LR",
defaultScreenRenderMode: "custom",
defaultDomainsViewMode: "mindmap",
defaultDomainDiagramViewMode: "mindmap",
@ -131,6 +138,9 @@ export function normalizeModelWeaveSettings(
PROCESS_RENDER_MODES,
DEFAULT_MODEL_WEAVE_SETTINGS.defaultProcessRenderMode
),
defaultBusinessFlowDirection: normalizeAppProcessBusinessFlowDirectionWithFallback(
raw.defaultBusinessFlowDirection
),
defaultScreenRenderMode: normalizeEnumValue(
raw.defaultScreenRenderMode,
SCREEN_RENDER_MODES,

View file

@ -12,6 +12,7 @@ import type {
ValidationWarningCode,
ValidationWarningSeverity
} from "./warnings";
import type { AppProcessBusinessFlowDirection } from "../core/app-process-business-flow-direction";
export type FileType = (typeof FILE_TYPES)[number];
export type DfdObjectKind = (typeof DFD_OBJECT_KINDS)[number];
@ -234,6 +235,7 @@ export interface AppProcessModel extends BaseFileModel<"app-process"> {
id: string;
name: string;
kind?: string;
flowDirection?: AppProcessBusinessFlowDirection;
summary?: string;
domains: DomainEntry[];
domainSources: DomainSourceRef[];

View file

@ -26,6 +26,11 @@ import {
renderAppProcessBusinessFlow,
type AppProcessBusinessFlowModel
} from "../renderers/app-process-business-flow";
import {
normalizeAppProcessBusinessFlowDirection,
resolveAppProcessBusinessFlowDirection,
type AppProcessBusinessFlowDirection
} from "../core/app-process-business-flow-direction";
import {
attachGraphViewportInteractions,
resetGraphViewportState,
@ -358,6 +363,7 @@ type PreviewState =
}>;
}>;
businessFlow?: AppProcessBusinessFlowModel;
businessFlowDirection?: AppProcessBusinessFlowDirection;
appProcessDomainPlacement?: ResolvedAppProcessDomainPlacement;
colorScheme?: ResolvedColorScheme;
relatedReferences?: Array<{ label: string; line?: number; ch?: number; count?: number }>;
@ -408,6 +414,7 @@ const DEFAULT_VIEWER_PREFERENCES: ModelWeaveViewerPreferences = {
nodeDensity: "normal",
defaultDomainsViewMode: "mindmap",
defaultDomainDiagramViewMode: "mindmap",
defaultBusinessFlowDirection: "LR",
localSourceRoot: "",
uiLanguage: "auto",
showMermaidRenderDebug: false
@ -461,6 +468,8 @@ export class ModelingPreviewView extends ItemView {
private readonly splitRatioByKey = new Map<string, number>();
private domainsDiagramMode: DomainsMermaidMode = "mindmap";
private domainsDiagramModeFilePath: string | null = null;
private appProcessBusinessFlowDirectionOverride: AppProcessBusinessFlowDirection | null = null;
private appProcessBusinessFlowDirectionFilePath: string | null = null;
private domainsDiagramModeState: "domains" | "domain-diagram" | null = null;
private activeScrollContainer: HTMLElement | null = null;
private focusModeEnabled = false;
@ -654,6 +663,7 @@ export class ModelingPreviewView extends ItemView {
this.persistActiveViewportState();
this.persistCurrentScrollPosition();
this.prepareDomainsDiagramMode(state, nextFilePath);
this.prepareAppProcessBusinessFlowDirection(state, nextFilePath);
this.prepareViewportState(state, reason);
this.state = state;
this.renderCurrentState();
@ -812,6 +822,26 @@ export class ModelingPreviewView extends ItemView {
}
}
private prepareAppProcessBusinessFlowDirection(
state: PreviewState,
nextFilePath: string | null
): void {
if (
state.mode !== "summary" ||
(state.businessFlow?.steps.length ?? 0) === 0
) {
this.appProcessBusinessFlowDirectionOverride = null;
this.appProcessBusinessFlowDirectionFilePath = null;
return;
}
if (this.appProcessBusinessFlowDirectionFilePath === nextFilePath) {
return;
}
this.appProcessBusinessFlowDirectionOverride = null;
this.appProcessBusinessFlowDirectionFilePath = nextFilePath;
}
private prepareDomainsDiagramMode(
state: PreviewState,
nextFilePath: string | null
@ -1005,7 +1035,8 @@ export class ModelingPreviewView extends ItemView {
renderAppProcessBusinessFlow(state.businessFlow!, {
forExport: true,
debug: false,
colorScheme: state.colorScheme
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state)
})
};
}
@ -2099,6 +2130,123 @@ export class ModelingPreviewView extends ItemView {
rightGroup.appendChild(wrapper);
}
private appendAppProcessBusinessFlowDirectionSelector(
container: HTMLElement,
filePath: string
): void {
const toolbar = container.querySelector<HTMLElement>(".mdspec-zoom-toolbar");
if (!toolbar) {
return;
}
toolbar.addClass("model-weave-render-mode-toolbar-host");
toolbar.querySelector(".model-weave-business-flow-direction-select-group")?.remove();
const doc = container.ownerDocument;
const wrapper = doc.createElement("div");
wrapper.className =
"model-weave-business-flow-direction-select-group model-weave-render-mode-row";
const label = doc.createElement("span");
label.addClass("model-weave-render-mode-label");
label.textContent = this.t("appProcess.businessFlow.direction");
wrapper.appendChild(label);
const select = doc.createElement("select");
select.addClass("model-weave-business-flow-direction-select");
for (const direction of ["LR", "TD"] as const) {
const option = doc.createElement("option");
option.value = direction;
option.textContent = this.getAppProcessBusinessFlowDirectionLabel(direction);
option.selected = this.getAppProcessBusinessFlowDirectionForFile(filePath) === direction;
select.appendChild(option);
}
select.addEventListener("change", () => {
this.setAppProcessBusinessFlowDirection(select.value, filePath);
});
wrapper.appendChild(select);
const rightGroup = toolbar.querySelector<HTMLElement>(".model-weave-zoom-toolbar-right") ?? toolbar;
rightGroup.appendChild(wrapper);
}
private getAppProcessBusinessFlowDirectionLabel(
direction: AppProcessBusinessFlowDirection
): string {
return direction === "TD"
? this.t("appProcess.businessFlow.direction.td")
: this.t("appProcess.businessFlow.direction.lr");
}
private getAppProcessBusinessFlowDirection(
state: Extract<PreviewState, { mode: "summary" }>
): AppProcessBusinessFlowDirection {
return resolveAppProcessBusinessFlowDirection({
toolbarOverride: this.getAppProcessBusinessFlowDirectionOverride(state.filePath),
frontmatterDirection: state.businessFlowDirection,
settingsDefaultDirection: this.viewerPreferences.defaultBusinessFlowDirection
});
}
private getAppProcessBusinessFlowDirectionForFile(
filePath: string
): AppProcessBusinessFlowDirection {
const summaryState = this.state.mode === "summary" && this.state.filePath === filePath
? this.state
: null;
return resolveAppProcessBusinessFlowDirection({
toolbarOverride: this.getAppProcessBusinessFlowDirectionOverride(filePath),
frontmatterDirection: summaryState?.businessFlowDirection,
settingsDefaultDirection: this.viewerPreferences.defaultBusinessFlowDirection
});
}
private getAppProcessBusinessFlowDirectionOverride(
filePath: string
): AppProcessBusinessFlowDirection | null {
return this.appProcessBusinessFlowDirectionFilePath === filePath
? this.appProcessBusinessFlowDirectionOverride
: null;
}
private setAppProcessBusinessFlowDirection(
direction: unknown,
filePath: string
): void {
const nextDirection = normalizeAppProcessBusinessFlowDirection(direction);
if (!nextDirection) {
return;
}
if (
this.appProcessBusinessFlowDirectionOverride === nextDirection &&
this.appProcessBusinessFlowDirectionFilePath === filePath
) {
return;
}
const shouldRestoreViewOnly =
this.viewOnlyEnabled &&
this.viewOnlyTarget?.classList.contains("model-weave-app-process-business-flow");
this.appProcessBusinessFlowDirectionOverride = nextDirection;
this.appProcessBusinessFlowDirectionFilePath = filePath;
this.viewportStateCache.delete(filePath);
resetGraphViewportState(this.screenPreviewViewportState);
this.renderCurrentState();
this.restoreCurrentScrollPosition();
if (shouldRestoreViewOnly) {
const view = this.contentEl.ownerDocument.defaultView;
view?.requestAnimationFrame(() => {
view.requestAnimationFrame(() => {
const nextTarget = this.contentEl.querySelector<HTMLElement>(
".model-weave-app-process-business-flow"
);
if (nextTarget) {
this.setViewOnlyMode(true, { target: nextTarget });
}
});
});
}
}
private getDomainDiagramModeLabel(mode: DomainsMermaidMode): string {
if (mode === "mindmap") {
return this.t("domains.preview.mindmap");
@ -2150,6 +2298,7 @@ export class ModelingPreviewView extends ItemView {
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug,
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state),
app: this.app,
interactionSourcePath: state.filePath,
viewportState: this.screenPreviewViewportState,
@ -2159,6 +2308,7 @@ export class ModelingPreviewView extends ItemView {
});
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
this.appendViewerToolbarControls(businessFlowRoot);
this.appendAppProcessBusinessFlowDirectionSelector(businessFlowRoot, state.filePath);
shell.topPane.appendChild(businessFlowRoot);
this.renderSummaryDetails(shell.bottomPane, state, {
suppressBusinessFlowChart: true
@ -2280,6 +2430,7 @@ export class ModelingPreviewView extends ItemView {
const businessFlowRoot = renderAppProcessBusinessFlow(state.businessFlow, {
viewportState: this.screenPreviewViewportState,
colorScheme: state.colorScheme,
flowDirection: this.getAppProcessBusinessFlowDirection(state),
app: this.app,
interactionSourcePath: state.filePath,
...getGraphExportLabels(this.t),
@ -2291,6 +2442,7 @@ export class ModelingPreviewView extends ItemView {
});
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
this.appendViewerToolbarControls(businessFlowRoot);
this.appendAppProcessBusinessFlowDirectionSelector(businessFlowRoot, state.filePath);
section.appendChild(businessFlowRoot);
}

View file

@ -11,7 +11,9 @@ await build({
'export { resolveAppProcessDomainPlacement } from "./src/core/app-process-domain-resolver";',
'export { buildVaultIndex } from "./src/core/vault-index";',
'export { buildCurrentObjectDiagnostics, localizeDiagnosticMessage } from "./src/core/current-file-diagnostics";',
'export { buildAppProcessBusinessFlowMermaidSource, getAppProcessBusinessFlowColorSchemeTargets } from "./src/renderers/app-process-business-flow";'
'export { buildAppProcessBusinessFlowMermaidSource, getAppProcessBusinessFlowColorSchemeTargets } from "./src/renderers/app-process-business-flow";',
'export { resolveAppProcessBusinessFlowDirection } from "./src/core/app-process-business-flow-direction";',
'export { normalizeModelWeaveSettings } from "./src/settings/model-weave-settings";'
].join("\n"),
resolveDir: ".",
sourcefile: "test-app-process-domain-placement-entry.ts",
@ -72,9 +74,28 @@ const {
buildCurrentObjectDiagnostics,
localizeDiagnosticMessage,
buildAppProcessBusinessFlowMermaidSource,
getAppProcessBusinessFlowColorSchemeTargets
getAppProcessBusinessFlowColorSchemeTargets,
resolveAppProcessBusinessFlowDirection,
normalizeModelWeaveSettings
} = await import(`../${outputFile}?t=${Date.now()}`);
function processMarkdownWithFrontmatter(frontmatter, stepsHeader, stepsRows) {
return `---
type: app_process
id: PROC-DIRECTION
name: Direction Process
${frontmatter}
---
# Direction Process
## Steps
${stepsHeader}
${stepsRows}
`;
}
function processMarkdown(stepsHeader, stepsRows, domainSources = "") {
return `---
type: app_process
@ -147,6 +168,75 @@ function parseSteps(stepsHeader, stepsRows, preStepsSections = "") {
return result;
}
test("app_process flow_direction TD renders Business Flow top down", () => {
const parsed = parseAppProcessFile(
processMarkdownWithFrontmatter(
"flow_direction: TD",
"| id | domain | label | kind | input | output | rule | invoke | screen | notes |\n|---|---|---|---|---|---|---|---|---|---|",
"| start | | Start | start | | | | | | |"
),
"model/app_process/PROC-DIRECTION.md"
);
assert.equal(parsed.file?.flowDirection, "TD");
const source = buildAppProcessBusinessFlowMermaidSource(
{
title: parsed.file?.name ?? "Direction Process",
steps: parsed.file?.steps ?? [],
flows: parsed.file?.flows ?? [],
hasExplicitFlows: Boolean(parsed.file?.hasExplicitFlows)
},
undefined,
parsed.file?.flowDirection
);
assert.match(source, /^flowchart TD/);
});
test("app_process invalid flow_direction normalizes to undefined", () => {
const parsed = parseAppProcessFile(
processMarkdownWithFrontmatter(
"flow_direction: RL",
"| id | domain | label | kind | input | output | rule | invoke | screen | notes |\n|---|---|---|---|---|---|---|---|---|---|",
"| start | | Start | start | | | | | | |"
),
"model/app_process/PROC-DIRECTION.md"
);
assert.equal(parsed.file?.flowDirection, undefined);
});
test("app_process Business Flow direction uses settings default when frontmatter is absent", () => {
const settings = normalizeModelWeaveSettings({
defaultBusinessFlowDirection: "TD"
});
const direction = resolveAppProcessBusinessFlowDirection({
settingsDefaultDirection: settings.defaultBusinessFlowDirection
});
assert.equal(direction, "TD");
const source = buildAppProcessBusinessFlowMermaidSource(
{
title: "Direction Process",
steps: [{ id: "start", label: "Start" }],
flows: [],
hasExplicitFlows: false
},
undefined,
direction
);
assert.match(source, /^flowchart TD/);
});
test("app_process Business Flow toolbar override wins over frontmatter and settings", () => {
const direction = resolveAppProcessBusinessFlowDirection({
toolbarOverride: "LR",
frontmatterDirection: "TD",
settingsDefaultDirection: "TD"
});
assert.equal(direction, "LR");
});
test("app_process Transitions.to accepts generic Model Weave asset refs", () => {
const index = buildVaultIndex([
{ path: "PROC-SOURCE.md", content: `---

View file

@ -42,6 +42,46 @@ const { buildAppProcessBusinessFlowMermaidSource } = await import(
`../${outputFile}?t=${Date.now()}`
);
test("app_process Business Flow source defaults to LR direction", () => {
const source = buildAppProcessBusinessFlowMermaidSource({
title: "Direction test",
hasExplicitFlows: false,
steps: [{ id: "start", label: "Start" }],
flows: []
});
assert.match(source, /^flowchart LR/);
});
test("app_process Business Flow source supports TD direction", () => {
const source = buildAppProcessBusinessFlowMermaidSource(
{
title: "Direction test",
hasExplicitFlows: false,
steps: [{ id: "start", label: "Start" }],
flows: []
},
undefined,
"TD"
);
assert.match(source, /^flowchart TD/);
});
test("app_process Business Flow source falls back to LR for unknown direction", () => {
const source = buildAppProcessBusinessFlowMermaidSource(
{
title: "Direction test",
hasExplicitFlows: false,
steps: [{ id: "start", label: "Start" }],
flows: []
},
undefined,
"RL"
);
assert.match(source, /^flowchart LR/);
});
test("app_process Mermaid labels preserve visible punctuation", () => {
const source = buildAppProcessBusinessFlowMermaidSource({
title: "Label escaping verification",

View file

@ -20,7 +20,7 @@ await build({
'export { buildDomainTree } from "./src/core/domain-tree";',
'export { buildDomainRelationshipSummaries } from "./src/core/domain-relationships";',
'export { mergeDomainDiagramSources, resolveDomainDiagram } from "./src/core/domain-diagram-resolver";',
'export { resolveRenderMode } from "./src/core/render-mode";',
'export { getSupportedRenderModes, resolveRenderMode } from "./src/core/render-mode";',
'export { buildImpactSummary } from "./src/core/impact-analyzer";',
'export { buildWeaveMapModel } from "./src/core/weave-map";',
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
@ -117,6 +117,7 @@ const {
buildVaultIndex,
resolveObjectContext,
mergeDomainDiagramSources,
getSupportedRenderModes,
resolveRenderMode,
resolveDomainDiagram,
buildCurrentObjectDiagnostics,
@ -924,6 +925,11 @@ test("English settings labels for Domain view modes do not contain Japanese fixe
assert.equal(/領域|ツリー|マインドマップ|初期表示モード|表示モードです/.test(labels.join(" ")), false);
});
test("Domains render_mode supports domain-specific view modes", () => {
assert.deepEqual(getSupportedRenderModes("domains"), ["mindmap", "area", "tree"]);
assert.deepEqual(getSupportedRenderModes("domain-diagram"), ["mindmap", "area", "tree"]);
});
test("resolves Domains render_mode values without warnings", () => {
for (const [value, expected] of [
["tree", "tree"],