diff --git a/docs/formats/FORMAT-app_process.md b/docs/formats/FORMAT-app_process.md index 5a72504..3ea6a6b 100644 --- a/docs/formats/FORMAT-app_process.md +++ b/docs/formats/FORMAT-app_process.md @@ -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: diff --git a/docs/ja/formats/FORMAT-app_process.md b/docs/ja/formats/FORMAT-app_process.md index 06dff57..acb368f 100644 --- a/docs/ja/formats/FORMAT-app_process.md +++ b/docs/ja/formats/FORMAT-app_process.md @@ -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` とは別の設定です。 + ## セクション 推奨構成: diff --git a/main.js b/main.js index 6089bb9..04f0b05 100644 --- a/main.js +++ b/main.js @@ -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)) { diff --git a/src/core/app-process-business-flow-direction.ts b/src/core/app-process-business-flow-direction.ts new file mode 100644 index 0000000..3ad01c0 --- /dev/null +++ b/src/core/app-process-business-flow-direction.ts @@ -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" + ); +} \ No newline at end of file diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 50c2f0c..3aa01ac 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -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 diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 6f27a9d..6f8e975 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -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 の初期表示モード", diff --git a/src/main.ts b/src/main.ts index 49ba9fa..c940cf3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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")) diff --git a/src/parsers/app-process-parser.ts b/src/parsers/app-process-parser.ts index 7913493..581ebb9 100644 --- a/src/parsers/app-process-parser.ts +++ b/src/parsers/app-process-parser.ts @@ -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) => ({ diff --git a/src/renderers/app-process-business-flow.ts b/src/renderers/app-process-business-flow.ts index f2ecc01..39515cf 100644 --- a/src/renderers/app-process-business-flow.ts +++ b/src/renderers/app-process-business-flow.ts @@ -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(); const stepNodeIdsByStepId = new Map(); @@ -167,7 +174,8 @@ export function buildAppProcessBusinessFlowMermaidSource( } }); - const lines = ["flowchart LR"]; + const normalizedDirection = normalizeAppProcessBusinessFlowDirectionWithFallback(flowDirection); + const lines = [`flowchart ${normalizedDirection}`]; const colorClasses = new Map(); const domainStyles: string[] = []; const nodeClasses: string[] = []; diff --git a/src/settings/model-weave-settings.ts b/src/settings/model-weave-settings.ts index 99cf172..26d5da0 100644 --- a/src/settings/model-weave-settings.ts +++ b/src/settings/model-weave-settings.ts @@ -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, diff --git a/src/types/models.ts b/src/types/models.ts index 2190a52..b786796 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -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[]; diff --git a/src/views/modeling-preview-view.ts b/src/views/modeling-preview-view.ts index 6c0bb64..9ef8c6c 100644 --- a/src/views/modeling-preview-view.ts +++ b/src/views/modeling-preview-view.ts @@ -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(); 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(".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(".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 + ): 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( + ".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); } diff --git a/test/app-process-domain-placement.test.mjs b/test/app-process-domain-placement.test.mjs index 9ed1b0b..b6c6b6b 100644 --- a/test/app-process-domain-placement.test.mjs +++ b/test/app-process-domain-placement.test.mjs @@ -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: `--- diff --git a/test/app-process-mermaid-labels.test.mjs b/test/app-process-mermaid-labels.test.mjs index b9d54b4..bd34e7d 100644 --- a/test/app-process-mermaid-labels.test.mjs +++ b/test/app-process-mermaid-labels.test.mjs @@ -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", diff --git a/test/domains-model.test.mjs b/test/domains-model.test.mjs index 32a9f3a..f6191b5 100644 --- a/test/domains-model.test.mjs +++ b/test/domains-model.test.mjs @@ -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"],