mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
commit
0b2e23dec9
44 changed files with 1990 additions and 228 deletions
11
README-ja.md
11
README-ja.md
|
|
@ -18,13 +18,12 @@ Model Weave は、構造化されたソフトウェアモデリングの機能
|
|||
* **アプリケーションロジック**: 業務フロー(App Process)、画面定義、ビジネスルール。
|
||||
* **影響分析**: 設計全体にわたる関係性や依存関係を自動的に検出。
|
||||
|
||||
## 0.1.19 の主な更新
|
||||
## 0.1.20 の主な更新
|
||||
|
||||
* Flow Diagram MVP として、画面、コンテキスト、プロセス、ストア、セッション、外部システム間の communication / data handoff flow を書く `type: flow_diagram` を追加しました。
|
||||
* Flow Diagram の edge は `kind`, `trigger`, `data`, `condition` による richer semantics と、node / edge label の hover details に対応しました。
|
||||
* Diagnostics は schema-driven な table header guidance、Flow endpoint の local object guidance、DFD / Flow Diagram `Flows.data` の Wikilink diagnostics を改善しました。
|
||||
* Color Scheme preview editing では、`fill`, `stroke`, `text` を swatch と native color picker controls で編集しやすくしました。
|
||||
* Applied Color Scheme は Details tab に compact な `Target | Kind | Preview | Notes | Source` table として表示されます。
|
||||
* Flow Diagram に Detail view と Screen view を追加しました。Viewer の `Flow view` selector で Markdown を変更せず一時切替でき、`flow_view` は初期表示、未指定時は **Default Flow Diagram view** が初期値を指定します。
|
||||
* Screen view は詳細な communication flow から screen、actor、external system、context、message を中心に表示し、内部処理 node を折りたたみます。
|
||||
* Flow Diagram は `## Domain Sources` と local `## Domains` に対応し、親子の nested group と Domain kind ベースの Color Scheme を利用できます。
|
||||
* 新規 Mapping は `target_ref | source_ref | transform | rule | required | notes` の target-first header を使います。既存の source-first Mapping も warning なしで互換読み取りできます。
|
||||
|
||||
## 基本の考え方
|
||||
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -18,13 +18,12 @@ It helps keep design assets human-readable, Git-friendly, and usable as structur
|
|||
* **Application logic**: App Process business flows, Screen definitions, and Business Rules.
|
||||
* **Impact analysis**: Automatically detect relationships and dependencies across the design.
|
||||
|
||||
## Recent updates in 0.1.19
|
||||
## Recent updates in 0.1.20
|
||||
|
||||
* Flow Diagram MVP adds `type: flow_diagram` for screen, context, process, store, session, and external communication/data handoff flows.
|
||||
* Flow Diagram edges support richer `kind`, `trigger`, `data`, and `condition` semantics, with hover details for nodes and edge labels.
|
||||
* Diagnostics now provide schema-driven table-header guidance, local Flow endpoint guidance, and Wikilink diagnostics for DFD / Flow Diagram `Flows.data`.
|
||||
* Color Scheme preview editing adds swatches and native color picker controls for `fill`, `stroke`, and `text`.
|
||||
* Applied Color Scheme now uses a compact Details-tab table with `Target | Kind | Preview | Notes | Source`.
|
||||
* Flow Diagrams add a Screen view alongside Detail view. Use the Viewer `Flow view` selector to temporarily switch views without changing Markdown; `flow_view` selects the initial view and **Default Flow Diagram view** supplies the default when it is absent.
|
||||
* Screen view projects detailed communication flows around screens, actors, external systems, contexts, and messages while folding internal processing nodes.
|
||||
* Flow Diagrams can use `## Domain Sources` and local `## Domains`, including nested parent groups and Domain kind-based Color Scheme rows.
|
||||
* New Mapping files use the target-first header `target_ref | source_ref | transform | rule | required | notes`; existing source-first Mapping files remain compatible without warnings.
|
||||
|
||||
## Basic idea
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ Describe the mapping purpose and scope.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-SAMPLE-SOURCE]].sample_id | [[DATA-SAMPLE-TARGET]].sample_id | Direct copy | | Y | |
|
||||
| [[DATA-SAMPLE-SOURCE]].sample_name | [[DATA-SAMPLE-TARGET]].sample_name | Direct copy | | N | |
|
||||
| [[DATA-SAMPLE-TARGET]].sample_id | [[DATA-SAMPLE-SOURCE]].sample_id | Direct copy | | Y | |
|
||||
| [[DATA-SAMPLE-TARGET]].sample_name | [[DATA-SAMPLE-SOURCE]].sample_name | Direct copy | | N | |
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ Templates help you start new models with the correct frontmatter and section str
|
|||
You can configure the following in **Settings** > **Model Weave**:
|
||||
|
||||
- **Default render mode**: Choose between `Auto`, `Custom`, or `Mermaid`.
|
||||
- **Default Flow Diagram view**: Choose `Detail` or `Screen` as the initial view for Flow Diagrams without `flow_view`. The Viewer `Flow view` selector can temporarily switch the current Flow Diagram without modifying its Markdown.
|
||||
- **Default zoom**: Set the initial zoom state (`Fit` or `100%`).
|
||||
- **Font size**: Adjust text size in the preview.
|
||||
- **Node density**: Control how compact diagrams appear.
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ Current Color Scheme application:
|
|||
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
|
||||
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` matching Domain kind rows |
|
||||
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
|
||||
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> matching Domain kind rows |
|
||||
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
|
||||
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,38 @@ kind: screen_communication
|
|||
| `id` | yes | Unique model id |
|
||||
| `name` | yes | Human-readable name |
|
||||
| `kind` | yes | `screen_communication` for the MVP |
|
||||
| `flow_view` | no | `detail` (default) or `screen` |
|
||||
|
||||
|
||||
## Viewer Settings
|
||||
|
||||
`flow_view` selects the initial view only when a Flow Diagram is first opened in the Viewer. After that, the toolbar selection is kept as Viewer state for that file and does not modify Markdown.
|
||||
|
||||
When `flow_view` is absent or invalid, **Default Flow Diagram view** in plugin settings selects the initial mode. Its default is `detail`.
|
||||
|
||||
## Domain Sources / Domains
|
||||
|
||||
`flow_diagram` may use the same Domain placement support as DFD and App Process views.
|
||||
|
||||
Use `## Domain Sources` to reference reusable `type: domains` files:
|
||||
|
||||
```markdown
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
```
|
||||
|
||||
Use local `## Domains` when the Flow Diagram owns or overrides Domain definitions:
|
||||
|
||||
```markdown
|
||||
| id | name | kind | parent | description |
|
||||
|---|---|---|---|---|
|
||||
```
|
||||
|
||||
`Objects.domain` refers to a Domain `id`. When Domain Sources or local Domains are present, Model Weave resolves the Domain `name`, `kind`, and `parent` and uses them for nested Mermaid domain groups. Domain group colors use Color Scheme rows with `target=domain` and `kind=<Domains.kind>`.
|
||||
|
||||
If neither Domain Sources nor local Domains are present, `Objects.domain` remains a lightweight raw group key for compatibility. In that mode, unknown Domain warnings are not produced and the raw value is used as the group label and Color Scheme kind.
|
||||
|
||||
If Domain Sources or local Domains are present, an `Objects.domain` value that cannot be resolved to a Domain `id` produces a warning.
|
||||
|
||||
## Objects
|
||||
|
||||
|
|
@ -64,6 +96,13 @@ Supported MVP object kinds:
|
|||
| `store` | `lin-cyl` | `store` |
|
||||
| `datastore` | `lin-cyl` | `store` |
|
||||
| `external` | `rect` | `external` |
|
||||
| `actor` | `rect` | `external` |
|
||||
| `user` | `rect` | `external` |
|
||||
| `message` | `rect` | `context` |
|
||||
| `data` | `rect` | `process` |
|
||||
| `api` | `rect` | `process` |
|
||||
| `service` | `rect` | `process` |
|
||||
| `handler` | `rect` | `process` |
|
||||
| unknown values | `rect` | fallback |
|
||||
|
||||
`Objects.ref` may point to `screen`, `app_process`, `data_object`, `dfd_object`, or another model asset. Unknown object kinds should not block rendering.
|
||||
|
|
@ -84,9 +123,17 @@ Mermaid edge labels are assembled compactly from `trigger`, `kind`, and `data`.
|
|||
|
||||
## Rendering
|
||||
|
||||
The MVP uses Internal Detail View only and renders the raw Objects / Flows graph without projection, folding, or view selectors.
|
||||
Flow Diagram supports two MVP view modes.
|
||||
|
||||
Surface View, Communication View, projection, context/process/store folding, transition coverage, and automatic generation from screen or app_process models are future work.
|
||||
`detail` is the default view and renders the raw Objects / Flows graph. It keeps internal processes, contexts, stores, external systems, and all declared flow edges visible.
|
||||
|
||||
`screen` is a Screen Flow projection. It keeps user-visible objects with kind `screen`, `external`, `actor`, `user`, `context`, or `message`, and folds internal objects such as `work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, and `handler`. Edges are projected as reachability between visible objects. If a diagram has only folded/internal objects, rendering falls back to the Detail view so the graph is not empty.
|
||||
|
||||
Screen Flow edge labels are taken from the incoming flow to the visible target, using this priority: `condition`, then `trigger`, then `kind`. When multiple projected paths produce the same visible source/target pair, labels are merged with ` / `.
|
||||
|
||||
Screen Flow is a derived rendering projection. It does not change the source Markdown, does not create data nodes from `Flows.data`, and does not replace the detailed communication model.
|
||||
|
||||
`flow_view` supplies the Viewer initial view when no current Viewer state exists for the file. The Viewer toolbar can temporarily switch between Detail and Screen without changing the Markdown file. Detail is for inspecting the full communication graph; Screen is for following the projected handoff flow between user-visible objects.
|
||||
|
||||
## AI Generation Notes
|
||||
|
||||
|
|
@ -95,4 +142,5 @@ Surface View, Communication View, projection, context/process/store folding, tra
|
|||
- Keep table headers exactly as documented.
|
||||
- Use local `Objects.id` values in `Flows.from` and `Flows.to`.
|
||||
- Put edge semantics in `Flows.kind`, user/system events in `Flows.trigger`, payloads or data references in `Flows.data`, and guards in `Flows.condition`.
|
||||
- Use `flow_view: screen` only when the same detailed graph should default to Screen Flow projection.
|
||||
- Do not add generated state matrices, folding rules, or automatic screen/process derivations.
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ Maps order request fields to the order entity.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | |
|
||||
| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | |
|
||||
```
|
||||
|
||||
## Full example
|
||||
|
|
@ -128,14 +128,14 @@ Maps an external inventory CSV file to inventory-related database entities.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | [[ENT-INVENTORY]].shipper_id | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | [[ENT-INVENTORY]].warehouse_id | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].item_code | [[ENT-INVENTORY]].item_id | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-INVENTORY]].quantity | parse decimal | | Y | Convert text to numeric |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].status | [[ENT-INVENTORY]].inventory_status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-STOCK-MOVEMENT]].movement_quantity | copy when quantity is positive | | N | Create movement row only when quantity exists |
|
||||
| [[ENT-INVENTORY]].shipper_id | [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].warehouse_id | [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].item_id | [[DATA-INVENTORY-IMPORT-FILE]].item_code | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | parse decimal | | Y | Convert text to numeric |
|
||||
| [[ENT-INVENTORY]].inventory_status | [[DATA-INVENTORY-IMPORT-FILE]].status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value |
|
||||
| [[ENT-STOCK-MOVEMENT]].movement_quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | copy when quantity is positive | | N | Create movement row only when quantity exists |
|
||||
|
||||
## Source Links
|
||||
|
||||
|
|
@ -245,16 +245,18 @@ Use `## Mappings` to define field-level mapping rows.
|
|||
Expected header:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
```
|
||||
|
||||
The legacy source-first header `source_ref | target_ref | transform | rule | required | notes` remains supported without diagnostics for existing files, but new files should use the target-first order above.
|
||||
|
||||
Columns:
|
||||
|
||||
| column | meaning |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| `source_ref` | Source field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. |
|
||||
| `target_ref` | Target field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. |
|
||||
| `source_ref` | Source field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. |
|
||||
| `transform` | Transformation description. Use simple text such as `copy`, `format date`, `lookup`, or `convert status`. |
|
||||
| `rule` | Optional related `rule` reference when transformation is governed by a rule model. |
|
||||
| `required` | Whether the mapping is mandatory. Use `Y` or `N`. |
|
||||
|
|
@ -265,11 +267,11 @@ Example:
|
|||
```markdown
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | |
|
||||
| [[DATA-ORDER-REQUEST]].status | [[ENT-ORDER]].order_status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion |
|
||||
| current_user.user_id | [[ENT-ORDER]].created_by | copy | | N | Audit field |
|
||||
| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].order_status | [[DATA-ORDER-REQUEST]].status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion |
|
||||
| [[ENT-ORDER]].created_by | current_user.user_id | copy | | N | Audit field |
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
|
@ -325,7 +327,7 @@ Do not add unsupported columns to structured tables just to store extra informat
|
|||
### Mappings table
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
```
|
||||
|
||||
|
|
@ -452,7 +454,7 @@ Avoid using display labels as mapping endpoints.
|
|||
Risky:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Customer ID | Customer ID | copy | | Y | ambiguous |
|
||||
```
|
||||
|
|
@ -460,9 +462,9 @@ Risky:
|
|||
Prefer stable qualified references:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | |
|
||||
```
|
||||
|
||||
### Confusing mapping with app_process
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ Model Weave のすべての機能は、Obsidian のコマンドパレットか
|
|||
- **UI Language**: ビューアーのキャプションの言語(English または 日本語)を切り替えます。
|
||||
- **Local source root**: Obsidian Vault 外のファイルへの `## Source Links`(相対パス)を解決するためのベースパス。
|
||||
- **Default render mode**: `Custom`(詳細)または `Mermaid`(概要)レンダリングの優先設定。
|
||||
- **Default Flow Diagram view**: `flow_view` が未指定の Flow Diagram の初期表示を `Detail` または `Screen` から選びます。Viewer の `Flow view` selector では、Markdown を変更せずに現在の Flow Diagram を一時切替できます。
|
||||
- **Relationship View**: 「Impact / Relationships(影響/関連)」分析パネルの有効/無効。
|
||||
|
||||
### 次に読むもの
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ ref が解決できない場合、または対象ファイルが `type: color_sc
|
|||
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
|
||||
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
|
||||
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` に対応する Domain kind rows |
|
||||
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
|
||||
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> に対応する Domain kind rows |
|
||||
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
|
||||
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,38 @@ kind: screen_communication
|
|||
| `id` | yes | 一意な model id |
|
||||
| `name` | yes | 表示名 |
|
||||
| `kind` | yes | MVPでは `screen_communication` |
|
||||
| `flow_view` | no | `detail` (default) または `screen` |
|
||||
|
||||
|
||||
## Viewer Settings
|
||||
|
||||
`flow_view` は Flow Diagram を Viewer で最初に開くときだけ初期 view を指定します。その後は toolbar の選択がファイルごとの Viewer state として保持され、Markdown は変更されません。
|
||||
|
||||
`flow_view` が未指定または不正な場合は、plugin settings の **Default Flow Diagram view** を初期値に使います。既定値は `detail` です。
|
||||
|
||||
## Domain Sources / Domains
|
||||
|
||||
`flow_diagram` では、DFD や App Process view と同じ Domain placement support を利用できます。
|
||||
|
||||
`## Domain Sources` は、再利用可能な `type: domains` ファイルを参照します。
|
||||
|
||||
```markdown
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
```
|
||||
|
||||
Flow Diagram 側で Domain 定義を持つ、または上書きする場合は local `## Domains` を使います。
|
||||
|
||||
```markdown
|
||||
| id | name | kind | parent | description |
|
||||
|---|---|---|---|---|
|
||||
```
|
||||
|
||||
`Objects.domain` は Domain `id` を参照します。Domain Sources または local Domains がある場合、Model Weave は Domain の `name`、`kind`、`parent` を解決し、nested Mermaid domain group に使います。Domain group の配色には `target=domain` と `kind=<Domains.kind>` の Color Scheme row を使います。
|
||||
|
||||
Domain Sources も local Domains も無い場合、互換性のため `Objects.domain` は lightweight な raw group key として扱います。この mode では unknown Domain warning は出さず、raw value を group label と Color Scheme kind に使います。
|
||||
|
||||
Domain Sources または local Domains がある場合、Domain `id` として解決できない `Objects.domain` は warning になります。
|
||||
|
||||
## Objects
|
||||
|
||||
|
|
@ -64,6 +96,13 @@ MVPで対応する object kind:
|
|||
| `store` | `lin-cyl` | `store` |
|
||||
| `datastore` | `lin-cyl` | `store` |
|
||||
| `external` | `rect` | `external` |
|
||||
| `actor` | `rect` | `external` |
|
||||
| `user` | `rect` | `external` |
|
||||
| `message` | `rect` | `context` |
|
||||
| `data` | `rect` | `process` |
|
||||
| `api` | `rect` | `process` |
|
||||
| `service` | `rect` | `process` |
|
||||
| `handler` | `rect` | `process` |
|
||||
| unknown values | `rect` | fallback |
|
||||
|
||||
`Objects.ref` は `screen`, `app_process`, `data_object`, `dfd_object` または他の model asset を参照できます。未知の object kind はレンダリングを止めません。
|
||||
|
|
@ -84,9 +123,17 @@ Mermaid edge label は `trigger`、`kind`、`data` から compact に組み立
|
|||
|
||||
## Rendering
|
||||
|
||||
MVPでは Internal Detail View のみを使います。Objects / Flows の raw graph を表示し、projection、folding、view selector は実装しません。
|
||||
Flow Diagram は MVP として2つの view mode を持ちます。
|
||||
|
||||
Surface View、Communication View、projection、context/process/store folding、transition coverage、screen や app_process からの自動生成は将来対応です。
|
||||
`detail` は既定の view です。Objects / Flows の raw graph を表示し、内部 process、context、store、external system、宣言された flow edge をそのまま表示します。
|
||||
|
||||
`screen` は Screen Flow projection です。`screen`, `external`, `actor`, `user`, `context`, `message` のような user-visible object を残し、`work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, `handler` などの内部 object を折りたたみます。Edge は表示対象 object 間の到達関係として投影されます。折りたたみ対象の内部 object しか無い diagram では、空表示にせず Detail view に fallback します。
|
||||
|
||||
Screen Flow の edge label は、表示対象 target へ入る flow から `condition`、`trigger`、`kind` の優先順で作ります。同じ visible source / target に複数 path が畳み込まれる場合、label は ` / ` で結合されます。
|
||||
|
||||
Screen Flow は派生レンダリング projection です。source Markdown は変更せず、`Flows.data` から data node を作らず、詳細な communication model を置き換えるものではありません。
|
||||
|
||||
`flow_view` は、そのファイルの現在の Viewer state が無い場合に初期 view を選びます。Viewer toolbar では Markdown を変更せずに Detail と Screen を一時切り替えできます。Detail は communication graph 全体の確認、Screen は user-visible object 間の投影された handoff flow の確認に使います。
|
||||
|
||||
## AI生成時の注意
|
||||
|
||||
|
|
@ -95,4 +142,5 @@ Surface View、Communication View、projection、context/process/store folding
|
|||
- テーブルヘッダーを仕様通りに保つ。
|
||||
- `Flows.from` と `Flows.to` にはローカル `Objects.id` を使う。
|
||||
- edge semantics は `Flows.kind`、user/system event は `Flows.trigger`、payload や data reference は `Flows.data`、guard は `Flows.condition` に書く。
|
||||
- 同じ詳細 graph を Screen Flow projection で既定表示したい場合だけ `flow_view: screen` を使う。
|
||||
- state matrix、folding rule、自動生成された screen/process derivation は追加しない。
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ Maps order request fields to the order entity.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | |
|
||||
| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | |
|
||||
```
|
||||
|
||||
## 詳細例
|
||||
|
|
@ -128,14 +128,14 @@ Maps an external inventory CSV file to inventory-related database entities.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | [[ENT-INVENTORY]].shipper_id | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | [[ENT-INVENTORY]].warehouse_id | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].item_code | [[ENT-INVENTORY]].item_id | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-INVENTORY]].quantity | parse decimal | | Y | Convert text to numeric |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].status | [[ENT-INVENTORY]].inventory_status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value |
|
||||
| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-STOCK-MOVEMENT]].movement_quantity | copy when quantity is positive | | N | Create movement row only when quantity exists |
|
||||
| [[ENT-INVENTORY]].shipper_id | [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].warehouse_id | [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].item_id | [[DATA-INVENTORY-IMPORT-FILE]].item_code | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion |
|
||||
| [[ENT-INVENTORY]].quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | parse decimal | | Y | Convert text to numeric |
|
||||
| [[ENT-INVENTORY]].inventory_status | [[DATA-INVENTORY-IMPORT-FILE]].status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value |
|
||||
| [[ENT-STOCK-MOVEMENT]].movement_quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | copy when quantity is positive | | N | Create movement row only when quantity exists |
|
||||
|
||||
## Source Links
|
||||
|
||||
|
|
@ -245,16 +245,18 @@ tags:
|
|||
期待されるヘッダー:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
```
|
||||
|
||||
旧形式の source-first header `source_ref | target_ref | transform | rule | required | notes` は既存ファイル互換のため診断なしで読み取れます。ただし新規ファイルでは上記の target-first order を使ってください。
|
||||
|
||||
列の意味:
|
||||
|
||||
| column | meaning |
|
||||
| ------------ | ----------------------------------------------------------------------- |
|
||||
| `source_ref` | source field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 |
|
||||
| `target_ref` | target field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 |
|
||||
| `source_ref` | source field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 |
|
||||
| `transform` | 変換内容です。`copy`, `format date`, `lookup`, `convert status` などの短い説明を使います。 |
|
||||
| `rule` | 変換がrule modelに従う場合の任意の関連 `rule` 参照です。 |
|
||||
| `required` | mappingが必須かどうかを示します。`Y` または `N` を使います。 |
|
||||
|
|
@ -265,11 +267,11 @@ tags:
|
|||
```markdown
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | |
|
||||
| [[DATA-ORDER-REQUEST]].status | [[ENT-ORDER]].order_status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion |
|
||||
| current_user.user_id | [[ENT-ORDER]].created_by | copy | | N | Audit field |
|
||||
| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].order_status | [[DATA-ORDER-REQUEST]].status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion |
|
||||
| [[ENT-ORDER]].created_by | current_user.user_id | copy | | N | Audit field |
|
||||
```
|
||||
|
||||
注意:
|
||||
|
|
@ -326,7 +328,7 @@ mappingを、実装ファイル、インターフェース仕様、ETLスクリ
|
|||
### Mappings table
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
```
|
||||
|
||||
|
|
@ -453,7 +455,7 @@ FORMATが明示的に定義していない限り、`id`, `source`, `target`, `co
|
|||
危険な例:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Customer ID | Customer ID | copy | | Y | ambiguous |
|
||||
```
|
||||
|
|
@ -461,9 +463,9 @@ FORMATが明示的に定義していない限り、`id`, `source`, `target`, `co
|
|||
推奨:
|
||||
|
||||
```markdown
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | |
|
||||
| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | |
|
||||
```
|
||||
|
||||
### mappingとapp_processを混同する
|
||||
|
|
|
|||
39
docs/releases/v0.1.20.md
Normal file
39
docs/releases/v0.1.20.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Model Weave v0.1.20
|
||||
|
||||
Model Weave v0.1.20 adds a screen-focused Flow Diagram projection, domain-aware Flow Diagram grouping, and a target-first Mapping authoring format.
|
||||
|
||||
## Highlights
|
||||
|
||||
### Screen-focused Flow Diagram view
|
||||
|
||||
- Flow Diagrams now provide `Detail` and `Screen` views.
|
||||
- Use the Viewer `Flow view` selector to switch views temporarily; it does not modify Markdown.
|
||||
- Screen view folds internal processing nodes and projects communication around screens, actors, external systems, contexts, and messages.
|
||||
- `flow_view` selects the initial Flow Diagram view for a file.
|
||||
- **Default Flow Diagram view** selects the initial view when `flow_view` is absent or invalid.
|
||||
|
||||
### Domain-aware Flow Diagrams
|
||||
|
||||
- Flow Diagrams support reusable `## Domain Sources` and local `## Domains`.
|
||||
- Resolved Domain names label groups, and `parent` relationships create nested groups.
|
||||
- Domain kind-based Color Scheme rows apply to Flow Diagram domain groups.
|
||||
- Existing Flow Diagrams without Domain definitions continue to use their raw `Objects.domain` groups.
|
||||
|
||||
### Target-first Mapping format
|
||||
|
||||
- New Mapping files use `target_ref | source_ref | transform | rule | required | notes`.
|
||||
- Legacy source-first Mapping headers remain supported without warnings.
|
||||
- Target-first and source-first Mapping files can coexist in the same vault.
|
||||
- Internal `source_ref` and `target_ref` semantics are unchanged.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Existing Mapping files do not need to be rewritten.
|
||||
- Existing Flow Diagrams without Domain definitions continue to render as before.
|
||||
- Flow Diagrams default to Detail view when no view is specified.
|
||||
- Viewer toolbar changes do not modify Markdown.
|
||||
- Dogfood Mapping column-order updates are intentionally not included in this release.
|
||||
|
||||
## Validation
|
||||
|
||||
- Run the full automated test, TypeScript, lint, CSS compatibility, and production build checks before release.
|
||||
542
main.js
542
main.js
|
|
@ -224,6 +224,15 @@ function formatDfdObjectUnknownDomainMessage(objectId, domainId) {
|
|||
function formatDfdObjectDomainWithoutLocalDomainsMessage(objectId, domainId) {
|
||||
return `DFD object "${objectId}" references Domain "${domainId}", but this DFD has no local Domains.`;
|
||||
}
|
||||
function formatFlowDiagramLocalDomainOverridesSourceMessage(id, field, localValue, sourceValue) {
|
||||
return `Flow Diagram local Domain "${id}" overrides Domain Source ${field} "${sourceValue}" with "${localValue}".`;
|
||||
}
|
||||
function formatFlowDiagramObjectUnknownLocalDomainMessage(objectId, domainId) {
|
||||
return `Flow Diagram object "${objectId}" references unknown local Domain "${domainId}".`;
|
||||
}
|
||||
function formatFlowDiagramObjectUnknownDomainMessage(objectId, domainId) {
|
||||
return `Flow Diagram object "${objectId}" references unknown Domain "${domainId}".`;
|
||||
}
|
||||
function formatStandaloneDomainDuplicateMessage(id) {
|
||||
return `Domain "${id}" is defined in multiple Domains files.`;
|
||||
}
|
||||
|
|
@ -1613,6 +1622,8 @@ var SECTION_HEADERS = {
|
|||
"source links": SOURCE_LINKS_HEADER
|
||||
},
|
||||
"flow-diagram": {
|
||||
"domain sources": DOMAIN_SOURCES_HEADER,
|
||||
domains: DOMAIN_HEADER,
|
||||
objects: "id | label | kind | ref | domain | notes",
|
||||
flows: "id | from | to | kind | trigger | data | condition | notes",
|
||||
"source links": SOURCE_LINKS_HEADER
|
||||
|
|
@ -1633,7 +1644,7 @@ var SECTION_HEADERS = {
|
|||
},
|
||||
mapping: {
|
||||
scope: "role | ref | notes",
|
||||
mappings: "source_ref | target_ref | transform | rule | required | notes",
|
||||
mappings: "target_ref | source_ref | transform | rule | required | notes",
|
||||
"source links": SOURCE_LINKS_HEADER
|
||||
},
|
||||
message: {
|
||||
|
|
@ -3051,6 +3062,7 @@ function localizeDiagnosticMessage(message, language) {
|
|||
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `\u4E88\u7D04\u6E08\u307F kind "${kind}" \u304C\u4F7F\u308F\u308C\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} \u3067\u306F ${renderer} renderer \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
||||
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" \u306F\u4E0D\u660E\u3067\u3059\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
||||
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view \u306F "detail" \u307E\u305F\u306F "screen" \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'],
|
||||
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" \u306F\u901A\u5E38\u3068\u7570\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`],
|
||||
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" \u306F\u81EA\u5DF1\u30EB\u30FC\u30D7\u3067\u3059\u3002`],
|
||||
[/^Domain id is required\.$/, "Domain \u306E id \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
||||
|
|
@ -3077,6 +3089,9 @@ function localizeDiagnosticMessage(message, language) {
|
|||
[/^DFD-local Domain "([^"]+)" overrides Domain Source (name|kind|parent) "([^"]*)" with "([^"]*)"\.$/, (_match, domain, field, source, local) => `DFD\u5185\u306E Domain "${domain}" \u306F Domain Source \u306E ${field} "${source}" \u3092 "${local}" \u3067\u4E0A\u66F8\u304D\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^DFD object "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E\u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^DFD object "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^Flow Diagram local Domain "([^"]+)" overrides Domain Source (name|kind|parent) "([^"]*)" with "([^"]*)"\.$/, (_match, domain, field, source, local) => `Flow Diagram \u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u306F Domain Source \u306E ${field} "${source}" \u3092 "${local}" \u3067\u4E0A\u66F8\u304D\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^Flow Diagram object "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, object, domain) => `Flow Diagram object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E\u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^Flow Diagram object "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, object, domain) => `Flow Diagram object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
||||
[/^DFD object "([^"]+)" references Domain "([^"]+)", but this DFD has no local Domains\.$/, (_match, object, domain) => `DFD object "${object}" \u304C Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u304C\u3001\u3053\u306E DFD \u306B\u306F\u30ED\u30FC\u30AB\u30EB Domains \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
||||
[/^DFD local object "([^"]+)" is treated as an inline object without ref\.$/, (_match, object) => `DFD local object "${object}" \u306F ref \u306A\u3057\u306E\u56F3\u5185\u5B9A\u7FA9\u3068\u3057\u3066\u6271\u308F\u308C\u307E\u3059\u3002`],
|
||||
[/^DFD object "([^"]+)" has no kind, and it could not be inferred from ref\.$/, (_match, object) => `DFD object "${object}" \u306E kind \u304C\u306A\u304F\u3001ref \u304B\u3089\u3082\u63A8\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
||||
|
|
@ -4677,15 +4692,15 @@ function resolveErDiagramRelations(diagram, index) {
|
|||
function resolveDfdDiagramRelations(diagram, index) {
|
||||
const warnings = [];
|
||||
const isFlowDiagram = diagram.schema === "flow_diagram";
|
||||
const domainResolution = isFlowDiagram ? { warnings: [], sourceSummaries: [], domains: [] } : resolveDfdDiagramDomains(diagram, index);
|
||||
const domainResolution = resolveDfdDiagramDomains(diagram, index);
|
||||
warnings.push(...domainResolution.warnings);
|
||||
const resolvedDiagram = isFlowDiagram ? diagram : {
|
||||
const resolvedDiagram = {
|
||||
...diagram,
|
||||
domainSourceSummaries: domainResolution.sourceSummaries,
|
||||
domains: domainResolution.domains
|
||||
};
|
||||
const objectResolution = resolveDfdDiagramObjects(resolvedDiagram, index, {
|
||||
hasDomainSources: diagram.schema === "dfd_diagram" && diagram.domainSources.length > 0
|
||||
hasDomainSources: diagram.domainSources.length > 0
|
||||
});
|
||||
const hasUnreadableFlowObjects = isFlowDiagram && hasInvalidDfdLikeSectionHeader(diagram.path, index, "Objects");
|
||||
const edges = [];
|
||||
|
|
@ -4888,7 +4903,12 @@ function resolveDfdDiagramDomains(diagram, index) {
|
|||
if (localValue && sourceValue && localValue !== sourceValue) {
|
||||
warnings.push({
|
||||
code: "invalid-structure",
|
||||
message: formatDfdLocalDomainOverridesSourceMessage(
|
||||
message: diagram.schema === "flow_diagram" ? formatFlowDiagramLocalDomainOverridesSourceMessage(
|
||||
domain.id,
|
||||
field,
|
||||
localValue,
|
||||
sourceValue
|
||||
) : formatDfdLocalDomainOverridesSourceMessage(
|
||||
domain.id,
|
||||
field,
|
||||
localValue,
|
||||
|
|
@ -4928,7 +4948,7 @@ function resolveDfdDiagramObjects(diagram, index, domainContext) {
|
|||
rowIndex,
|
||||
compatibilityMode: "legacy_ref_only"
|
||||
}));
|
||||
const localDomainIds = new Set((diagram.schema === "dfd_diagram" ? diagram.domains ?? [] : []).map((domain) => domain.id));
|
||||
const localDomainIds = new Set((diagram.domains ?? []).map((domain) => domain.id));
|
||||
for (const entry of entries) {
|
||||
const ref = entry.ref?.trim();
|
||||
const resolvedObject = ref ? resolveDfdObjectReference(ref, index) ?? void 0 : void 0;
|
||||
|
|
@ -4981,10 +5001,16 @@ function resolveDfdDiagramObjects(diagram, index, domainContext) {
|
|||
field: "Objects.domain",
|
||||
context: { rowIndex: entry.rowIndex + 1 }
|
||||
});
|
||||
} else if (diagram.schema === "dfd_diagram" && domain && !localDomainIds.has(domain)) {
|
||||
} else if (domain && !localDomainIds.has(domain) && (diagram.schema === "dfd_diagram" || localDomainIds.size > 0 || domainContext.hasDomainSources)) {
|
||||
warnings.push({
|
||||
code: "unresolved-reference",
|
||||
message: domainContext.hasDomainSources ? formatDfdObjectUnknownDomainMessage(
|
||||
message: diagram.schema === "flow_diagram" ? domainContext.hasDomainSources ? formatFlowDiagramObjectUnknownDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
) : formatFlowDiagramObjectUnknownLocalDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
) : domainContext.hasDomainSources ? formatDfdObjectUnknownDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
) : formatDfdObjectUnknownLocalDomainMessage(
|
||||
|
|
@ -7637,32 +7663,36 @@ function getMappingCompletion(lines, cursor, line, index) {
|
|||
}
|
||||
}
|
||||
if (section === "Mappings") {
|
||||
if (!hasTableHeader(lines, cursor.line, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])) {
|
||||
const mappingHeader = getNearestSupportedMappingHeader(lines, cursor.line);
|
||||
if (!mappingHeader) {
|
||||
return null;
|
||||
}
|
||||
if (cell.columnIndex === 0 || cell.columnIndex === 1) {
|
||||
const columnName = mappingHeader[cell.columnIndex];
|
||||
if (columnName === "source_ref" || columnName === "target_ref") {
|
||||
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
||||
const isSourceRef = columnName === "source_ref";
|
||||
const placeholder = isSourceRef ? "Complete mapping source_ref" : "Complete mapping target_ref";
|
||||
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
||||
cursor,
|
||||
cell,
|
||||
cellValue,
|
||||
index,
|
||||
cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref"
|
||||
placeholder
|
||||
);
|
||||
if (qualifiedMemberRequest) {
|
||||
return qualifiedMemberRequest;
|
||||
}
|
||||
return {
|
||||
kind: cell.columnIndex === 0 ? "mapping-source-ref" : "mapping-target-ref",
|
||||
kind: isSourceRef ? "mapping-source-ref" : "mapping-target-ref",
|
||||
replaceFrom: cell.replaceFrom,
|
||||
replaceTo: cell.replaceTo,
|
||||
suggestions: buildStructuredReferenceSuggestions(index),
|
||||
placeholder: cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref",
|
||||
placeholder,
|
||||
initialQuery: normalizeCompletionQuery(cellValue),
|
||||
tableColumnIndex: cell.columnIndex
|
||||
};
|
||||
}
|
||||
if (cell.columnIndex === 3) {
|
||||
if (columnName === "rule") {
|
||||
return {
|
||||
kind: "mapping-rule-ref",
|
||||
replaceFrom: cell.replaceFrom,
|
||||
|
|
@ -7675,17 +7705,17 @@ function getMappingCompletion(lines, cursor, line, index) {
|
|||
initialQuery: normalizeCompletionQuery(
|
||||
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
||||
),
|
||||
tableColumnIndex: 3
|
||||
tableColumnIndex: cell.columnIndex
|
||||
};
|
||||
}
|
||||
if (cell.columnIndex === 4) {
|
||||
if (columnName === "required") {
|
||||
return buildOptionCompletionRequest(
|
||||
"mapping-rule-ref",
|
||||
cell,
|
||||
line,
|
||||
["Y", "N"],
|
||||
"Complete mapping required",
|
||||
4
|
||||
cell.columnIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7827,6 +7857,25 @@ function getScreenFieldTargetSuggestions(lines) {
|
|||
kind: "reference"
|
||||
}));
|
||||
}
|
||||
function getNearestSupportedMappingHeader(lines, cursorLine) {
|
||||
const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => {
|
||||
const row = parseMarkdownTableRow(candidate);
|
||||
return isSupportedMappingHeader(row);
|
||||
});
|
||||
if (tableHeaderIndex < 0 || cursorLine <= tableHeaderIndex + 1) {
|
||||
return null;
|
||||
}
|
||||
return parseMarkdownTableRow(lines[tableHeaderIndex] ?? "");
|
||||
}
|
||||
function isSupportedMappingHeader(header) {
|
||||
if (!header) {
|
||||
return false;
|
||||
}
|
||||
return sameOrderedHeaders(header, ["target_ref", "source_ref", "transform", "rule", "required", "notes"]) || sameOrderedHeaders(header, ["source_ref", "target_ref", "transform", "rule", "required", "notes"]);
|
||||
}
|
||||
function sameOrderedHeaders(actual, expected) {
|
||||
return actual.length === expected.length && expected.every((header, index) => actual[index] === header);
|
||||
}
|
||||
function hasTableHeader(lines, cursorLine, expectedHeader) {
|
||||
const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => {
|
||||
const row = parseMarkdownTableRow(candidate);
|
||||
|
|
@ -9926,6 +9975,7 @@ var DEFAULT_MODEL_WEAVE_SETTINGS = {
|
|||
defaultProcessRenderMode: "custom",
|
||||
defaultBusinessFlowDirection: "LR",
|
||||
defaultScreenRenderMode: "custom",
|
||||
defaultFlowDiagramViewMode: "detail",
|
||||
defaultDomainsViewMode: "mindmap",
|
||||
defaultDomainDiagramViewMode: "mindmap",
|
||||
defaultZoom: "fit",
|
||||
|
|
@ -9971,6 +10021,10 @@ var VALID_DOMAIN_VIEW_MODES = /* @__PURE__ */ new Set([
|
|||
"area",
|
||||
"tree"
|
||||
]);
|
||||
var VALID_FLOW_DIAGRAM_VIEW_MODES = /* @__PURE__ */ new Set([
|
||||
"detail",
|
||||
"screen"
|
||||
]);
|
||||
var VALID_UI_LANGUAGES = /* @__PURE__ */ new Set(["auto", "en", "ja"]);
|
||||
function normalizeModelWeaveSettings(value) {
|
||||
const raw = isRecord(value) ? value : {};
|
||||
|
|
@ -10008,6 +10062,11 @@ function normalizeModelWeaveSettings(value) {
|
|||
SCREEN_RENDER_MODES,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS.defaultScreenRenderMode
|
||||
),
|
||||
defaultFlowDiagramViewMode: normalizeEnumValue(
|
||||
raw.defaultFlowDiagramViewMode,
|
||||
VALID_FLOW_DIAGRAM_VIEW_MODES,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS.defaultFlowDiagramViewMode
|
||||
),
|
||||
defaultDomainsViewMode: normalizeEnumValue(
|
||||
raw.defaultDomainsViewMode,
|
||||
VALID_DOMAIN_VIEW_MODES,
|
||||
|
|
@ -10584,7 +10643,7 @@ tags:
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## Rules
|
||||
|
|
@ -11673,6 +11732,13 @@ var DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"];
|
|||
var LEGACY_OBJECT_HEADERS = ["ref", "notes"];
|
||||
var FLOW_OBJECT_KINDS = /* @__PURE__ */ new Set([
|
||||
"screen",
|
||||
"actor",
|
||||
"user",
|
||||
"message",
|
||||
"data",
|
||||
"api",
|
||||
"service",
|
||||
"handler",
|
||||
"process",
|
||||
"app_process",
|
||||
"context",
|
||||
|
|
@ -11717,6 +11783,9 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
||||
const level = typeof frontmatter.level === "string" || typeof frontmatter.level === "number" ? String(frontmatter.level).trim() : void 0;
|
||||
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim() ? frontmatter.kind.trim() : options.defaultKind;
|
||||
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
|
||||
const flowViewRaw = frontmatter.flow_view;
|
||||
const flowViewSpecified = options.schema === "flow_diagram" && !isUnknownFlowDiagramViewMode(frontmatter.flow_view) && typeof frontmatter.flow_view === "string" && frontmatter.flow_view.trim().length > 0;
|
||||
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
|
||||
const isAcceptedType = rawType === options.type || options.type === "flow_diagram" && rawType === "flow-diagram";
|
||||
if (!isAcceptedType) {
|
||||
|
|
@ -11731,13 +11800,16 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
|
||||
warnings.push(createWarning8(path2, "kind", 'expected kind "screen_communication"'));
|
||||
}
|
||||
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
|
||||
warnings.push(createWarning8(path2, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
|
||||
}
|
||||
const objectsTable = parseDfdObjectsTable(sections.Objects, path2, {
|
||||
schema: options.schema,
|
||||
allowLegacyObjects: options.allowLegacyObjects,
|
||||
requireObjectId: options.requireObjectId
|
||||
});
|
||||
const domainsTable = options.schema === "dfd_diagram" ? parseDomainEntries(sections.Domains, path2) : { rows: [], warnings: [] };
|
||||
const domainSourcesTable = options.schema === "dfd_diagram" ? parseDomainSourcesTable(sections["Domain Sources"], path2) : { rows: [], warnings: [] };
|
||||
const domainsTable = parseDomainEntries(sections.Domains, path2);
|
||||
const domainSourcesTable = parseDomainSourcesTable(sections["Domain Sources"], path2);
|
||||
const flowHeaders = options.schema === "flow_diagram" ? FLOW_DIAGRAM_FLOW_HEADERS : DFD_FLOW_HEADERS;
|
||||
const flowsTable = parseMarkdownTable(sections.Flows, flowHeaders, path2, "Flows");
|
||||
const hasInvalidFlowsHeader = flowsTable.warnings.some(
|
||||
|
|
@ -11850,6 +11922,11 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
name: name || fallbackTitle,
|
||||
kind: "screen_communication",
|
||||
description: joinSectionLines2(sections.Summary),
|
||||
flowView,
|
||||
flowViewSpecified,
|
||||
flowViewRaw,
|
||||
domainSources: domainSourcesTable.rows,
|
||||
domains: domainsTable.rows,
|
||||
objectRefs,
|
||||
objectEntries,
|
||||
nodes,
|
||||
|
|
@ -11884,6 +11961,12 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
|
|||
warnings
|
||||
};
|
||||
}
|
||||
function parseFlowDiagramViewMode(value) {
|
||||
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
|
||||
}
|
||||
function isUnknownFlowDiagramViewMode(value) {
|
||||
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
|
||||
}
|
||||
function getFileStem4(path2) {
|
||||
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
@ -13512,7 +13595,8 @@ function createWarning16(path2, field, message) {
|
|||
|
||||
// src/parsers/mapping-parser.ts
|
||||
var SCOPE_HEADERS = ["role", "ref", "notes"];
|
||||
var MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"];
|
||||
var MAPPING_HEADERS = ["target_ref", "source_ref", "transform", "rule", "required", "notes"];
|
||||
var LEGACY_MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"];
|
||||
function parseMappingFile(markdown, path2) {
|
||||
const frontmatterResult = parseFrontmatter(markdown);
|
||||
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
||||
|
|
@ -13536,7 +13620,12 @@ function parseMappingFile(markdown, path2) {
|
|||
warnings.push(createWarning17(path2, "name", 'required frontmatter "name" is missing'));
|
||||
}
|
||||
const scopeTable = parseMarkdownTable(sections.Scope, SCOPE_HEADERS, path2, "Scope");
|
||||
const mappingsTable = parseMarkdownTable(sections.Mappings, MAPPING_HEADERS, path2, "Mappings");
|
||||
const mappingsTable = parseMarkdownTable(
|
||||
sections.Mappings,
|
||||
getAcceptedMappingHeaders(sections.Mappings),
|
||||
path2,
|
||||
"Mappings"
|
||||
);
|
||||
warnings.push(...scopeTable.warnings, ...mappingsTable.warnings);
|
||||
const fallbackName = name || id || getFileStem13(path2) || "Untitled Mapping";
|
||||
return {
|
||||
|
|
@ -13572,6 +13661,20 @@ function parseMappingFile(markdown, path2) {
|
|||
warnings
|
||||
};
|
||||
}
|
||||
function getAcceptedMappingHeaders(lines) {
|
||||
const actualHeader = getMarkdownTableHeader(lines);
|
||||
if (actualHeader && sameHeaders7(actualHeader, LEGACY_MAPPING_HEADERS)) {
|
||||
return [...LEGACY_MAPPING_HEADERS];
|
||||
}
|
||||
return [...MAPPING_HEADERS];
|
||||
}
|
||||
function getMarkdownTableHeader(lines) {
|
||||
const headerLine = lines?.map((line) => line.trim()).find((line) => line.startsWith("|"));
|
||||
return headerLine ? splitMarkdownTableRow(headerLine) : null;
|
||||
}
|
||||
function sameHeaders7(headers, expectedHeaders) {
|
||||
return headers.length === expectedHeaders.length && expectedHeaders.every((header, index) => headers[index] === header);
|
||||
}
|
||||
function getFileStem13(path2) {
|
||||
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
@ -14953,6 +15056,10 @@ function createShallowModel(path2, fileType, frontmatter) {
|
|||
id,
|
||||
name,
|
||||
kind: "screen_communication",
|
||||
flowView: "detail",
|
||||
flowViewSpecified: false,
|
||||
domainSources: [],
|
||||
domains: [],
|
||||
objectRefs: [],
|
||||
objectEntries: [],
|
||||
nodes: [],
|
||||
|
|
@ -18659,22 +18766,23 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
|
||||
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
||||
});
|
||||
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram, options?.flowDiagramViewMode);
|
||||
if (!options?.hideDetails) {
|
||||
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
|
||||
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
|
||||
if (domainDetails) {
|
||||
shell3.root.appendChild(domainDetails);
|
||||
}
|
||||
shell3.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram) ? buildFlowDiagramHoverMetadata(diagram, sourcePath) : null;
|
||||
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram) ? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath) : null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
renderedDiagram,
|
||||
sourcePath
|
||||
);
|
||||
const ready = renderMermaidSourceIntoShell(shell3, {
|
||||
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
||||
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
|
||||
renderIdPrefix: "model_weave_dfd",
|
||||
fitVerticalAlign: options?.fitVerticalAlign,
|
||||
viewportState: options?.viewportState,
|
||||
|
|
@ -18703,7 +18811,7 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext,
|
||||
openLinkText: isFlowDiagramModel(diagram.diagram) ? (target, event) => {
|
||||
const linktext = getFlowDiagramOpenLinkText(target);
|
||||
|
|
@ -18936,9 +19044,9 @@ function buildDfdMermaidInteractionTargets(diagram, sourcePath) {
|
|||
return target;
|
||||
}).filter((target) => Boolean(target));
|
||||
}
|
||||
function buildDfdMermaidSource(diagram, colorScheme) {
|
||||
function buildDfdMermaidSource(diagram, colorScheme, flowDiagramViewMode) {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return buildFlowDiagramMermaidSource(diagram, colorScheme);
|
||||
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram, flowDiagramViewMode), colorScheme);
|
||||
}
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines = ["flowchart LR"];
|
||||
|
|
@ -19009,37 +19117,161 @@ function buildDfdMermaidSource(diagram, colorScheme) {
|
|||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function getFlowDiagramRenderedDiagram(diagram, effectiveViewMode) {
|
||||
if (!isFlowDiagramModel(diagram.diagram) || effectiveViewMode !== "screen") {
|
||||
return diagram;
|
||||
}
|
||||
return buildFlowDiagramScreenFlowProjection(diagram);
|
||||
}
|
||||
function buildFlowDiagramScreenFlowProjection(diagram) {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return diagram;
|
||||
}
|
||||
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
|
||||
if (visibleNodes.length === 0) {
|
||||
return diagram;
|
||||
}
|
||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const outgoing = /* @__PURE__ */ new Map();
|
||||
for (const edge of diagram.edges) {
|
||||
const edges = outgoing.get(edge.source) ?? [];
|
||||
edges.push(edge);
|
||||
outgoing.set(edge.source, edges);
|
||||
}
|
||||
const projectedByPair = /* @__PURE__ */ new Map();
|
||||
for (const sourceNode of visibleNodes) {
|
||||
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
|
||||
edge,
|
||||
visitedEdges: /* @__PURE__ */ new Set([getProjectionEdgeVisitKey(edge)])
|
||||
}));
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
const targetId = current.edge.target;
|
||||
if (visibleIds.has(targetId)) {
|
||||
if (targetId !== sourceNode.id) {
|
||||
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const nextEdge of outgoing.get(targetId) ?? []) {
|
||||
const visitKey = getProjectionEdgeVisitKey(nextEdge);
|
||||
if (current.visitedEdges.has(visitKey)) {
|
||||
continue;
|
||||
}
|
||||
const nextVisitedEdges = new Set(current.visitedEdges);
|
||||
nextVisitedEdges.add(visitKey);
|
||||
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...diagram,
|
||||
diagram: {
|
||||
...diagram.diagram,
|
||||
flowView: "detail"
|
||||
},
|
||||
nodes: visibleNodes,
|
||||
edges: [...projectedByPair.values()]
|
||||
};
|
||||
}
|
||||
function isScreenFlowVisibleNode(node) {
|
||||
switch (node.kind) {
|
||||
case "screen":
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "context":
|
||||
case "message":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function addProjectedScreenFlowEdge(projectedByPair, source, target, incomingEdge) {
|
||||
const key = `${source}->${target}`;
|
||||
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
|
||||
const existing = projectedByPair.get(key);
|
||||
if (!existing) {
|
||||
projectedByPair.set(key, {
|
||||
id: `screen_flow:${source}->${target}`,
|
||||
source,
|
||||
target,
|
||||
kind: "flow",
|
||||
label,
|
||||
metadata: {
|
||||
...incomingEdge.metadata,
|
||||
projected: true,
|
||||
sourceFlowId: incomingEdge.id,
|
||||
dataRaw: void 0
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
existing.label = mergeScreenFlowLabels(existing.label, label);
|
||||
}
|
||||
function buildScreenFlowProjectionEdgeLabel(edge) {
|
||||
return getStringMetadata(edge.metadata, "condition")?.trim() || getStringMetadata(edge.metadata, "trigger")?.trim() || getStringMetadata(edge.metadata, "flowKind")?.trim() || void 0;
|
||||
}
|
||||
function mergeScreenFlowLabels(left, right) {
|
||||
const values = [
|
||||
...(left ?? "").split(" / "),
|
||||
...(right ?? "").split(" / ")
|
||||
].map((value) => value.trim()).filter(Boolean);
|
||||
const unique = [...new Set(values)];
|
||||
if (unique.length === 0) {
|
||||
return void 0;
|
||||
}
|
||||
const merged = unique.join(" / ");
|
||||
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
|
||||
}
|
||||
function getProjectionEdgeVisitKey(edge) {
|
||||
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
|
||||
}
|
||||
function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines = [
|
||||
"flowchart LR",
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
];
|
||||
const lines = ["flowchart LR"];
|
||||
const colorClasses = /* @__PURE__ */ new Map();
|
||||
if (!colorScheme) {
|
||||
lines.push(
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
);
|
||||
}
|
||||
const nodeIds = /* @__PURE__ */ new Map();
|
||||
const domainStyles = [];
|
||||
const flowDomains = getFlowDiagramDomains(diagram);
|
||||
const usesResolvedDomains = flowDomains.length > 0;
|
||||
const flowDomainsById = new Map(flowDomains.map((domain) => [domain.id, domain]));
|
||||
const groupedNodes = /* @__PURE__ */ new Map();
|
||||
const ungroupedNodes = [];
|
||||
for (const node of diagram.nodes) {
|
||||
const domainId = getNodeDomainId(node);
|
||||
if (domainId) {
|
||||
if (!flowDomainsById.has(domainId)) {
|
||||
flowDomainsById.set(domainId, createSyntheticDomainEntry(domainId, flowDomainsById.size));
|
||||
}
|
||||
if (domainId && flowDomainsById.has(domainId)) {
|
||||
if (!groupedNodes.has(domainId)) {
|
||||
groupedNodes.set(domainId, []);
|
||||
}
|
||||
groupedNodes.get(domainId).push(node);
|
||||
} else if (domainId && !usesResolvedDomains) {
|
||||
const synthetic = createSyntheticDomainEntry(domainId, flowDomainsById.size);
|
||||
flowDomainsById.set(domainId, synthetic);
|
||||
groupedNodes.set(domainId, [node]);
|
||||
} else {
|
||||
ungroupedNodes.push(node);
|
||||
}
|
||||
|
|
@ -19052,11 +19284,12 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
|||
nodeIds,
|
||||
1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
for (const node of ungroupedNodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
|
||||
}
|
||||
for (const edge of diagram.edges) {
|
||||
const from = nodeIds.get(edge.source);
|
||||
|
|
@ -19071,12 +19304,18 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
|
|||
lines.push(` ${from} --> ${to}`);
|
||||
}
|
||||
}
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, domainStyles) {
|
||||
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, colorClasses, domainStyles) {
|
||||
const childLines = [];
|
||||
for (const child of domainNode.children) {
|
||||
appendFlowDiagramDomainSubgraph(
|
||||
|
|
@ -19086,6 +19325,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
nodeIds,
|
||||
depth + 1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
|
@ -19098,7 +19338,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
|
||||
lines.push(...childLines);
|
||||
for (const node of nodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
|
||||
}
|
||||
lines.push(`${indent}end`);
|
||||
if (colorScheme) {
|
||||
|
|
@ -19110,25 +19350,17 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
|
|||
}
|
||||
return true;
|
||||
}
|
||||
function appendFlowDiagramNode(lines, node, nodeIds, depth) {
|
||||
function appendFlowDiagramNode(lines, node, nodeIds, depth, colorScheme, colorClasses) {
|
||||
const mermaidId = toMermaidNodeId(node.id);
|
||||
const shape = toFlowDiagramMermaidShape(node.kind);
|
||||
const className = toFlowDiagramClassName(node.kind);
|
||||
const className = colorScheme ? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses) : toFlowDiagramClassName(node.kind);
|
||||
const indent = " ".repeat(depth);
|
||||
nodeIds.set(node.id, mermaidId);
|
||||
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel2(node.label ?? node.ref ?? node.id)}" }`);
|
||||
lines.push(`${indent}class ${mermaidId} ${className}`);
|
||||
}
|
||||
function getFlowDiagramDomains(diagram) {
|
||||
const resolvedDomains = getOptionalResolvedDomains(diagram);
|
||||
const domainsById = new Map(resolvedDomains.map((domain) => [domain.id, domain]));
|
||||
for (const node of diagram.nodes) {
|
||||
const domainId = getNodeDomainId(node);
|
||||
if (domainId && !domainsById.has(domainId)) {
|
||||
domainsById.set(domainId, createSyntheticDomainEntry(domainId, domainsById.size));
|
||||
}
|
||||
}
|
||||
return Array.from(domainsById.values());
|
||||
return getOptionalResolvedDomains(diagram);
|
||||
}
|
||||
function getOptionalResolvedDomains(diagram) {
|
||||
const maybeDomains = diagram.diagram.domains;
|
||||
|
|
@ -19151,7 +19383,7 @@ function getFlowDomainColorKind(domain) {
|
|||
}
|
||||
function getDfdMermaidColorSchemeTargets(diagram) {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) ? ["domain"] : [];
|
||||
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
|
||||
}
|
||||
if (isDfdDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0 ? ["dfd", "domain"] : ["dfd"];
|
||||
|
|
@ -19173,12 +19405,31 @@ function toFlowDiagramMermaidShape(kind) {
|
|||
case "app_process":
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "message":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
case "external":
|
||||
case "unknown":
|
||||
default:
|
||||
return "rect";
|
||||
}
|
||||
}
|
||||
function registerFlowDiagramColorClass(kind, colorScheme, colorClasses) {
|
||||
const className = toFlowDiagramColorClassName(kind);
|
||||
colorClasses?.set(
|
||||
className,
|
||||
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : void 0)
|
||||
);
|
||||
return className;
|
||||
}
|
||||
function toFlowDiagramColorClassName(kind) {
|
||||
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
|
||||
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
|
||||
}
|
||||
function toFlowDiagramClassName(kind) {
|
||||
switch (kind) {
|
||||
case "screen":
|
||||
|
|
@ -19189,11 +19440,18 @@ function toFlowDiagramClassName(kind) {
|
|||
return "store";
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "message":
|
||||
return "context";
|
||||
case "process":
|
||||
case "app_process":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
return "process";
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
return "external";
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
@ -19558,6 +19816,51 @@ function createReservedKindFallback(kind) {
|
|||
return root;
|
||||
}
|
||||
|
||||
// src/core/flow-diagram-view-mode.ts
|
||||
function normalizeFlowDiagramViewMode(value) {
|
||||
if (typeof value !== "string") {
|
||||
return void 0;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "detail" || normalized === "screen" ? normalized : void 0;
|
||||
}
|
||||
function resolveInitialFlowDiagramViewMode(diagram, defaultViewMode) {
|
||||
return diagram.flowViewSpecified ? diagram.flowView : normalizeFlowDiagramViewMode(defaultViewMode) ?? "detail";
|
||||
}
|
||||
function buildFlowDiagramViewModeInitializationKey(diagram, defaultViewMode) {
|
||||
if (diagram.flowViewSpecified) {
|
||||
return `frontmatter:${diagram.flowView}`;
|
||||
}
|
||||
const defaultMode = normalizeFlowDiagramViewMode(defaultViewMode) ?? "detail";
|
||||
const rawValue = typeof diagram.flowViewRaw === "string" ? diagram.flowViewRaw.trim() : "";
|
||||
return rawValue ? `invalid:${rawValue}:settings:${defaultMode}` : `settings:${defaultMode}`;
|
||||
}
|
||||
var FlowDiagramViewModeState = class {
|
||||
constructor() {
|
||||
this.entriesByFilePath = /* @__PURE__ */ new Map();
|
||||
}
|
||||
synchronize(filePath, diagram, defaultViewMode) {
|
||||
const initializationKey = buildFlowDiagramViewModeInitializationKey(diagram, defaultViewMode);
|
||||
const existing = this.entriesByFilePath.get(filePath);
|
||||
if (existing?.initializationKey === initializationKey) {
|
||||
return { mode: existing.mode, initializationChanged: false };
|
||||
}
|
||||
const mode = resolveInitialFlowDiagramViewMode(diagram, defaultViewMode);
|
||||
this.entriesByFilePath.set(filePath, { mode, initializationKey });
|
||||
return { mode, initializationChanged: true };
|
||||
}
|
||||
getOrInitialize(filePath, diagram, defaultViewMode) {
|
||||
return this.synchronize(filePath, diagram, defaultViewMode).mode;
|
||||
}
|
||||
set(filePath, mode) {
|
||||
const existing = this.entriesByFilePath.get(filePath);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
existing.mode = mode;
|
||||
}
|
||||
};
|
||||
|
||||
// src/core/app-process-step-interaction-target.ts
|
||||
function resolveAppProcessStepInteractionTarget(model, step, context) {
|
||||
const index = context.index ?? null;
|
||||
|
|
@ -20605,6 +20908,9 @@ var EN_MESSAGES = {
|
|||
"appProcess.businessFlow.direction": "Business flow direction",
|
||||
"appProcess.businessFlow.direction.lr": "Left to right",
|
||||
"appProcess.businessFlow.direction.td": "Top down",
|
||||
"flowDiagram.viewMode": "Flow view",
|
||||
"flowDiagram.viewMode.detail": "Detail",
|
||||
"flowDiagram.viewMode.screen": "Screen",
|
||||
/* eslint-disable obsidianmd/ui/sentence-case-locale-module -- Connect Flow is the requested toolbar label. */
|
||||
"appProcess.businessFlow.connectFlow.short": "Connect Flow",
|
||||
"appProcess.businessFlow.connectFlow.title": "Connect Flow",
|
||||
|
|
@ -20932,6 +21238,10 @@ var EN_MESSAGES = {
|
|||
"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",
|
||||
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module -- Flow Diagram is the requested format name.
|
||||
"settings.defaultFlowDiagramViewMode.name": "Default Flow Diagram view",
|
||||
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module -- Flow Diagram is the requested format name.
|
||||
"settings.defaultFlowDiagramViewMode.desc": "Used for a Flow Diagram initial view when frontmatter.flow_view is not set or is invalid.",
|
||||
"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
|
||||
|
|
@ -21050,6 +21360,9 @@ var JA_MESSAGES = {
|
|||
"appProcess.businessFlow.direction": "Business Flow \u65B9\u5411",
|
||||
"appProcess.businessFlow.direction.lr": "\u5DE6\u304B\u3089\u53F3",
|
||||
"appProcess.businessFlow.direction.td": "\u4E0A\u304B\u3089\u4E0B",
|
||||
"flowDiagram.viewMode": "Flow view",
|
||||
"flowDiagram.viewMode.detail": "Detail",
|
||||
"flowDiagram.viewMode.screen": "Screen",
|
||||
"appProcess.businessFlow.connectFlow.short": "Flow\u63A5\u7D9A",
|
||||
"appProcess.businessFlow.connectFlow.title": "Flow\u63A5\u7D9A",
|
||||
"appProcess.businessFlow.connectFlow.selectedTitle": "Flow\u63A5\u7D9A: {stepId} \u3092\u9078\u629E\u4E2D",
|
||||
|
|
@ -21333,6 +21646,8 @@ var JA_MESSAGES = {
|
|||
"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.defaultFlowDiagramViewMode.name": "Flow Diagram \u306E\u521D\u671F\u8868\u793A\u30E2\u30FC\u30C9",
|
||||
"settings.defaultFlowDiagramViewMode.desc": "Flow Diagram \u3067 frontmatter.flow_view \u304C\u672A\u8A2D\u5B9A\u307E\u305F\u306F\u4E0D\u6B63\u306A\u5834\u5408\u306E\u521D\u671F\u8868\u793A\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
||||
"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",
|
||||
|
|
@ -22084,7 +22399,7 @@ function updateColorSchemeColorCell(markdown, request) {
|
|||
return unchanged2(markdown, "table-missing");
|
||||
}
|
||||
const headers = splitMarkdownTableRow(lines[tableStart]) ?? [];
|
||||
if (!sameHeaders7(headers, [...COLOR_HEADERS2])) {
|
||||
if (!sameHeaders8(headers, [...COLOR_HEADERS2])) {
|
||||
return unchanged2(markdown, "header-mismatch");
|
||||
}
|
||||
const separatorCells = splitMarkdownTableRow(lines[tableStart + 1]);
|
||||
|
|
@ -22171,7 +22486,7 @@ function findDataRowLineIndex(lines, start, end, targetRowIndex) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function sameHeaders7(actual, expected) {
|
||||
function sameHeaders8(actual, expected) {
|
||||
return actual.length === expected.length && actual.every((header, index) => header === expected[index]);
|
||||
}
|
||||
function normalizeHeadingName(value) {
|
||||
|
|
@ -22507,6 +22822,7 @@ var DEFAULT_VIEWER_PREFERENCES = {
|
|||
defaultDomainsViewMode: "mindmap",
|
||||
defaultDomainDiagramViewMode: "mindmap",
|
||||
defaultBusinessFlowDirection: "LR",
|
||||
defaultFlowDiagramViewMode: "detail",
|
||||
localSourceRoot: "",
|
||||
uiLanguage: "auto",
|
||||
showMermaidRenderDebug: false
|
||||
|
|
@ -22567,6 +22883,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
this.appProcessBusinessFlowDirectionOverride = null;
|
||||
this.appProcessBusinessFlowDirectionFilePath = null;
|
||||
this.appProcessFlowConnectFilePath = null;
|
||||
this.flowDiagramViewModes = new FlowDiagramViewModeState();
|
||||
this.appProcessFlowConnectModeEnabled = false;
|
||||
this.appProcessFlowConnectSourceStepId = null;
|
||||
this.domainsDiagramModeState = null;
|
||||
|
|
@ -22740,6 +23057,11 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
this.prepareDomainsDiagramMode(state, nextFilePath);
|
||||
this.prepareAppProcessBusinessFlowDirection(state, nextFilePath);
|
||||
this.prepareAppProcessFlowConnectMode(nextFilePath);
|
||||
const flowViewReinitialized = this.prepareFlowDiagramViewMode(state, nextFilePath);
|
||||
if (flowViewReinitialized && nextFilePath) {
|
||||
this.viewportStateCache.delete(nextFilePath);
|
||||
resetGraphViewportState(this.diagramViewportState);
|
||||
}
|
||||
this.prepareViewportState(state, reason);
|
||||
this.state = state;
|
||||
this.renderCurrentState();
|
||||
|
|
@ -22970,6 +23292,16 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
this.domainsDiagramModeFilePath = nextFilePath;
|
||||
this.domainsDiagramModeState = state.mode;
|
||||
}
|
||||
prepareFlowDiagramViewMode(state, nextFilePath) {
|
||||
if (state.mode !== "diagram" || state.diagram.diagram.schema !== "flow_diagram" || !nextFilePath) {
|
||||
return false;
|
||||
}
|
||||
return this.flowDiagramViewModes.synchronize(
|
||||
nextFilePath,
|
||||
state.diagram.diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
).initializationChanged;
|
||||
}
|
||||
rememberViewportState(filePath, state) {
|
||||
if (!state.hasAutoFitted && !state.hasUserInteracted) {
|
||||
return;
|
||||
|
|
@ -23023,6 +23355,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
forExport: true,
|
||||
renderMode: getStandardRenderMode(state.rendererSelection),
|
||||
colorScheme: state.colorScheme,
|
||||
flowDiagramViewMode: state.diagram.diagram.schema === "flow_diagram" ? this.getFlowDiagramViewMode(state) : void 0,
|
||||
...getMermaidSourceLabels(this.t)
|
||||
})
|
||||
};
|
||||
|
|
@ -24132,6 +24465,35 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
|
||||
rightGroup.appendChild(wrapper);
|
||||
}
|
||||
appendFlowDiagramViewSelector(container, filePath) {
|
||||
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
|
||||
if (!toolbar) {
|
||||
return;
|
||||
}
|
||||
toolbar.addClass("model-weave-render-mode-toolbar-host");
|
||||
toolbar.querySelector(".model-weave-flow-diagram-view-select-group")?.remove();
|
||||
const wrapper = container.ownerDocument.createElement("div");
|
||||
wrapper.className = "model-weave-flow-diagram-view-select-group model-weave-render-mode-row";
|
||||
const label = container.ownerDocument.createElement("span");
|
||||
label.addClass("model-weave-render-mode-label");
|
||||
label.textContent = this.t("flowDiagram.viewMode");
|
||||
wrapper.appendChild(label);
|
||||
const select = container.ownerDocument.createElement("select");
|
||||
select.addClass("model-weave-flow-diagram-view-select");
|
||||
for (const mode of ["detail", "screen"]) {
|
||||
const option = container.ownerDocument.createElement("option");
|
||||
option.value = mode;
|
||||
option.textContent = this.t(`flowDiagram.viewMode.${mode}`);
|
||||
option.selected = this.getFlowDiagramViewModeForFile(filePath) === mode;
|
||||
select.appendChild(option);
|
||||
}
|
||||
select.addEventListener("change", () => {
|
||||
this.setFlowDiagramViewMode(select.value, filePath);
|
||||
});
|
||||
wrapper.appendChild(select);
|
||||
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
|
||||
rightGroup.appendChild(wrapper);
|
||||
}
|
||||
appendAppProcessFlowConnectControl(container, filePath) {
|
||||
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
|
||||
if (!toolbar) {
|
||||
|
|
@ -24257,6 +24619,41 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
getAppProcessBusinessFlowDirectionOverride(filePath) {
|
||||
return this.appProcessBusinessFlowDirectionFilePath === filePath ? this.appProcessBusinessFlowDirectionOverride : null;
|
||||
}
|
||||
getFlowDiagramViewMode(state) {
|
||||
const diagram = state.diagram.diagram;
|
||||
if (diagram.schema !== "flow_diagram") {
|
||||
return "detail";
|
||||
}
|
||||
return this.flowDiagramViewModes.getOrInitialize(
|
||||
diagram.path,
|
||||
diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
);
|
||||
}
|
||||
getFlowDiagramViewModeForFile(filePath) {
|
||||
const diagramState = this.state.mode === "diagram" && this.state.diagram.diagram.path === filePath ? this.state : null;
|
||||
if (!diagramState || diagramState.diagram.diagram.schema !== "flow_diagram") {
|
||||
return "detail";
|
||||
}
|
||||
return this.flowDiagramViewModes.getOrInitialize(
|
||||
filePath,
|
||||
diagramState.diagram.diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
);
|
||||
}
|
||||
setFlowDiagramViewMode(mode, filePath) {
|
||||
if (mode !== "detail" && mode !== "screen") {
|
||||
return;
|
||||
}
|
||||
if (this.getFlowDiagramViewModeForFile(filePath) === mode) {
|
||||
return;
|
||||
}
|
||||
this.flowDiagramViewModes.set(filePath, mode);
|
||||
this.viewportStateCache.delete(filePath);
|
||||
resetGraphViewportState(this.diagramViewportState);
|
||||
this.renderCurrentState();
|
||||
this.restoreCurrentScrollPosition();
|
||||
}
|
||||
setAppProcessBusinessFlowDirection(direction, filePath) {
|
||||
const nextDirection = normalizeAppProcessBusinessFlowDirection(direction);
|
||||
if (!nextDirection) {
|
||||
|
|
@ -25515,6 +25912,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
onOpenObject: state.onOpenObject ?? void 0,
|
||||
app: this.app,
|
||||
interactionSourcePath: filePath,
|
||||
flowDiagramViewMode: state.diagram.diagram.schema === "flow_diagram" ? this.getFlowDiagramViewMode(state) : void 0,
|
||||
renderMode: getStandardRenderMode(state.rendererSelection),
|
||||
colorScheme: state.colorScheme,
|
||||
viewportState: this.diagramViewportState,
|
||||
|
|
@ -25531,6 +25929,9 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|||
ensureGraphIdentityTitle(diagramRoot, buildGraphIdentityTitle(state.diagram.diagram));
|
||||
this.appendRendererSelection(diagramRoot, state.rendererSelection);
|
||||
this.appendViewerToolbarControls(diagramRoot);
|
||||
if (isFlowDiagramViewSelectorVisible(state.diagram)) {
|
||||
this.appendFlowDiagramViewSelector(diagramRoot, filePath);
|
||||
}
|
||||
this.moveDetailSections(diagramRoot, lowerSlots.details);
|
||||
this.renderImpactSummarySection(
|
||||
lowerSlots.impact,
|
||||
|
|
@ -27567,6 +27968,9 @@ function getFrontmatterString2(value, key) {
|
|||
function getModelDisplayName(value) {
|
||||
return getStringField(value, "name") ?? getStringField(value, "title") ?? getStringField(value, "logicalName") ?? getStringField(value, "physicalName") ?? getFrontmatterString2(value, "name") ?? getFrontmatterString2(value, "title") ?? getModelId3(value);
|
||||
}
|
||||
function isFlowDiagramViewSelectorVisible(diagram) {
|
||||
return diagram.diagram.schema === "flow_diagram";
|
||||
}
|
||||
function getModelId3(value) {
|
||||
return getStringField(value, "id") ?? getFrontmatterString2(value, "id");
|
||||
}
|
||||
|
|
@ -27699,6 +28103,10 @@ var DOMAIN_VIEW_MODE_OPTIONS = [
|
|||
"area",
|
||||
"tree"
|
||||
];
|
||||
var FLOW_DIAGRAM_VIEW_MODE_OPTIONS = [
|
||||
"detail",
|
||||
"screen"
|
||||
];
|
||||
function isClassRenderModeOption(value) {
|
||||
return CLASS_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
|
|
@ -27714,6 +28122,9 @@ function isProcessRenderModeOption(value) {
|
|||
function isBusinessFlowDirectionOption(value) {
|
||||
return BUSINESS_FLOW_DIRECTION_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
function isFlowDiagramViewModeOption(value) {
|
||||
return FLOW_DIAGRAM_VIEW_MODE_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
function isScreenRenderModeOption(value) {
|
||||
return SCREEN_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
|
|
@ -28161,6 +28572,7 @@ var ModelWeavePlugin = class extends import_obsidian8.Plugin {
|
|||
defaultDomainDiagramViewMode: this.settings.defaultDomainDiagramViewMode,
|
||||
defaultBusinessFlowDirection: this.settings.defaultBusinessFlowDirection,
|
||||
localSourceRoot: this.settings.localSourceRoot,
|
||||
defaultFlowDiagramViewMode: this.settings.defaultFlowDiagramViewMode,
|
||||
uiLanguage: this.settings.uiLanguage,
|
||||
showMermaidRenderDebug: this.settings.showMermaidRenderDebug
|
||||
};
|
||||
|
|
@ -30710,6 +31122,16 @@ var ModelWeaveSettingTab = class extends import_obsidian8.PluginSettingTab {
|
|||
});
|
||||
});
|
||||
});
|
||||
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultFlowDiagramViewMode.name")).setDesc(t("settings.defaultFlowDiagramViewMode.desc")).addDropdown((dropdown) => {
|
||||
dropdown.addOption("detail", t("flowDiagram.viewMode.detail")).addOption("screen", t("flowDiagram.viewMode.screen")).setValue(settings.defaultFlowDiagramViewMode).onChange(async (value) => {
|
||||
if (!isFlowDiagramViewModeOption(value)) {
|
||||
return;
|
||||
}
|
||||
await this.plugin.updateSettings({
|
||||
defaultFlowDiagramViewMode: 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)) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "model-weave",
|
||||
"name": "Model Weave",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"minAppVersion": "1.8.7",
|
||||
"description": "Manage text-first models, relationships, and diagrams in Markdown.",
|
||||
"author": "nejimakibird",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-model-weave",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-model-weave",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-model-weave",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"description": "An Obsidian plugin for managing text-first models, relationships, and diagrams in Markdown.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ The samples are organized by format and purpose rather than by historical versio
|
|||
- [CodeSet](codeset/)
|
||||
- [Message](message/)
|
||||
- [Mapping](mapping/)
|
||||
- [Domains](domains/)
|
||||
- [Flow Diagram](flow_diagram/)
|
||||
- [Color Scheme](color_scheme/)
|
||||
- [Relations](relations/) - Schema-driven samples that use `schema: model_relations_v1`, not a normal `type:` value.
|
||||
|
||||
## Recommended manual checks
|
||||
|
|
@ -41,10 +44,14 @@ The samples are organized by format and purpose rather than by historical versio
|
|||
- [dfd/basic/DFD-WMS-L0.md](dfd/basic/DFD-WMS-L0.md)
|
||||
- [dfd/local-objects/DFD-WMS-L0-LOCAL.md](dfd/local-objects/DFD-WMS-L0-LOCAL.md)
|
||||
|
||||
### Screen / Process / Data
|
||||
### Screen / Process / Data / Flow
|
||||
|
||||
- [screen/SCR-INVENTORY-SEARCH.md](screen/SCR-INVENTORY-SEARCH.md)
|
||||
- [app_process/PROC-INVENTORY-SEARCH.md](app_process/PROC-INVENTORY-SEARCH.md)
|
||||
- [app_process/PROC-SAMPLE-ORDER-ENTRY-FLOW.md](app_process/PROC-SAMPLE-ORDER-ENTRY-FLOW.md)
|
||||
- [data/DATA-INVENTORY-SEARCH-CONDITION.md](data/DATA-INVENTORY-SEARCH-CONDITION.md)
|
||||
- [relations/REL-WMS-CORE.md](relations/REL-WMS-CORE.md)
|
||||
- [domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md](domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md)
|
||||
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md)
|
||||
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md)
|
||||
- [color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md](color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md)
|
||||
|
|
|
|||
28
samples/color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md
Normal file
28
samples/color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
type: color_scheme
|
||||
id: COLOR-SCHEME-SAMPLE-FLOW-DOMAIN
|
||||
name: Sample Flow Diagram Domain Colors
|
||||
tags:
|
||||
- Sample
|
||||
- ColorScheme
|
||||
- FlowDiagram
|
||||
---
|
||||
|
||||
# Sample Flow Diagram Domain Colors
|
||||
|
||||
## Summary
|
||||
|
||||
Flow Diagram の Domain group 配色を確認するためのサンプルColor Schemeです。
|
||||
Settings の defaultColorSchemeRef にこのファイルを指定して確認します。
|
||||
|
||||
## Colors
|
||||
|
||||
| target | kind | fill | stroke | text | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| domain | system | #f7f7f7 | #999999 | #222222 | Root system boundary |
|
||||
| domain | actor | #fff8dc | #d6a100 | #3a2a00 | Actor domain group |
|
||||
| domain | application | #e9fbe9 | #2f8f46 | #123d1b | Application domain group |
|
||||
| domain | ui | #e8f1ff | #3b73d9 | #102a5c | Frontend/UI domain group |
|
||||
| domain | data | #f3e8ff | #7c3bbd | #2d124c | Data domain group |
|
||||
| domain | integration | #fff0e6 | #d97706 | #4a2200 | Integration domain group |
|
||||
| domain | external | #ffecec | #cc4b4b | #4a1010 | External domain group |
|
||||
28
samples/domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md
Normal file
28
samples/domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
type: domains
|
||||
id: DOMAINS-SAMPLE-CHECKOUT-FLOW
|
||||
name: Sample Checkout Flow Domains
|
||||
tags:
|
||||
- Sample
|
||||
- Domains
|
||||
- FlowDiagram
|
||||
---
|
||||
|
||||
# Sample Checkout Flow Domains
|
||||
|
||||
## Summary
|
||||
|
||||
Flow Diagram の Domain Sources、Domain group、parent-child Domain group、Color Scheme 適用を確認するためのサンプルDomain定義です。
|
||||
|
||||
## Domains
|
||||
|
||||
| id | name | kind | parent | description |
|
||||
|---|---|---|---|---|
|
||||
| checkout_system | Checkout System | system | | Overall checkout flow boundary |
|
||||
| actor_area | Actor Area | actor | checkout_system | Customer and external actor side |
|
||||
| internal_area | Internal Area | application | checkout_system | Internal application and data boundary |
|
||||
| frontend_area | Frontend Area | ui | internal_area | Browser and screen side |
|
||||
| application_area | Application Area | application | internal_area | Application process and API handling |
|
||||
| data_area | Data Area | data | internal_area | Session, database, and persisted data |
|
||||
| integration_area | Integration Area | integration | checkout_system | External integration boundary |
|
||||
| external_area | External Area | external | integration_area | External service boundary |
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
type: flow_diagram
|
||||
id: FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION
|
||||
name: Sample Checkout Screen Communication Flow
|
||||
kind: screen_communication
|
||||
tags:
|
||||
- Sample
|
||||
- FlowDiagram
|
||||
- DomainSources
|
||||
---
|
||||
|
||||
# Sample Checkout Screen Communication Flow
|
||||
|
||||
## Summary
|
||||
|
||||
Checkout screen submitから、画面、アプリケーション処理、セッション、データストア、外部決済サービスまでの通信・処理フローを示すFlow Diagramサンプルです。
|
||||
|
||||
このサンプルでは、Domain Sources、Objects.domain、parent-child Domain group、Flow Diagram node kind、Flows.kind / trigger / data / condition を確認します。
|
||||
|
||||
## Domain Sources
|
||||
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
| [[DOMAINS-SAMPLE-CHECKOUT-FLOW]] | Sample Domains for Flow Diagram group rendering |
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| user | Customer | external | | actor_area | User who submits the checkout form |
|
||||
| checkout_screen | Checkout Screen | screen | | frontend_area | Checkout screen |
|
||||
| submit_action | Submit Order Action | work_object | | frontend_area | UI action on the checkout screen |
|
||||
| checkout_process | Checkout Process | app_process | | application_area | Server-side checkout handling |
|
||||
| session_store | Session Store | session | | data_area | Temporary checkout session |
|
||||
| order_store | Order Store | datastore | | data_area | Persisted order data |
|
||||
| payment_gateway | Payment Gateway | external | | external_area | External payment service |
|
||||
| completion_screen | Completion Screen | screen | | frontend_area | Final screen after successful checkout |
|
||||
| error_message | Error Message | context | | frontend_area | Validation or payment error display |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | user | checkout_screen | event | open | Checkout page request | | User opens checkout screen |
|
||||
| F02 | checkout_screen | submit_action | event | click submit | Checkout form values | | UI submit action |
|
||||
| F03 | submit_action | checkout_process | command | submit | DATA-CHECKOUT-SUBMIT-REQUEST | | Submit request from screen to process |
|
||||
| F04 | checkout_process | session_store | read | load session | Checkout session | | Plain text data label |
|
||||
| F05 | checkout_process | payment_gateway | call | authorize payment | DATA-PAYMENT-AUTHORIZATION-REQUEST | payment required | External payment authorization |
|
||||
| F06 | payment_gateway | checkout_process | response | authorization result | DATA-PAYMENT-AUTHORIZATION-RESULT | | Payment result response |
|
||||
| F07 | checkout_process | order_store | command | save order | DATA-ORDER-CREATE-COMMAND | authorized | Persist order |
|
||||
| F08 | checkout_process | completion_screen | navigation | show complete | DATA-CHECKOUT-RESULT | success | Navigate to completion screen |
|
||||
| F09 | checkout_process | error_message | event | show error | Checkout error | failed | Error branch |
|
||||
53
samples/flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md
Normal file
53
samples/flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
type: flow_diagram
|
||||
id: FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW
|
||||
name: Sample Checkout Screen Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
tags:
|
||||
- Sample
|
||||
- FlowDiagram
|
||||
- DomainSources
|
||||
---
|
||||
|
||||
# Sample Checkout Screen Flow
|
||||
|
||||
## Summary
|
||||
|
||||
Checkout screen submitから、内部処理を折りたたんだScreen Flow projectionを確認するFlow Diagramサンプルです。
|
||||
|
||||
このサンプルでは、同じ詳細Objects / Flowsから、user / screen / external / message中心の簡略表示、Domain Sources、parent-child Domain group、投影edge labelを確認します。
|
||||
|
||||
## Domain Sources
|
||||
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
| [[DOMAINS-SAMPLE-CHECKOUT-FLOW]] | Sample Domains for Flow Diagram group rendering |
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| user | Customer | external | | actor_area | User who submits the checkout form |
|
||||
| checkout_screen | Checkout Screen | screen | | frontend_area | Checkout screen |
|
||||
| submit_action | Submit Order Action | work_object | | frontend_area | UI action on the checkout screen |
|
||||
| checkout_process | Checkout Process | app_process | | application_area | Server-side checkout handling |
|
||||
| session_store | Session Store | session | | data_area | Temporary checkout session |
|
||||
| order_store | Order Store | datastore | | data_area | Persisted order data |
|
||||
| payment_gateway | Payment Gateway | external | | external_area | External payment service |
|
||||
| completion_screen | Completion Screen | screen | | frontend_area | Final screen after successful checkout |
|
||||
| error_message | Error Message | context | | frontend_area | Validation or payment error display |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | user | checkout_screen | event | open | Checkout page request | | User opens checkout screen |
|
||||
| F02 | checkout_screen | submit_action | event | click submit | Checkout form values | | UI submit action |
|
||||
| F03 | submit_action | checkout_process | command | submit | DATA-CHECKOUT-SUBMIT-REQUEST | | Submit request from screen to process |
|
||||
| F04 | checkout_process | session_store | read | load session | Checkout session | | Plain text data label |
|
||||
| F05 | checkout_process | payment_gateway | call | authorize payment | DATA-PAYMENT-AUTHORIZATION-REQUEST | payment required | External payment authorization |
|
||||
| F06 | payment_gateway | checkout_process | response | authorization result | DATA-PAYMENT-AUTHORIZATION-RESULT | | Payment result response |
|
||||
| F07 | checkout_process | order_store | command | save order | DATA-ORDER-CREATE-COMMAND | authorized | Persist order |
|
||||
| F08 | checkout_process | completion_screen | navigation | show complete | DATA-CHECKOUT-RESULT | success | Navigate to completion screen |
|
||||
| F09 | checkout_process | error_message | event | show error | Checkout error | failed | Error branch |
|
||||
|
|
@ -26,13 +26,13 @@ Maps inventory search screen fields to inventory search condition data object.
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[../screen/SCR-INVENTORY-SEARCH]].shipper_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].shipper_id | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | Y | |
|
||||
| [[../screen/SCR-INVENTORY-SEARCH]].warehouse_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].warehouse_id | Direct copy | | N | |
|
||||
| [[../screen/SCR-INVENTORY-SEARCH]].item_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].item_id | Direct copy | | N | |
|
||||
| [[../screen/SCR-INVENTORY-SEARCH]].lot_no | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].lot_no | Direct copy | | N | |
|
||||
| [[../screen/SCR-INVENTORY-SEARCH]].inventory_status | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].inventory_status | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | N | CodeSet value |
|
||||
| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].shipper_id | [[../screen/SCR-INVENTORY-SEARCH]].shipper_id | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | Y | |
|
||||
| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].warehouse_id | [[../screen/SCR-INVENTORY-SEARCH]].warehouse_id | Direct copy | | N | |
|
||||
| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].item_id | [[../screen/SCR-INVENTORY-SEARCH]].item_id | Direct copy | | N | |
|
||||
| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].lot_no | [[../screen/SCR-INVENTORY-SEARCH]].lot_no | Direct copy | | N | |
|
||||
| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].inventory_status | [[../screen/SCR-INVENTORY-SEARCH]].inventory_status | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | N | CodeSet value |
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -1522,6 +1522,7 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
|
|||
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `予約済み kind "${kind}" が使われています。`],
|
||||
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} では ${renderer} renderer はサポートされていません。format の既定 renderer を使います。`],
|
||||
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" は不明です。format の既定 renderer を使います。`],
|
||||
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view は "detail" または "screen" を指定してください。'],
|
||||
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" は通常と異なる可能性があります。`],
|
||||
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" は自己ループです。`],
|
||||
[/^Domain id is required\.$/, "Domain の id が必要です。"],
|
||||
|
|
@ -1548,6 +1549,9 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
|
|||
[/^DFD-local Domain "([^"]+)" overrides Domain Source (name|kind|parent) "([^"]*)" with "([^"]*)"\.$/, (_match, domain, field, source, local) => `DFD内の Domain "${domain}" は Domain Source の ${field} "${source}" を "${local}" で上書きしています。`],
|
||||
[/^DFD object "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" が未定義のローカル Domain "${domain}" を参照しています。`],
|
||||
[/^DFD object "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" が未定義の Domain "${domain}" を参照しています。`],
|
||||
[/^Flow Diagram local Domain "([^"]+)" overrides Domain Source (name|kind|parent) "([^"]*)" with "([^"]*)"\.$/, (_match, domain, field, source, local) => `Flow Diagram ローカル Domain "${domain}" は Domain Source の ${field} "${source}" を "${local}" で上書きしています。`],
|
||||
[/^Flow Diagram object "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, object, domain) => `Flow Diagram object "${object}" が未定義のローカル Domain "${domain}" を参照しています。`],
|
||||
[/^Flow Diagram object "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, object, domain) => `Flow Diagram object "${object}" が未定義の Domain "${domain}" を参照しています。`],
|
||||
[/^DFD object "([^"]+)" references Domain "([^"]+)", but this DFD has no local Domains\.$/, (_match, object, domain) => `DFD object "${object}" が Domain "${domain}" を参照していますが、この DFD にはローカル Domains が定義されていません。`],
|
||||
[/^DFD local object "([^"]+)" is treated as an inline object without ref\.$/, (_match, object) => `DFD local object "${object}" は ref なしの図内定義として扱われます。`],
|
||||
[/^DFD object "([^"]+)" has no kind, and it could not be inferred from ref\.$/, (_match, object) => `DFD object "${object}" の kind がなく、ref からも推定できません。`],
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ const SECTION_HEADERS: Partial<Record<FileType, Record<string, string>>> = {
|
|||
"source links": SOURCE_LINKS_HEADER
|
||||
},
|
||||
"flow-diagram": {
|
||||
"domain sources": DOMAIN_SOURCES_HEADER,
|
||||
domains: DOMAIN_HEADER,
|
||||
objects: "id | label | kind | ref | domain | notes",
|
||||
flows: "id | from | to | kind | trigger | data | condition | notes",
|
||||
"source links": SOURCE_LINKS_HEADER
|
||||
|
|
@ -79,7 +81,7 @@ const SECTION_HEADERS: Partial<Record<FileType, Record<string, string>>> = {
|
|||
},
|
||||
mapping: {
|
||||
scope: "role | ref | notes",
|
||||
mappings: "source_ref | target_ref | transform | rule | required | notes",
|
||||
mappings: "target_ref | source_ref | transform | rule | required | notes",
|
||||
"source links": SOURCE_LINKS_HEADER
|
||||
},
|
||||
message: {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,31 @@ export function formatDfdObjectDomainWithoutLocalDomainsMessage(
|
|||
return `DFD object "${objectId}" references Domain "${domainId}", but this DFD has no local Domains.`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function formatFlowDiagramLocalDomainOverridesSourceMessage(
|
||||
id: string,
|
||||
field: "name" | "kind" | "parent",
|
||||
localValue: string,
|
||||
sourceValue: string
|
||||
): string {
|
||||
return `Flow Diagram local Domain "${id}" overrides Domain Source ${field} "${sourceValue}" with "${localValue}".`;
|
||||
}
|
||||
|
||||
export function formatFlowDiagramObjectUnknownLocalDomainMessage(
|
||||
objectId: string,
|
||||
domainId: string
|
||||
): string {
|
||||
return `Flow Diagram object "${objectId}" references unknown local Domain "${domainId}".`;
|
||||
}
|
||||
|
||||
export function formatFlowDiagramObjectUnknownDomainMessage(
|
||||
objectId: string,
|
||||
domainId: string
|
||||
): string {
|
||||
return `Flow Diagram object "${objectId}" references unknown Domain "${domainId}".`;
|
||||
}
|
||||
|
||||
export function formatStandaloneDomainDuplicateMessage(id: string): string {
|
||||
return `Domain "${id}" is defined in multiple Domains files.`;
|
||||
}
|
||||
|
|
|
|||
80
src/core/flow-diagram-view-mode.ts
Normal file
80
src/core/flow-diagram-view-mode.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import type { FlowDiagramModel, FlowDiagramViewMode } from "../types/models";
|
||||
|
||||
interface FlowDiagramViewModeEntry {
|
||||
mode: FlowDiagramViewMode;
|
||||
initializationKey: string;
|
||||
}
|
||||
|
||||
export interface FlowDiagramViewModeSyncResult {
|
||||
mode: FlowDiagramViewMode;
|
||||
initializationChanged: boolean;
|
||||
}
|
||||
|
||||
export function normalizeFlowDiagramViewMode(value: unknown): FlowDiagramViewMode | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "detail" || normalized === "screen" ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolveInitialFlowDiagramViewMode(
|
||||
diagram: Pick<FlowDiagramModel, "flowView" | "flowViewSpecified" | "flowViewRaw">,
|
||||
defaultViewMode?: unknown
|
||||
): FlowDiagramViewMode {
|
||||
return diagram.flowViewSpecified
|
||||
? diagram.flowView
|
||||
: normalizeFlowDiagramViewMode(defaultViewMode) ?? "detail";
|
||||
}
|
||||
|
||||
export function buildFlowDiagramViewModeInitializationKey(
|
||||
diagram: Pick<FlowDiagramModel, "flowView" | "flowViewSpecified" | "flowViewRaw">,
|
||||
defaultViewMode?: unknown
|
||||
): string {
|
||||
if (diagram.flowViewSpecified) {
|
||||
return `frontmatter:${diagram.flowView}`;
|
||||
}
|
||||
|
||||
const defaultMode = normalizeFlowDiagramViewMode(defaultViewMode) ?? "detail";
|
||||
const rawValue = typeof diagram.flowViewRaw === "string" ? diagram.flowViewRaw.trim() : "";
|
||||
return rawValue
|
||||
? `invalid:${rawValue}:settings:${defaultMode}`
|
||||
: `settings:${defaultMode}`;
|
||||
}
|
||||
|
||||
export class FlowDiagramViewModeState {
|
||||
private readonly entriesByFilePath = new Map<string, FlowDiagramViewModeEntry>();
|
||||
|
||||
synchronize(
|
||||
filePath: string,
|
||||
diagram: Pick<FlowDiagramModel, "flowView" | "flowViewSpecified" | "flowViewRaw">,
|
||||
defaultViewMode?: unknown
|
||||
): FlowDiagramViewModeSyncResult {
|
||||
const initializationKey = buildFlowDiagramViewModeInitializationKey(diagram, defaultViewMode);
|
||||
const existing = this.entriesByFilePath.get(filePath);
|
||||
if (existing?.initializationKey === initializationKey) {
|
||||
return { mode: existing.mode, initializationChanged: false };
|
||||
}
|
||||
|
||||
const mode = resolveInitialFlowDiagramViewMode(diagram, defaultViewMode);
|
||||
this.entriesByFilePath.set(filePath, { mode, initializationKey });
|
||||
return { mode, initializationChanged: true };
|
||||
}
|
||||
|
||||
getOrInitialize(
|
||||
filePath: string,
|
||||
diagram: Pick<FlowDiagramModel, "flowView" | "flowViewSpecified" | "flowViewRaw">,
|
||||
defaultViewMode?: unknown
|
||||
): FlowDiagramViewMode {
|
||||
return this.synchronize(filePath, diagram, defaultViewMode).mode;
|
||||
}
|
||||
|
||||
set(filePath: string, mode: FlowDiagramViewMode): void {
|
||||
const existing = this.entriesByFilePath.get(filePath);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
existing.mode = mode;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ import {
|
|||
formatDfdObjectUnknownDomainMessage,
|
||||
formatDfdObjectDomainWithoutLocalDomainsMessage,
|
||||
formatDfdObjectUnknownLocalDomainMessage,
|
||||
formatFlowDiagramLocalDomainOverridesSourceMessage,
|
||||
formatFlowDiagramObjectUnknownDomainMessage,
|
||||
formatFlowDiagramObjectUnknownLocalDomainMessage,
|
||||
formatDomainDiagramEmptySourceMessage,
|
||||
formatDomainDiagramInvalidSourceTypeMessage,
|
||||
formatDomainDiagramUnresolvedSourceMessage
|
||||
|
|
@ -129,19 +132,15 @@ function resolveDfdDiagramRelations(
|
|||
): ResolvedDiagram {
|
||||
const warnings: ValidationWarning[] = [];
|
||||
const isFlowDiagram = diagram.schema === "flow_diagram";
|
||||
const domainResolution = isFlowDiagram
|
||||
? { warnings: [], sourceSummaries: [], domains: [] }
|
||||
: resolveDfdDiagramDomains(diagram, index);
|
||||
const domainResolution = resolveDfdDiagramDomains(diagram, index);
|
||||
warnings.push(...domainResolution.warnings);
|
||||
const resolvedDiagram: DfdDiagramModel | FlowDiagramModel = isFlowDiagram
|
||||
? diagram
|
||||
: {
|
||||
...diagram,
|
||||
domainSourceSummaries: domainResolution.sourceSummaries,
|
||||
domains: domainResolution.domains
|
||||
};
|
||||
const resolvedDiagram: DfdDiagramModel | FlowDiagramModel = {
|
||||
...diagram,
|
||||
domainSourceSummaries: domainResolution.sourceSummaries,
|
||||
domains: domainResolution.domains
|
||||
};
|
||||
const objectResolution = resolveDfdDiagramObjects(resolvedDiagram, index, {
|
||||
hasDomainSources: diagram.schema === "dfd_diagram" && diagram.domainSources.length > 0
|
||||
hasDomainSources: diagram.domainSources.length > 0
|
||||
});
|
||||
const hasUnreadableFlowObjects = isFlowDiagram && hasInvalidDfdLikeSectionHeader(diagram.path, index, "Objects");
|
||||
const edges: DiagramEdge[] = [];
|
||||
|
|
@ -293,7 +292,7 @@ function getDiagnosticSectionName(warning: ValidationWarning): string | null {
|
|||
}
|
||||
|
||||
function resolveDfdDiagramDomains(
|
||||
diagram: DfdDiagramModel,
|
||||
diagram: DfdDiagramModel | FlowDiagramModel,
|
||||
index: ModelingVaultIndex
|
||||
): {
|
||||
domains: DomainEntry[];
|
||||
|
|
@ -383,12 +382,19 @@ function resolveDfdDiagramDomains(
|
|||
if (localValue && sourceValue && localValue !== sourceValue) {
|
||||
warnings.push({
|
||||
code: "invalid-structure",
|
||||
message: formatDfdLocalDomainOverridesSourceMessage(
|
||||
domain.id,
|
||||
field,
|
||||
localValue,
|
||||
sourceValue
|
||||
),
|
||||
message: diagram.schema === "flow_diagram"
|
||||
? formatFlowDiagramLocalDomainOverridesSourceMessage(
|
||||
domain.id,
|
||||
field,
|
||||
localValue,
|
||||
sourceValue
|
||||
)
|
||||
: formatDfdLocalDomainOverridesSourceMessage(
|
||||
domain.id,
|
||||
field,
|
||||
localValue,
|
||||
sourceValue
|
||||
),
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: `Domains.${field}`,
|
||||
|
|
@ -450,7 +456,7 @@ function resolveDfdDiagramObjects(
|
|||
rowIndex,
|
||||
compatibilityMode: "legacy_ref_only" as const
|
||||
}));
|
||||
const localDomainIds = new Set((diagram.schema === "dfd_diagram" ? diagram.domains ?? [] : []).map((domain) => domain.id));
|
||||
const localDomainIds = new Set((diagram.domains ?? []).map((domain) => domain.id));
|
||||
|
||||
for (const entry of entries) {
|
||||
const ref = entry.ref?.trim();
|
||||
|
|
@ -508,18 +514,28 @@ function resolveDfdDiagramObjects(
|
|||
field: "Objects.domain",
|
||||
context: { rowIndex: entry.rowIndex + 1 }
|
||||
});
|
||||
} else if (diagram.schema === "dfd_diagram" && domain && !localDomainIds.has(domain)) {
|
||||
} else if (domain && !localDomainIds.has(domain) && (diagram.schema === "dfd_diagram" || localDomainIds.size > 0 || domainContext.hasDomainSources)) {
|
||||
warnings.push({
|
||||
code: "unresolved-reference",
|
||||
message: domainContext.hasDomainSources
|
||||
? formatDfdObjectUnknownDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
)
|
||||
: formatDfdObjectUnknownLocalDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
),
|
||||
message: diagram.schema === "flow_diagram"
|
||||
? (domainContext.hasDomainSources
|
||||
? formatFlowDiagramObjectUnknownDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
)
|
||||
: formatFlowDiagramObjectUnknownLocalDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
))
|
||||
: (domainContext.hasDomainSources
|
||||
? formatDfdObjectUnknownDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
)
|
||||
: formatDfdObjectUnknownLocalDomainMessage(
|
||||
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
||||
domain
|
||||
)),
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: "Objects.domain",
|
||||
|
|
|
|||
|
|
@ -943,6 +943,10 @@ function createShallowModel(
|
|||
id,
|
||||
name,
|
||||
kind: "screen_communication",
|
||||
flowView: "detail",
|
||||
flowViewSpecified: false,
|
||||
domainSources: [],
|
||||
domains: [],
|
||||
objectRefs: [],
|
||||
objectEntries: [],
|
||||
nodes: [],
|
||||
|
|
|
|||
|
|
@ -1691,32 +1691,36 @@ function getMappingCompletion(
|
|||
}
|
||||
|
||||
if (section === "Mappings") {
|
||||
if (!hasTableHeader(lines, cursor.line, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])) {
|
||||
const mappingHeader = getNearestSupportedMappingHeader(lines, cursor.line);
|
||||
if (!mappingHeader) {
|
||||
return null;
|
||||
}
|
||||
if (cell.columnIndex === 0 || cell.columnIndex === 1) {
|
||||
const columnName = mappingHeader[cell.columnIndex];
|
||||
if (columnName === "source_ref" || columnName === "target_ref") {
|
||||
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
||||
const isSourceRef = columnName === "source_ref";
|
||||
const placeholder = isSourceRef ? "Complete mapping source_ref" : "Complete mapping target_ref";
|
||||
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
||||
cursor,
|
||||
cell,
|
||||
cellValue,
|
||||
index,
|
||||
cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref"
|
||||
placeholder
|
||||
);
|
||||
if (qualifiedMemberRequest) {
|
||||
return qualifiedMemberRequest;
|
||||
}
|
||||
return {
|
||||
kind: cell.columnIndex === 0 ? "mapping-source-ref" : "mapping-target-ref",
|
||||
kind: isSourceRef ? "mapping-source-ref" : "mapping-target-ref",
|
||||
replaceFrom: cell.replaceFrom,
|
||||
replaceTo: cell.replaceTo,
|
||||
suggestions: buildStructuredReferenceSuggestions(index),
|
||||
placeholder: cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref",
|
||||
placeholder,
|
||||
initialQuery: normalizeCompletionQuery(cellValue),
|
||||
tableColumnIndex: cell.columnIndex
|
||||
};
|
||||
}
|
||||
if (cell.columnIndex === 3) {
|
||||
if (columnName === "rule") {
|
||||
return {
|
||||
kind: "mapping-rule-ref",
|
||||
replaceFrom: cell.replaceFrom,
|
||||
|
|
@ -1731,17 +1735,17 @@ function getMappingCompletion(
|
|||
initialQuery: normalizeCompletionQuery(
|
||||
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
||||
),
|
||||
tableColumnIndex: 3
|
||||
tableColumnIndex: cell.columnIndex
|
||||
};
|
||||
}
|
||||
if (cell.columnIndex === 4) {
|
||||
if (columnName === "required") {
|
||||
return buildOptionCompletionRequest(
|
||||
"mapping-rule-ref",
|
||||
cell,
|
||||
line,
|
||||
["Y", "N"],
|
||||
"Complete mapping required",
|
||||
4
|
||||
cell.columnIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1958,6 +1962,32 @@ function getScreenFieldTargetSuggestions(lines: string[]): CompletionSuggestion[
|
|||
}));
|
||||
}
|
||||
|
||||
function getNearestSupportedMappingHeader(lines: string[], cursorLine: number): string[] | null {
|
||||
const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => {
|
||||
const row = parseMarkdownTableRow(candidate);
|
||||
return isSupportedMappingHeader(row);
|
||||
});
|
||||
if (tableHeaderIndex < 0 || cursorLine <= tableHeaderIndex + 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseMarkdownTableRow(lines[tableHeaderIndex] ?? "");
|
||||
}
|
||||
|
||||
function isSupportedMappingHeader(header: string[] | null): header is string[] {
|
||||
if (!header) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
sameOrderedHeaders(header, ["target_ref", "source_ref", "transform", "rule", "required", "notes"]) ||
|
||||
sameOrderedHeaders(header, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])
|
||||
);
|
||||
}
|
||||
|
||||
function sameOrderedHeaders(actual: readonly string[], expected: readonly string[]): boolean {
|
||||
return actual.length === expected.length && expected.every((header, index) => actual[index] === header);
|
||||
}
|
||||
|
||||
function hasTableHeader(
|
||||
lines: string[],
|
||||
cursorLine: number,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = {
|
|||
"appProcess.businessFlow.direction": "Business flow direction",
|
||||
"appProcess.businessFlow.direction.lr": "Left to right",
|
||||
"appProcess.businessFlow.direction.td": "Top down",
|
||||
"flowDiagram.viewMode": "Flow view",
|
||||
"flowDiagram.viewMode.detail": "Detail",
|
||||
"flowDiagram.viewMode.screen": "Screen",
|
||||
|
||||
/* eslint-disable obsidianmd/ui/sentence-case-locale-module -- Connect Flow is the requested toolbar label. */
|
||||
"appProcess.businessFlow.connectFlow.short": "Connect Flow",
|
||||
|
|
@ -407,6 +410,10 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = {
|
|||
"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",
|
||||
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module -- Flow Diagram is the requested format name.
|
||||
"settings.defaultFlowDiagramViewMode.name": "Default Flow Diagram view",
|
||||
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module -- Flow Diagram is the requested format name.
|
||||
"settings.defaultFlowDiagramViewMode.desc": "Used for a Flow Diagram initial view when frontmatter.flow_view is not set or is invalid.",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = {
|
|||
"appProcess.businessFlow.direction": "Business Flow 方向",
|
||||
"appProcess.businessFlow.direction.lr": "左から右",
|
||||
"appProcess.businessFlow.direction.td": "上から下",
|
||||
"flowDiagram.viewMode": "Flow view",
|
||||
"flowDiagram.viewMode.detail": "Detail",
|
||||
"flowDiagram.viewMode.screen": "Screen",
|
||||
|
||||
"appProcess.businessFlow.connectFlow.short": "Flow接続",
|
||||
"appProcess.businessFlow.connectFlow.title": "Flow接続",
|
||||
|
|
@ -358,6 +361,8 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = {
|
|||
"settings.defaultBusinessFlowDirection.desc": "app_process Business Flow preview で frontmatter.flow_direction が未設定の場合に使用します。",
|
||||
"settings.defaultBusinessFlowDirection.lr": "左から右",
|
||||
"settings.defaultBusinessFlowDirection.td": "上から下",
|
||||
"settings.defaultFlowDiagramViewMode.name": "Flow Diagram の初期表示モード",
|
||||
"settings.defaultFlowDiagramViewMode.desc": "Flow Diagram で frontmatter.flow_view が未設定または不正な場合の初期表示に使用します。",
|
||||
"settings.defaultScreenRenderMode.name": "Screen の初期 render_mode",
|
||||
"settings.defaultScreenRenderMode.desc": "screen ファイルで frontmatter.render_mode が未設定の場合に使用します。",
|
||||
"settings.defaultDomainsViewMode.name": "Domains の初期表示モード",
|
||||
|
|
|
|||
31
src/main.ts
31
src/main.ts
|
|
@ -166,11 +166,15 @@ const BUSINESS_FLOW_DIRECTION_OPTIONS: readonly ModelWeaveSettings["defaultBusin
|
|||
"LR",
|
||||
"TD"
|
||||
];
|
||||
|
||||
const DOMAIN_VIEW_MODE_OPTIONS: readonly ModelWeaveSettings["defaultDomainsViewMode"][] = [
|
||||
"mindmap",
|
||||
"area",
|
||||
"tree"
|
||||
];
|
||||
const FLOW_DIAGRAM_VIEW_MODE_OPTIONS: readonly ModelWeaveSettings["defaultFlowDiagramViewMode"][] = [
|
||||
"detail", "screen"
|
||||
];
|
||||
|
||||
function isClassRenderModeOption(
|
||||
value: string
|
||||
|
|
@ -202,9 +206,16 @@ function isBusinessFlowDirectionOption(
|
|||
return BUSINESS_FLOW_DIRECTION_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
|
||||
function isFlowDiagramViewModeOption(
|
||||
value: string
|
||||
): value is ModelWeaveSettings["defaultFlowDiagramViewMode"] {
|
||||
return FLOW_DIAGRAM_VIEW_MODE_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
|
||||
function isScreenRenderModeOption(
|
||||
value: string
|
||||
): value is ModelWeaveSettings["defaultScreenRenderMode"] {
|
||||
|
||||
return SCREEN_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
||||
}
|
||||
|
||||
|
|
@ -740,6 +751,7 @@ export default class ModelWeavePlugin extends Plugin {
|
|||
defaultDomainDiagramViewMode: this.settings.defaultDomainDiagramViewMode,
|
||||
defaultBusinessFlowDirection: this.settings.defaultBusinessFlowDirection,
|
||||
localSourceRoot: this.settings.localSourceRoot,
|
||||
defaultFlowDiagramViewMode: this.settings.defaultFlowDiagramViewMode,
|
||||
uiLanguage: this.settings.uiLanguage,
|
||||
showMermaidRenderDebug: this.settings.showMermaidRenderDebug
|
||||
};
|
||||
|
|
@ -4048,6 +4060,25 @@ class ModelWeaveSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName(t("settings.defaultFlowDiagramViewMode.name"))
|
||||
.setDesc(t("settings.defaultFlowDiagramViewMode.desc"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("detail", t("flowDiagram.viewMode.detail"))
|
||||
.addOption("screen", t("flowDiagram.viewMode.screen"))
|
||||
.setValue(settings.defaultFlowDiagramViewMode)
|
||||
.onChange(async (value) => {
|
||||
if (!isFlowDiagramViewModeOption(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.plugin.updateSettings({
|
||||
defaultFlowDiagramViewMode: value
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("settings.defaultScreenRenderMode.name"))
|
||||
.setDesc(t("settings.defaultScreenRenderMode.desc"))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ const DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"
|
|||
const LEGACY_OBJECT_HEADERS = ["ref", "notes"];
|
||||
const FLOW_OBJECT_KINDS = new Set<string>([
|
||||
"screen",
|
||||
"actor",
|
||||
"user",
|
||||
"message",
|
||||
"data",
|
||||
"api",
|
||||
"service",
|
||||
"handler",
|
||||
"process",
|
||||
"app_process",
|
||||
"context",
|
||||
|
|
@ -103,6 +110,9 @@ function parseDfdLikeDiagramFile(
|
|||
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim()
|
||||
? frontmatter.kind.trim()
|
||||
: options.defaultKind;
|
||||
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
|
||||
const flowViewRaw = frontmatter.flow_view;
|
||||
const flowViewSpecified = options.schema === "flow_diagram" && !isUnknownFlowDiagramViewMode(frontmatter.flow_view) && typeof frontmatter.flow_view === "string" && frontmatter.flow_view.trim().length > 0;
|
||||
|
||||
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
|
||||
const isAcceptedType = rawType === options.type ||
|
||||
|
|
@ -119,18 +129,17 @@ function parseDfdLikeDiagramFile(
|
|||
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
|
||||
warnings.push(createWarning(path, "kind", 'expected kind "screen_communication"'));
|
||||
}
|
||||
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
|
||||
warnings.push(createWarning(path, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
|
||||
}
|
||||
|
||||
const objectsTable = parseDfdObjectsTable(sections.Objects, path, {
|
||||
schema: options.schema,
|
||||
allowLegacyObjects: options.allowLegacyObjects,
|
||||
requireObjectId: options.requireObjectId
|
||||
});
|
||||
const domainsTable = options.schema === "dfd_diagram"
|
||||
? parseDomainEntries(sections.Domains, path)
|
||||
: { rows: [], warnings: [] };
|
||||
const domainSourcesTable = options.schema === "dfd_diagram"
|
||||
? parseDomainSourcesTable(sections["Domain Sources"], path)
|
||||
: { rows: [], warnings: [] };
|
||||
const domainsTable = parseDomainEntries(sections.Domains, path);
|
||||
const domainSourcesTable = parseDomainSourcesTable(sections["Domain Sources"], path);
|
||||
const flowHeaders = options.schema === "flow_diagram" ? FLOW_DIAGRAM_FLOW_HEADERS : DFD_FLOW_HEADERS;
|
||||
const flowsTable = parseMarkdownTable(sections.Flows, flowHeaders, path, "Flows");
|
||||
const hasInvalidFlowsHeader = flowsTable.warnings.some(
|
||||
|
|
@ -252,6 +261,11 @@ function parseDfdLikeDiagramFile(
|
|||
name: name || fallbackTitle,
|
||||
kind: "screen_communication",
|
||||
description: joinSectionLines(sections.Summary),
|
||||
flowView,
|
||||
flowViewSpecified,
|
||||
flowViewRaw,
|
||||
domainSources: domainSourcesTable.rows,
|
||||
domains: domainsTable.rows,
|
||||
objectRefs,
|
||||
objectEntries,
|
||||
nodes,
|
||||
|
|
@ -288,6 +302,14 @@ function parseDfdLikeDiagramFile(
|
|||
};
|
||||
}
|
||||
|
||||
function parseFlowDiagramViewMode(value: unknown): "detail" | "screen" {
|
||||
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
|
||||
}
|
||||
|
||||
function isUnknownFlowDiagramViewMode(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
|
||||
}
|
||||
|
||||
function getFileStem(path: string): string {
|
||||
return path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { parseFrontmatter } from "./frontmatter-parser";
|
||||
import { parseMarkdownTable } from "./markdown-table";
|
||||
import { parseMarkdownTable, splitMarkdownTableRow } from "./markdown-table";
|
||||
import { extractMarkdownSections } from "./markdown-sections";
|
||||
import { parseSourceLinks } from "./source-links-parser";
|
||||
import type { MappingModel, ValidationWarning } from "../types/models";
|
||||
|
||||
const SCOPE_HEADERS = ["role", "ref", "notes"];
|
||||
const MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"];
|
||||
const MAPPING_HEADERS = ["target_ref", "source_ref", "transform", "rule", "required", "notes"] as const;
|
||||
const LEGACY_MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"] as const;
|
||||
|
||||
export function parseMappingFile(
|
||||
markdown: string,
|
||||
|
|
@ -36,7 +37,12 @@ export function parseMappingFile(
|
|||
}
|
||||
|
||||
const scopeTable = parseMarkdownTable(sections.Scope, SCOPE_HEADERS, path, "Scope");
|
||||
const mappingsTable = parseMarkdownTable(sections.Mappings, MAPPING_HEADERS, path, "Mappings");
|
||||
const mappingsTable = parseMarkdownTable(
|
||||
sections.Mappings,
|
||||
getAcceptedMappingHeaders(sections.Mappings),
|
||||
path,
|
||||
"Mappings"
|
||||
);
|
||||
|
||||
warnings.push(...scopeTable.warnings, ...mappingsTable.warnings);
|
||||
|
||||
|
|
@ -80,6 +86,23 @@ export function parseMappingFile(
|
|||
};
|
||||
}
|
||||
|
||||
function getAcceptedMappingHeaders(lines: string[] | undefined): string[] {
|
||||
const actualHeader = getMarkdownTableHeader(lines);
|
||||
if (actualHeader && sameHeaders(actualHeader, LEGACY_MAPPING_HEADERS)) {
|
||||
return [...LEGACY_MAPPING_HEADERS];
|
||||
}
|
||||
return [...MAPPING_HEADERS];
|
||||
}
|
||||
|
||||
function getMarkdownTableHeader(lines: string[] | undefined): string[] | null {
|
||||
const headerLine = lines?.map((line) => line.trim()).find((line) => line.startsWith("|"));
|
||||
return headerLine ? splitMarkdownTableRow(headerLine) : null;
|
||||
}
|
||||
|
||||
function sameHeaders(headers: readonly string[], expectedHeaders: readonly string[]): boolean {
|
||||
return headers.length === expectedHeaders.length && expectedHeaders.every((header, index) => headers[index] === header);
|
||||
}
|
||||
|
||||
function getFileStem(path: string): string {
|
||||
return path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type {
|
|||
DiagramEdge,
|
||||
DfdDiagramModel,
|
||||
DomainEntry,
|
||||
FlowDiagramModel,
|
||||
FlowDiagramViewMode,
|
||||
ResolvedColorScheme,
|
||||
ResolvedColorStyle,
|
||||
ResolvedDiagram
|
||||
|
|
@ -84,6 +86,7 @@ export function renderDfdMermaidDiagram(
|
|||
exportPngTitle?: string;
|
||||
exportAndOpenPngLabel?: string;
|
||||
exportAndOpenPngTitle?: string;
|
||||
flowDiagramViewMode?: FlowDiagramViewMode;
|
||||
colorScheme?: ResolvedColorScheme;
|
||||
dfdDetailLabels?: DfdDetailLabels;
|
||||
}
|
||||
|
|
@ -106,26 +109,28 @@ export function renderDfdMermaidDiagram(
|
|||
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
||||
});
|
||||
|
||||
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram, options?.flowDiagramViewMode);
|
||||
|
||||
if (!options?.hideDetails) {
|
||||
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
|
||||
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
|
||||
if (domainDetails) {
|
||||
shell.root.appendChild(domainDetails);
|
||||
}
|
||||
shell.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
|
||||
shell.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram)
|
||||
? buildFlowDiagramHoverMetadata(diagram, sourcePath)
|
||||
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram)
|
||||
? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath)
|
||||
: null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
renderedDiagram,
|
||||
sourcePath
|
||||
);
|
||||
|
||||
const ready = renderMermaidSourceIntoShell(shell, {
|
||||
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
||||
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
|
||||
renderIdPrefix: "model_weave_dfd",
|
||||
fitVerticalAlign: options?.fitVerticalAlign,
|
||||
viewportState: options?.viewportState,
|
||||
|
|
@ -155,7 +160,7 @@ export function renderDfdMermaidDiagram(
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label
|
||||
? `${target.label} (${target.targetType ?? "model"})`
|
||||
: target.linktext,
|
||||
|
|
@ -437,10 +442,11 @@ function buildDfdMermaidInteractionTargets(
|
|||
|
||||
export function buildDfdMermaidSource(
|
||||
diagram: ResolvedDiagram,
|
||||
colorScheme?: ResolvedColorScheme
|
||||
colorScheme?: ResolvedColorScheme,
|
||||
flowDiagramViewMode?: FlowDiagramViewMode
|
||||
): string {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return buildFlowDiagramMermaidSource(diagram, colorScheme);
|
||||
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram, flowDiagramViewMode), colorScheme);
|
||||
}
|
||||
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
|
|
@ -520,6 +526,13 @@ export function buildDfdMermaidSource(
|
|||
}
|
||||
}
|
||||
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
|
|
@ -528,36 +541,184 @@ export function buildDfdMermaidSource(
|
|||
}
|
||||
|
||||
|
||||
function getFlowDiagramRenderedDiagram(
|
||||
diagram: ResolvedDiagram,
|
||||
effectiveViewMode?: FlowDiagramViewMode
|
||||
): ResolvedDiagram {
|
||||
if (!isFlowDiagramModel(diagram.diagram) || effectiveViewMode !== "screen") {
|
||||
return diagram;
|
||||
}
|
||||
return buildFlowDiagramScreenFlowProjection(diagram);
|
||||
}
|
||||
|
||||
|
||||
export function buildFlowDiagramScreenFlowProjection(diagram: ResolvedDiagram): ResolvedDiagram {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return diagram;
|
||||
}
|
||||
|
||||
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
|
||||
if (visibleNodes.length === 0) {
|
||||
return diagram;
|
||||
}
|
||||
|
||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const outgoing = new Map<string, DiagramEdge[]>();
|
||||
for (const edge of diagram.edges) {
|
||||
const edges = outgoing.get(edge.source) ?? [];
|
||||
edges.push(edge);
|
||||
outgoing.set(edge.source, edges);
|
||||
}
|
||||
|
||||
const projectedByPair = new Map<string, DiagramEdge>();
|
||||
for (const sourceNode of visibleNodes) {
|
||||
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
|
||||
edge,
|
||||
visitedEdges: new Set<string>([getProjectionEdgeVisitKey(edge)])
|
||||
}));
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetId = current.edge.target;
|
||||
if (visibleIds.has(targetId)) {
|
||||
if (targetId !== sourceNode.id) {
|
||||
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const nextEdge of outgoing.get(targetId) ?? []) {
|
||||
const visitKey = getProjectionEdgeVisitKey(nextEdge);
|
||||
if (current.visitedEdges.has(visitKey)) {
|
||||
continue;
|
||||
}
|
||||
const nextVisitedEdges = new Set(current.visitedEdges);
|
||||
nextVisitedEdges.add(visitKey);
|
||||
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
diagram: {
|
||||
...diagram.diagram,
|
||||
flowView: "detail"
|
||||
},
|
||||
nodes: visibleNodes,
|
||||
edges: [...projectedByPair.values()]
|
||||
};
|
||||
}
|
||||
|
||||
function isScreenFlowVisibleNode(node: DiagramNode): boolean {
|
||||
switch (node.kind) {
|
||||
case "screen":
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "context":
|
||||
case "message":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function addProjectedScreenFlowEdge(
|
||||
projectedByPair: Map<string, DiagramEdge>,
|
||||
source: string,
|
||||
target: string,
|
||||
incomingEdge: DiagramEdge
|
||||
): void {
|
||||
const key = `${source}->${target}`;
|
||||
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
|
||||
const existing = projectedByPair.get(key);
|
||||
if (!existing) {
|
||||
projectedByPair.set(key, {
|
||||
id: `screen_flow:${source}->${target}`,
|
||||
source,
|
||||
target,
|
||||
kind: "flow",
|
||||
label,
|
||||
metadata: {
|
||||
...incomingEdge.metadata,
|
||||
projected: true,
|
||||
sourceFlowId: incomingEdge.id,
|
||||
dataRaw: undefined
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
existing.label = mergeScreenFlowLabels(existing.label, label);
|
||||
}
|
||||
|
||||
function buildScreenFlowProjectionEdgeLabel(edge: DiagramEdge): string | undefined {
|
||||
return getStringMetadata(edge.metadata, "condition")?.trim() ||
|
||||
getStringMetadata(edge.metadata, "trigger")?.trim() ||
|
||||
getStringMetadata(edge.metadata, "flowKind")?.trim() ||
|
||||
undefined;
|
||||
}
|
||||
|
||||
function mergeScreenFlowLabels(left: string | undefined, right: string | undefined): string | undefined {
|
||||
const values = [
|
||||
...(left ?? "").split(" / "),
|
||||
...(right ?? "").split(" / ")
|
||||
]
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const unique = [...new Set(values)];
|
||||
if (unique.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const merged = unique.join(" / ");
|
||||
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
|
||||
}
|
||||
|
||||
function getProjectionEdgeVisitKey(edge: DiagramEdge): string {
|
||||
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
|
||||
}
|
||||
|
||||
|
||||
function buildFlowDiagramMermaidSource(
|
||||
diagram: ResolvedDiagram,
|
||||
colorScheme?: ResolvedColorScheme
|
||||
colorScheme?: ResolvedColorScheme,
|
||||
): string {
|
||||
const palette = getModelWeaveMermaidPalette();
|
||||
const lines: string[] = [
|
||||
"flowchart LR",
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
];
|
||||
const lines: string[] = ["flowchart LR"];
|
||||
const colorClasses = new Map<string, ResolvedColorStyle>();
|
||||
if (!colorScheme) {
|
||||
lines.push(
|
||||
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
||||
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
|
||||
);
|
||||
}
|
||||
const nodeIds = new Map<string, string>();
|
||||
const domainStyles: string[] = [];
|
||||
const flowDomains = getFlowDiagramDomains(diagram);
|
||||
const usesResolvedDomains = flowDomains.length > 0;
|
||||
const flowDomainsById = new Map(flowDomains.map((domain) => [domain.id, domain]));
|
||||
const groupedNodes = new Map<string, Array<typeof diagram.nodes[number]>>();
|
||||
const ungroupedNodes: typeof diagram.nodes = [];
|
||||
|
||||
for (const node of diagram.nodes) {
|
||||
const domainId = getNodeDomainId(node);
|
||||
if (domainId) {
|
||||
if (!flowDomainsById.has(domainId)) {
|
||||
flowDomainsById.set(domainId, createSyntheticDomainEntry(domainId, flowDomainsById.size));
|
||||
}
|
||||
if (domainId && flowDomainsById.has(domainId)) {
|
||||
if (!groupedNodes.has(domainId)) {
|
||||
groupedNodes.set(domainId, []);
|
||||
}
|
||||
groupedNodes.get(domainId)!.push(node);
|
||||
} else if (domainId && !usesResolvedDomains) {
|
||||
const synthetic = createSyntheticDomainEntry(domainId, flowDomainsById.size);
|
||||
flowDomainsById.set(domainId, synthetic);
|
||||
groupedNodes.set(domainId, [node]);
|
||||
} else {
|
||||
ungroupedNodes.push(node);
|
||||
}
|
||||
|
|
@ -571,12 +732,13 @@ function buildFlowDiagramMermaidSource(
|
|||
nodeIds,
|
||||
1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
||||
for (const node of ungroupedNodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
|
||||
}
|
||||
|
||||
for (const edge of diagram.edges) {
|
||||
|
|
@ -594,6 +756,13 @@ function buildFlowDiagramMermaidSource(
|
|||
}
|
||||
}
|
||||
|
||||
if (colorClasses.size > 0) {
|
||||
lines.push("");
|
||||
for (const [className, style] of colorClasses) {
|
||||
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainStyles.length > 0) {
|
||||
lines.push("", ...domainStyles);
|
||||
}
|
||||
|
|
@ -608,6 +777,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
nodeIds: Map<string, string>,
|
||||
depth: number,
|
||||
colorScheme: ResolvedColorScheme | undefined,
|
||||
colorClasses: Map<string, ResolvedColorStyle>,
|
||||
domainStyles: string[]
|
||||
): boolean {
|
||||
const childLines: string[] = [];
|
||||
|
|
@ -619,6 +789,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
nodeIds,
|
||||
depth + 1,
|
||||
colorScheme,
|
||||
colorClasses,
|
||||
domainStyles
|
||||
);
|
||||
}
|
||||
|
|
@ -633,7 +804,7 @@ function appendFlowDiagramDomainSubgraph(
|
|||
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
|
||||
lines.push(...childLines);
|
||||
for (const node of nodes) {
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
|
||||
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
|
||||
}
|
||||
lines.push(`${indent}end`);
|
||||
|
||||
|
|
@ -651,11 +822,15 @@ function appendFlowDiagramNode(
|
|||
lines: string[],
|
||||
node: DiagramNode,
|
||||
nodeIds: Map<string, string>,
|
||||
depth: number
|
||||
depth: number,
|
||||
colorScheme?: ResolvedColorScheme,
|
||||
colorClasses?: Map<string, ResolvedColorStyle>
|
||||
): void {
|
||||
const mermaidId = toMermaidNodeId(node.id);
|
||||
const shape = toFlowDiagramMermaidShape(node.kind);
|
||||
const className = toFlowDiagramClassName(node.kind);
|
||||
const className = colorScheme
|
||||
? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses)
|
||||
: toFlowDiagramClassName(node.kind);
|
||||
const indent = " ".repeat(depth);
|
||||
nodeIds.set(node.id, mermaidId);
|
||||
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel(node.label ?? node.ref ?? node.id)}" }`);
|
||||
|
|
@ -663,15 +838,7 @@ function appendFlowDiagramNode(
|
|||
}
|
||||
|
||||
function getFlowDiagramDomains(diagram: ResolvedDiagram): DomainEntry[] {
|
||||
const resolvedDomains = getOptionalResolvedDomains(diagram);
|
||||
const domainsById = new Map(resolvedDomains.map((domain) => [domain.id, domain]));
|
||||
for (const node of diagram.nodes) {
|
||||
const domainId = getNodeDomainId(node);
|
||||
if (domainId && !domainsById.has(domainId)) {
|
||||
domainsById.set(domainId, createSyntheticDomainEntry(domainId, domainsById.size));
|
||||
}
|
||||
}
|
||||
return Array.from(domainsById.values());
|
||||
return getOptionalResolvedDomains(diagram);
|
||||
}
|
||||
|
||||
function getOptionalResolvedDomains(diagram: ResolvedDiagram): DomainEntry[] {
|
||||
|
|
@ -701,7 +868,7 @@ function getFlowDomainColorKind(domain: DomainEntry): string {
|
|||
|
||||
export function getDfdMermaidColorSchemeTargets(diagram: ResolvedDiagram): string[] {
|
||||
if (isFlowDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) ? ["domain"] : [];
|
||||
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
|
||||
}
|
||||
if (isDfdDiagramModel(diagram.diagram)) {
|
||||
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0
|
||||
|
|
@ -727,6 +894,13 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
|
|||
case "app_process":
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "actor":
|
||||
case "user":
|
||||
case "message":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
case "external":
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
@ -734,6 +908,24 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
|
|||
}
|
||||
}
|
||||
|
||||
function registerFlowDiagramColorClass(
|
||||
kind: unknown,
|
||||
colorScheme: ResolvedColorScheme,
|
||||
colorClasses: Map<string, ResolvedColorStyle> | undefined
|
||||
): string {
|
||||
const className = toFlowDiagramColorClassName(kind);
|
||||
colorClasses?.set(
|
||||
className,
|
||||
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : undefined)
|
||||
);
|
||||
return className;
|
||||
}
|
||||
|
||||
function toFlowDiagramColorClassName(kind: unknown): string {
|
||||
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
|
||||
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
|
||||
}
|
||||
|
||||
function toFlowDiagramClassName(kind: unknown): string {
|
||||
switch (kind) {
|
||||
case "screen":
|
||||
|
|
@ -744,11 +936,18 @@ function toFlowDiagramClassName(kind: unknown): string {
|
|||
return "store";
|
||||
case "context":
|
||||
case "work_object":
|
||||
case "message":
|
||||
return "context";
|
||||
case "process":
|
||||
case "app_process":
|
||||
case "data":
|
||||
case "api":
|
||||
case "service":
|
||||
case "handler":
|
||||
return "process";
|
||||
case "external":
|
||||
case "actor":
|
||||
case "user":
|
||||
return "external";
|
||||
case "unknown":
|
||||
default:
|
||||
|
|
@ -818,7 +1017,7 @@ function isDfdDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is DfdD
|
|||
return diagram.schema === "dfd_diagram";
|
||||
}
|
||||
|
||||
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): boolean {
|
||||
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is FlowDiagramModel {
|
||||
return diagram.schema === "flow_diagram";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { App } from "obsidian";
|
||||
import type { ResolvedColorScheme, ResolvedDiagram } from "../types/models";
|
||||
import type { FlowDiagramViewMode, ResolvedColorScheme, ResolvedDiagram } from "../types/models";
|
||||
import type { RenderMode } from "../core/render-mode";
|
||||
import { modelWeaveText } from "../i18n/language";
|
||||
import type {
|
||||
|
|
@ -42,6 +42,7 @@ export function renderDiagramModel(
|
|||
onExportPng?: () => void | Promise<void>;
|
||||
onExportAndOpenPng?: () => void | Promise<void>;
|
||||
exportPngLabel?: string;
|
||||
flowDiagramViewMode?: FlowDiagramViewMode;
|
||||
exportPngTitle?: string;
|
||||
exportAndOpenPngLabel?: string;
|
||||
exportAndOpenPngTitle?: string;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import {
|
|||
normalizeAppProcessBusinessFlowDirectionWithFallback,
|
||||
type AppProcessBusinessFlowDirection
|
||||
} from "../core/app-process-business-flow-direction";
|
||||
import type { FlowDiagramViewMode } from "../types/models";
|
||||
import type { ModelWeaveUiLanguage } from "../i18n/messages";
|
||||
|
||||
export type ModelWeaveDefaultZoom = "fit" | "100";
|
||||
export type ModelWeaveFontSize = "small" | "normal" | "large";
|
||||
export type ModelWeaveNodeDensity = "compact" | "normal" | "relaxed";
|
||||
export type ModelWeaveDomainViewMode = "mindmap" | "area" | "tree";
|
||||
export type ModelWeaveFlowDiagramViewMode = FlowDiagramViewMode;
|
||||
|
||||
export const DOMAIN_VIEW_MODE_SETTING_OPTIONS: ReadonlyArray<{
|
||||
value: ModelWeaveDomainViewMode;
|
||||
|
|
@ -26,6 +28,7 @@ export interface ModelWeaveSettings {
|
|||
defaultProcessRenderMode: RenderMode;
|
||||
defaultBusinessFlowDirection: AppProcessBusinessFlowDirection;
|
||||
defaultScreenRenderMode: RenderMode;
|
||||
defaultFlowDiagramViewMode: ModelWeaveFlowDiagramViewMode;
|
||||
defaultDomainsViewMode: ModelWeaveDomainViewMode;
|
||||
defaultDomainDiagramViewMode: ModelWeaveDomainViewMode;
|
||||
defaultZoom: ModelWeaveDefaultZoom;
|
||||
|
|
@ -43,6 +46,7 @@ export type ModelWeaveViewerPreferences = Pick<
|
|||
| "defaultZoom"
|
||||
| "defaultBusinessFlowDirection"
|
||||
| "defaultDomainsViewMode"
|
||||
| "defaultFlowDiagramViewMode"
|
||||
| "defaultDomainDiagramViewMode"
|
||||
| "fontSize"
|
||||
| "nodeDensity"
|
||||
|
|
@ -59,6 +63,7 @@ export const DEFAULT_MODEL_WEAVE_SETTINGS: ModelWeaveSettings = {
|
|||
defaultProcessRenderMode: "custom",
|
||||
defaultBusinessFlowDirection: "LR",
|
||||
defaultScreenRenderMode: "custom",
|
||||
defaultFlowDiagramViewMode: "detail",
|
||||
defaultDomainsViewMode: "mindmap",
|
||||
defaultDomainDiagramViewMode: "mindmap",
|
||||
defaultZoom: "fit",
|
||||
|
|
@ -105,6 +110,9 @@ const VALID_DOMAIN_VIEW_MODES = new Set<ModelWeaveDomainViewMode>([
|
|||
"area",
|
||||
"tree"
|
||||
]);
|
||||
const VALID_FLOW_DIAGRAM_VIEW_MODES = new Set<ModelWeaveFlowDiagramViewMode>([
|
||||
"detail", "screen"
|
||||
]);
|
||||
const VALID_UI_LANGUAGES = new Set<ModelWeaveUiLanguage>(["auto", "en", "ja"]);
|
||||
|
||||
export function normalizeModelWeaveSettings(
|
||||
|
|
@ -146,6 +154,11 @@ export function normalizeModelWeaveSettings(
|
|||
SCREEN_RENDER_MODES,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS.defaultScreenRenderMode
|
||||
),
|
||||
defaultFlowDiagramViewMode: normalizeEnumValue(
|
||||
raw.defaultFlowDiagramViewMode,
|
||||
VALID_FLOW_DIAGRAM_VIEW_MODES,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS.defaultFlowDiagramViewMode
|
||||
),
|
||||
defaultDomainsViewMode: normalizeEnumValue(
|
||||
raw.defaultDomainsViewMode,
|
||||
VALID_DOMAIN_VIEW_MODES,
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ tags:
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## Rules
|
||||
|
|
|
|||
|
|
@ -17,8 +17,17 @@ import type { AppProcessBusinessFlowDirection } from "../core/app-process-busine
|
|||
export type FileType = (typeof FILE_TYPES)[number];
|
||||
export type DfdObjectKind = (typeof DFD_OBJECT_KINDS)[number];
|
||||
export type DfdDiagramObjectKind = DfdObjectKind | "other";
|
||||
export type FlowDiagramViewMode = "detail" | "screen";
|
||||
|
||||
export type FlowDiagramObjectKind =
|
||||
| "screen"
|
||||
| "actor"
|
||||
| "user"
|
||||
| "message"
|
||||
| "data"
|
||||
| "api"
|
||||
| "service"
|
||||
| "handler"
|
||||
| "process"
|
||||
| "app_process"
|
||||
| "context"
|
||||
|
|
@ -694,6 +703,12 @@ export interface FlowDiagramModel extends BaseFileModel<"flow-diagram"> {
|
|||
name: string;
|
||||
kind: "screen_communication";
|
||||
description?: string;
|
||||
flowView: FlowDiagramViewMode;
|
||||
flowViewSpecified: boolean;
|
||||
flowViewRaw?: unknown;
|
||||
domainSources: DomainSourceRef[];
|
||||
domainSourceSummaries?: DomainDiagramSourceSummary[];
|
||||
domains?: DomainEntry[];
|
||||
objectRefs: string[];
|
||||
objectEntries: DfdDiagramObjectEntry[];
|
||||
nodes: DiagramNode[];
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import {
|
|||
exportDiagramSnapshotAsPng
|
||||
} from "../export/png-export";
|
||||
import { renderDiagramModel } from "../renderers/diagram-renderer";
|
||||
import { getDfdMermaidColorSchemeTargets } from "../renderers/dfd-mermaid";
|
||||
import {
|
||||
getDfdMermaidColorSchemeTargets
|
||||
} from "../renderers/dfd-mermaid";
|
||||
import { FlowDiagramViewModeState } from "../core/flow-diagram-view-mode";
|
||||
import {
|
||||
getAppProcessBusinessFlowColorSchemeTargets,
|
||||
renderAppProcessBusinessFlow,
|
||||
|
|
@ -90,6 +93,7 @@ import type {
|
|||
ErEntity,
|
||||
ImpactReference,
|
||||
ImpactSourceLink,
|
||||
FlowDiagramViewMode,
|
||||
ImpactSummary,
|
||||
ObjectModel,
|
||||
RelationsFileModel,
|
||||
|
|
@ -442,6 +446,7 @@ const DEFAULT_VIEWER_PREFERENCES: ModelWeaveViewerPreferences = {
|
|||
defaultDomainsViewMode: "mindmap",
|
||||
defaultDomainDiagramViewMode: "mindmap",
|
||||
defaultBusinessFlowDirection: "LR",
|
||||
defaultFlowDiagramViewMode: "detail",
|
||||
localSourceRoot: "",
|
||||
uiLanguage: "auto",
|
||||
showMermaidRenderDebug: false
|
||||
|
|
@ -508,6 +513,7 @@ export class ModelingPreviewView extends ItemView {
|
|||
private appProcessBusinessFlowDirectionOverride: AppProcessBusinessFlowDirection | null = null;
|
||||
private appProcessBusinessFlowDirectionFilePath: string | null = null;
|
||||
private appProcessFlowConnectFilePath: string | null = null;
|
||||
private readonly flowDiagramViewModes = new FlowDiagramViewModeState();
|
||||
private appProcessFlowConnectModeEnabled = false;
|
||||
private appProcessFlowConnectSourceStepId: string | null = null;
|
||||
private domainsDiagramModeState: "domains" | "domain-diagram" | null = null;
|
||||
|
|
@ -709,6 +715,11 @@ export class ModelingPreviewView extends ItemView {
|
|||
this.prepareDomainsDiagramMode(state, nextFilePath);
|
||||
this.prepareAppProcessBusinessFlowDirection(state, nextFilePath);
|
||||
this.prepareAppProcessFlowConnectMode(nextFilePath);
|
||||
const flowViewReinitialized = this.prepareFlowDiagramViewMode(state, nextFilePath);
|
||||
if (flowViewReinitialized && nextFilePath) {
|
||||
this.viewportStateCache.delete(nextFilePath);
|
||||
resetGraphViewportState(this.diagramViewportState);
|
||||
}
|
||||
this.prepareViewportState(state, reason);
|
||||
this.state = state;
|
||||
this.renderCurrentState();
|
||||
|
|
@ -1019,6 +1030,23 @@ export class ModelingPreviewView extends ItemView {
|
|||
this.domainsDiagramModeState = state.mode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private prepareFlowDiagramViewMode(
|
||||
state: PreviewState,
|
||||
nextFilePath: string | null
|
||||
): boolean {
|
||||
if (state.mode !== "diagram" || state.diagram.diagram.schema !== "flow_diagram" || !nextFilePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.flowDiagramViewModes.synchronize(
|
||||
nextFilePath,
|
||||
state.diagram.diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
).initializationChanged;
|
||||
}
|
||||
|
||||
private rememberViewportState(filePath: string, state: GraphViewportState): void {
|
||||
if (
|
||||
!state.hasAutoFitted &&
|
||||
|
|
@ -1091,6 +1119,9 @@ export class ModelingPreviewView extends ItemView {
|
|||
forExport: true,
|
||||
renderMode: getStandardRenderMode(state.rendererSelection),
|
||||
colorScheme: state.colorScheme,
|
||||
flowDiagramViewMode: state.diagram.diagram.schema === "flow_diagram"
|
||||
? this.getFlowDiagramViewMode(state)
|
||||
: undefined,
|
||||
...getMermaidSourceLabels(this.t)
|
||||
})
|
||||
};
|
||||
|
|
@ -2447,6 +2478,42 @@ export class ModelingPreviewView extends ItemView {
|
|||
rightGroup.appendChild(wrapper);
|
||||
}
|
||||
|
||||
private appendFlowDiagramViewSelector(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-flow-diagram-view-select-group")?.remove();
|
||||
|
||||
const wrapper = container.ownerDocument.createElement("div");
|
||||
wrapper.className =
|
||||
"model-weave-flow-diagram-view-select-group model-weave-render-mode-row";
|
||||
|
||||
const label = container.ownerDocument.createElement("span");
|
||||
label.addClass("model-weave-render-mode-label");
|
||||
label.textContent = this.t("flowDiagram.viewMode");
|
||||
wrapper.appendChild(label);
|
||||
|
||||
const select = container.ownerDocument.createElement("select");
|
||||
select.addClass("model-weave-flow-diagram-view-select");
|
||||
for (const mode of ["detail", "screen"] as const) {
|
||||
const option = container.ownerDocument.createElement("option");
|
||||
option.value = mode;
|
||||
option.textContent = this.t(`flowDiagram.viewMode.${mode}`);
|
||||
option.selected = this.getFlowDiagramViewModeForFile(filePath) === mode;
|
||||
select.appendChild(option);
|
||||
}
|
||||
select.addEventListener("change", () => {
|
||||
this.setFlowDiagramViewMode(select.value, filePath);
|
||||
});
|
||||
wrapper.appendChild(select);
|
||||
|
||||
const rightGroup = toolbar.querySelector<HTMLElement>(".model-weave-zoom-toolbar-right") ?? toolbar;
|
||||
rightGroup.appendChild(wrapper);
|
||||
}
|
||||
|
||||
private appendAppProcessFlowConnectControl(
|
||||
container: HTMLElement,
|
||||
filePath: string
|
||||
|
|
@ -2626,6 +2693,48 @@ export class ModelingPreviewView extends ItemView {
|
|||
? this.appProcessBusinessFlowDirectionOverride
|
||||
: null;
|
||||
}
|
||||
|
||||
private getFlowDiagramViewMode(state: Extract<PreviewState, { mode: "diagram" }>): FlowDiagramViewMode {
|
||||
const diagram = state.diagram.diagram;
|
||||
if (diagram.schema !== "flow_diagram") {
|
||||
return "detail";
|
||||
}
|
||||
return this.flowDiagramViewModes.getOrInitialize(
|
||||
diagram.path,
|
||||
diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
);
|
||||
}
|
||||
|
||||
private getFlowDiagramViewModeForFile(filePath: string): FlowDiagramViewMode {
|
||||
const diagramState = this.state.mode === "diagram" && this.state.diagram.diagram.path === filePath
|
||||
? this.state
|
||||
: null;
|
||||
if (!diagramState || diagramState.diagram.diagram.schema !== "flow_diagram") {
|
||||
return "detail";
|
||||
}
|
||||
return this.flowDiagramViewModes.getOrInitialize(
|
||||
filePath,
|
||||
diagramState.diagram.diagram,
|
||||
this.viewerPreferences.defaultFlowDiagramViewMode
|
||||
);
|
||||
}
|
||||
|
||||
private setFlowDiagramViewMode(mode: unknown, filePath: string): void {
|
||||
if (mode !== "detail" && mode !== "screen") {
|
||||
return;
|
||||
}
|
||||
if (this.getFlowDiagramViewModeForFile(filePath) === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.flowDiagramViewModes.set(filePath, mode);
|
||||
this.viewportStateCache.delete(filePath);
|
||||
resetGraphViewportState(this.diagramViewportState);
|
||||
this.renderCurrentState();
|
||||
this.restoreCurrentScrollPosition();
|
||||
}
|
||||
|
||||
private setAppProcessBusinessFlowDirection(
|
||||
direction: unknown,
|
||||
filePath: string
|
||||
|
|
@ -4176,6 +4285,9 @@ export class ModelingPreviewView extends ItemView {
|
|||
onOpenObject: state.onOpenObject ?? undefined,
|
||||
app: this.app,
|
||||
interactionSourcePath: filePath,
|
||||
flowDiagramViewMode: state.diagram.diagram.schema === "flow_diagram"
|
||||
? this.getFlowDiagramViewMode(state)
|
||||
: undefined,
|
||||
renderMode: getStandardRenderMode(state.rendererSelection),
|
||||
colorScheme: state.colorScheme,
|
||||
viewportState: this.diagramViewportState,
|
||||
|
|
@ -4192,6 +4304,9 @@ export class ModelingPreviewView extends ItemView {
|
|||
ensureGraphIdentityTitle(diagramRoot, buildGraphIdentityTitle(state.diagram.diagram));
|
||||
this.appendRendererSelection(diagramRoot, state.rendererSelection);
|
||||
this.appendViewerToolbarControls(diagramRoot);
|
||||
if (isFlowDiagramViewSelectorVisible(state.diagram)) {
|
||||
this.appendFlowDiagramViewSelector(diagramRoot, filePath);
|
||||
}
|
||||
this.moveDetailSections(diagramRoot, lowerSlots.details);
|
||||
this.renderImpactSummarySection(
|
||||
lowerSlots.impact,
|
||||
|
|
@ -6928,6 +7043,10 @@ function getModelDisplayName(value: unknown): string | undefined {
|
|||
getModelId(value)
|
||||
);
|
||||
}
|
||||
export function isFlowDiagramViewSelectorVisible(diagram: ResolvedDiagram): boolean {
|
||||
return diagram.diagram.schema === "flow_diagram";
|
||||
}
|
||||
|
||||
|
||||
function getModelId(value: unknown): string | undefined {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ test("section guidance resolves stable headers across supported model types", ()
|
|||
["data_object", "Format", "key | value | notes"],
|
||||
["app_process", "Steps", "id | domain | label | kind | input | output | rule | invoke | screen | notes"],
|
||||
["screen", "Actions", "id | label | kind | target | event | condition | invoke | transition | rule | notes"],
|
||||
["mapping", "Mappings", "source_ref | target_ref | transform | rule | required | notes"],
|
||||
["mapping", "Mappings", "target_ref | source_ref | transform | rule | required | notes"],
|
||||
["codeset", "Values", "code | label | sort_order | active | notes"],
|
||||
["domains", "Domains", "id | name | kind | parent | description"],
|
||||
["domain_diagram", "Domain Sources", "ref | notes"],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ await build({
|
|||
'export { parseDomainDiagramFile } from "./src/parsers/domain-diagram-parser";',
|
||||
'export { parseColorSchemeFile } from "./src/parsers/color-scheme-parser";',
|
||||
'export { parseDfdDiagramFile } from "./src/parsers/dfd-diagram-parser";',
|
||||
'export { parseMappingFile } from "./src/parsers/mapping-parser";',
|
||||
'export { parseDiagramFile } from "./src/parsers/diagram-parser";',
|
||||
'export { MODEL_WEAVE_TEMPLATES } from "./src/templates/model-weave-templates";',
|
||||
'export { DEFAULT_MODEL_WEAVE_SETTINGS, DOMAIN_VIEW_MODE_SETTING_OPTIONS, normalizeModelWeaveSettings } from "./src/settings/model-weave-settings";',
|
||||
|
|
@ -92,6 +93,7 @@ const {
|
|||
parseDomainDiagramFile,
|
||||
parseColorSchemeFile,
|
||||
parseDfdDiagramFile,
|
||||
parseMappingFile,
|
||||
parseDiagramFile,
|
||||
MODEL_WEAVE_TEMPLATES,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS,
|
||||
|
|
@ -2376,6 +2378,63 @@ name: Menu actions
|
|||
|
||||
|
||||
|
||||
|
||||
test("mapping parser accepts target-first Mappings header as canonical", () => {
|
||||
const { file, warnings } = parseMappingFile(`---
|
||||
type: mapping
|
||||
id: MAP-TARGET-FIRST
|
||||
name: Target first
|
||||
---
|
||||
|
||||
## Mappings
|
||||
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-TARGET]].status_id | [[DATA-SOURCE]].issue | copy | [[RULE-MAP]] | Y | target first row |
|
||||
`, "MAP-TARGET-FIRST.md");
|
||||
|
||||
assert.equal(file?.fileType, "mapping");
|
||||
assert.equal(warnings.some((warning) => warning.code === "invalid-table-column"), false);
|
||||
assert.deepEqual(file?.mappings, [
|
||||
{
|
||||
sourceRef: "[[DATA-SOURCE]].issue",
|
||||
targetRef: "[[DATA-TARGET]].status_id",
|
||||
transform: "copy",
|
||||
rule: "[[RULE-MAP]]",
|
||||
required: "Y",
|
||||
notes: "target first row"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("mapping parser accepts legacy source-first Mappings header without warning", () => {
|
||||
const { file, warnings } = parseMappingFile(`---
|
||||
type: mapping
|
||||
id: MAP-SOURCE-FIRST
|
||||
name: Source first
|
||||
---
|
||||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [[DATA-SOURCE]].issue | [[DATA-TARGET]].status_id | copy | [[RULE-MAP]] | Y | source first row |
|
||||
`, "MAP-SOURCE-FIRST.md");
|
||||
|
||||
assert.equal(file?.fileType, "mapping");
|
||||
assert.equal(warnings.some((warning) => warning.code === "invalid-table-column"), false);
|
||||
assert.deepEqual(file?.mappings, [
|
||||
{
|
||||
sourceRef: "[[DATA-SOURCE]].issue",
|
||||
targetRef: "[[DATA-TARGET]].status_id",
|
||||
transform: "copy",
|
||||
rule: "[[RULE-MAP]]",
|
||||
required: "Y",
|
||||
notes: "source first row"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("mapping diagnostics treat member references and mapping rows as duplicate keys", () => {
|
||||
const index = buildVaultIndex([
|
||||
{ path: "DATA-MAP-SOURCE.md", content: `---
|
||||
|
|
|
|||
|
|
@ -8,16 +8,18 @@ await build({
|
|||
stdin: {
|
||||
contents: [
|
||||
'export { parseFlowDiagramFile } from "./src/parsers/dfd-diagram-parser";',
|
||||
'export { FlowDiagramViewModeState, resolveInitialFlowDiagramViewMode } from "./src/core/flow-diagram-view-mode";',
|
||||
'export { DEFAULT_MODEL_WEAVE_SETTINGS, normalizeModelWeaveSettings } from "./src/settings/model-weave-settings";',
|
||||
'export { buildVaultIndex } from "./src/core/vault-index";',
|
||||
'export { detectFileType } from "./src/core/schema-detector";',
|
||||
'export { isDiagramPreviewRouteFileType, isDfdLikeDiagramPreviewFileType } from "./src/core/preview-routing";',
|
||||
'export { isModelWeavePreviewSupportedFileType } from "./src/core/supported-formats";',
|
||||
'export { resolveDiagramRelations } from "./src/core/relation-resolver";',
|
||||
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
|
||||
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, buildFlowDiagramScreenFlowProjection, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
|
||||
'export { getAppliedColorSchemeRowsForTargets } from "./src/core/color-scheme";',
|
||||
'export { buildCurrentDiagramDiagnostics } from "./src/core/current-file-diagnostics";',
|
||||
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
|
||||
'export { getAppliedColorSchemeLowerPaneSlot, getDiagnosticActionCandidates, getDiagnosticDetailEntries } from "./src/views/modeling-preview-view";',
|
||||
'export { getAppliedColorSchemeLowerPaneSlot, getDiagnosticActionCandidates, getDiagnosticDetailEntries, isFlowDiagramViewSelectorVisible } from "./src/views/modeling-preview-view";',
|
||||
'export { getExpectedHeaderForDiagnostic } from "./src/core/diagnostic-section-guidance";'
|
||||
].join("\n"),
|
||||
resolveDir: ".",
|
||||
|
|
@ -80,8 +82,13 @@ await build({
|
|||
|
||||
const {
|
||||
buildCurrentDiagramDiagnostics,
|
||||
FlowDiagramViewModeState,
|
||||
resolveInitialFlowDiagramViewMode,
|
||||
DEFAULT_MODEL_WEAVE_SETTINGS,
|
||||
normalizeModelWeaveSettings,
|
||||
buildDfdMermaidSource,
|
||||
buildFlowDiagramHoverMetadata,
|
||||
buildFlowDiagramScreenFlowProjection,
|
||||
createModelWeaveTranslator,
|
||||
getAppliedColorSchemeLowerPaneSlot,
|
||||
getAppliedColorSchemeRowsForTargets,
|
||||
|
|
@ -94,8 +101,9 @@ const {
|
|||
isDfdLikeDiagramPreviewFileType,
|
||||
isDiagramPreviewRouteFileType,
|
||||
isModelWeavePreviewSupportedFileType,
|
||||
isFlowDiagramViewSelectorVisible,
|
||||
parseFlowDiagramFile,
|
||||
resolveDiagramRelations
|
||||
resolveDiagramRelations,
|
||||
} = await import(`../${outputFile}?t=${Date.now()}`);
|
||||
|
||||
globalThis.activeDocument = {
|
||||
|
|
@ -162,6 +170,84 @@ test("flow_diagram is detected and parsed as a distinct model type", () => {
|
|||
});
|
||||
|
||||
|
||||
test("flow_diagram defaults flow_view to detail", () => {
|
||||
const { file } = parseFlow();
|
||||
|
||||
assert.equal(file.flowView, "detail");
|
||||
assert.equal(resolveInitialFlowDiagramViewMode(file), "detail");
|
||||
});
|
||||
|
||||
test("flow_diagram parses and resolves screen flow_view", () => {
|
||||
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen");
|
||||
const { file, warnings } = parseFlow(markdown);
|
||||
|
||||
assert.equal(file.flowView, "screen");
|
||||
assert.equal(resolveInitialFlowDiagramViewMode(file), "screen");
|
||||
assert.equal(warnings.some((warning) => warning.field === "flow_view"), false);
|
||||
});
|
||||
|
||||
test("flow_diagram unknown flow_view warns and falls back to detail", () => {
|
||||
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: storyboard");
|
||||
const { file, warnings } = parseFlow(markdown);
|
||||
|
||||
assert.equal(file.flowView, "detail");
|
||||
assert.equal(resolveInitialFlowDiagramViewMode(file), "detail");
|
||||
assert.equal(
|
||||
warnings.some((warning) => warning.field === "flow_view" && /unknown flow_view/.test(warning.message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("flow_diagram view state initializes once and keeps toolbar changes per file", () => {
|
||||
const screenMarkdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen");
|
||||
const noFrontmatter = parseFlow();
|
||||
const screen = parseFlow(screenMarkdown);
|
||||
const unknown = parseFlow(flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: storyboard"));
|
||||
|
||||
const detail = parseFlow(screenMarkdown.replace("flow_view: screen", "flow_view: detail"));
|
||||
const state = new FlowDiagramViewModeState();
|
||||
|
||||
assert.equal(screen.file.flowViewSpecified, true);
|
||||
assert.deepEqual(state.synchronize("A.md", screen.file, "detail"), {
|
||||
mode: "screen",
|
||||
initializationChanged: true
|
||||
});
|
||||
state.set("A.md", "detail");
|
||||
assert.deepEqual(state.synchronize("A.md", screen.file, "screen"), {
|
||||
mode: "detail",
|
||||
initializationChanged: false
|
||||
});
|
||||
assert.deepEqual(state.synchronize("A.md", detail.file, "screen"), {
|
||||
mode: "detail",
|
||||
initializationChanged: true
|
||||
});
|
||||
assert.deepEqual(state.synchronize("B.md", noFrontmatter.file, "screen"), {
|
||||
mode: "screen",
|
||||
initializationChanged: true
|
||||
});
|
||||
assert.deepEqual(state.synchronize("B.md", noFrontmatter.file, "detail"), {
|
||||
mode: "detail",
|
||||
initializationChanged: true
|
||||
});
|
||||
assert.deepEqual(state.synchronize("C.md", unknown.file, "screen"), {
|
||||
mode: "screen",
|
||||
initializationChanged: true
|
||||
});
|
||||
assert.equal(resolveInitialFlowDiagramViewMode(unknown.file, "invalid"), "detail");
|
||||
assert.equal(DEFAULT_MODEL_WEAVE_SETTINGS.defaultFlowDiagramViewMode, "detail");
|
||||
assert.equal(normalizeModelWeaveSettings({ defaultFlowDiagramViewMode: "screen" }).defaultFlowDiagramViewMode, "screen");
|
||||
assert.equal(normalizeModelWeaveSettings({ defaultFlowDiagramViewMode: "invalid" }).defaultFlowDiagramViewMode, "detail");
|
||||
assert.equal(unknown.warnings.some((warning) => warning.field === "flow_view"), true);
|
||||
});
|
||||
|
||||
test("flow_diagram selector appears only for Flow Diagram previews", () => {
|
||||
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen");
|
||||
const { file } = parseFlow(markdown);
|
||||
|
||||
assert.equal(resolveInitialFlowDiagramViewMode(file), "screen");
|
||||
assert.equal(isFlowDiagramViewSelectorVisible({ diagram: file }), true);
|
||||
assert.equal(isFlowDiagramViewSelectorVisible({ diagram: { schema: "dfd_diagram" } }), false);
|
||||
});
|
||||
test("flow_diagram follows the same preview support and diagram route checks", () => {
|
||||
const fileType = detectFileType({ type: "flow_diagram" });
|
||||
|
||||
|
|
@ -258,12 +344,130 @@ test("flow_diagram Mermaid edge labels use trigger, data, and kind", () => {
|
|||
assert.match(source, /mystery -->\|fallback \/ Unknown handoff\| order_screen/);
|
||||
});
|
||||
|
||||
const screenFlowMarkdown = `---
|
||||
type: flow_diagram
|
||||
id: FLOW-CHECKOUT-SCREEN-FLOW
|
||||
name: Checkout Screen Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
---
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| checkout_screen | Checkout Screen | screen | | frontend_area | Start screen |
|
||||
| submit_action | Submit Action | work_object | | frontend_area | User action detail |
|
||||
| checkout_process | Checkout Process | app_process | | application_area | Hidden submit process |
|
||||
| completion_screen | Completion Screen | screen | | frontend_area | Success screen |
|
||||
| error_message | Error Message | message | | frontend_area | User-visible error |
|
||||
| session_store | Session Store | session | | data_area | Hidden session store |
|
||||
| retry_process | Retry Process | process | | application_area | Alternate hidden route |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| SF01 | checkout_screen | submit_action | action | click:Submit | Checkout input | | Submit action |
|
||||
| SF02 | submit_action | checkout_process | command | | Checkout request | | Invoke process |
|
||||
| SF03 | checkout_process | completion_screen | navigate | showComplete | Completion payload | success | Show completion |
|
||||
| SF04 | checkout_process | error_message | message | showError | Error details | failed | Show error |
|
||||
| SF05 | checkout_process | session_store | store | save | Session data | | Hidden persistence |
|
||||
| SF06 | checkout_screen | retry_process | action | click:Retry | Retry input | | Retry action |
|
||||
| SF07 | retry_process | completion_screen | navigate | showComplete | Retry payload | alternate | Alternate success |
|
||||
`;
|
||||
function getFlowDiagramDataWarnings(markdown, extraFiles = []) {
|
||||
const { model, resolved } = resolveFlow(markdown, extraFiles);
|
||||
return buildCurrentDiagramDiagnostics({ diagram: model }, resolved.warnings)
|
||||
.filter((warning) => warning.code === "unresolved-reference" && /flow data reference/.test(warning.message));
|
||||
}
|
||||
|
||||
test("flow_diagram screen view projects internal paths into visible nodes", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
|
||||
assert.deepEqual(projected.nodes.map((node) => node.id), [
|
||||
"checkout_screen",
|
||||
"completion_screen",
|
||||
"error_message"
|
||||
]);
|
||||
assert.equal(projected.edges.length, 2);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "session_store"), false);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "completion_screen"), true);
|
||||
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "error_message"), true);
|
||||
});
|
||||
|
||||
test("flow_diagram screen view deduplicates projected edges and merges labels", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
const completionEdges = projected.edges.filter((edge) =>
|
||||
edge.source === "checkout_screen" && edge.target === "completion_screen"
|
||||
);
|
||||
|
||||
assert.equal(completionEdges.length, 1);
|
||||
assert.match(completionEdges[0].label ?? "", /^(success \/ alternate|alternate \/ success)$/);
|
||||
assert.equal(completionEdges[0].metadata.projected, true);
|
||||
});
|
||||
|
||||
test("flow_diagram screen flow Mermaid hides collapsed nodes and uses projected labels", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const source = buildDfdMermaidSource(resolved, undefined, "screen");
|
||||
|
||||
assert.match(source, /checkout_screen@\{ shape: curv-trap, label: "Checkout Screen" \}/);
|
||||
assert.match(source, /completion_screen@\{ shape: curv-trap, label: "Completion Screen" \}/);
|
||||
assert.match(source, /error_message@\{ shape: rect, label: "Error Message" \}/);
|
||||
assert.doesNotMatch(source, /submit_action@/);
|
||||
assert.doesNotMatch(source, /checkout_process@/);
|
||||
assert.doesNotMatch(source, /session_store@/);
|
||||
assert.match(source, /checkout_screen -->\|(success \/ alternate|alternate \/ success)\| completion_screen/);
|
||||
assert.match(source, /checkout_screen -->\|failed\| error_message/);
|
||||
assert.doesNotMatch(source, /Checkout input|Session data/);
|
||||
});
|
||||
test("flow_diagram effective mode drives Mermaid source in both directions", () => {
|
||||
const { resolved } = resolveFlow(screenFlowMarkdown);
|
||||
const detailSource = buildDfdMermaidSource(resolved, undefined, "detail");
|
||||
const screenSource = buildDfdMermaidSource(resolved, undefined, "screen");
|
||||
const detailAgain = buildDfdMermaidSource(resolved, undefined, "detail");
|
||||
|
||||
assert.match(detailSource, /submit_action@/);
|
||||
assert.match(detailSource, /session_store@/);
|
||||
assert.doesNotMatch(screenSource, /submit_action@/);
|
||||
assert.doesNotMatch(screenSource, /session_store@/);
|
||||
assert.equal(detailAgain, detailSource);
|
||||
});
|
||||
|
||||
test("flow_diagram screen view falls back to detail when no visible nodes exist", () => {
|
||||
const markdown = `---
|
||||
type: flow_diagram
|
||||
id: FLOW-INTERNAL-ONLY
|
||||
name: Internal Only Flow
|
||||
kind: screen_communication
|
||||
flow_view: screen
|
||||
---
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| process_a | Process A | app_process | | application | Internal process |
|
||||
| store_a | Store A | session | | data | Internal store |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | process_a | store_a | store | save | Session data | | Store state |
|
||||
`;
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const projected = buildFlowDiagramScreenFlowProjection(resolved);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
||||
assert.deepEqual(projected.nodes.map((node) => node.id), ["process_a", "store_a"]);
|
||||
assert.equal(projected.edges.length, 1);
|
||||
assert.match(source, /process_a@\{ shape: rect, label: "Process A" \}/);
|
||||
assert.match(source, /store_a@\{ shape: lin-cyl, label: "Store A" \}/);
|
||||
assert.match(source, /process_a -->\|save \/ Session data\| store_a/);
|
||||
});
|
||||
test("flow_diagram Flows.data plain text does not warn", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", "DATA-ORDER-DRAFT"));
|
||||
|
||||
|
|
@ -683,6 +887,174 @@ kind: screen_communication
|
|||
| F06 | ORDER_ENTRY | ORDER_SUBMIT | submit | click:Submit | [[DATA-ORDER-DRAFT]] | valid | Submit |
|
||||
`;
|
||||
|
||||
|
||||
const flowDomainSourceMarkdown = `---
|
||||
type: domains
|
||||
id: DOMAINS-FLOW-TEST
|
||||
name: Flow Test Domains
|
||||
---
|
||||
|
||||
## Domains
|
||||
|
||||
| id | name | kind | parent | description |
|
||||
|---|---|---|---|---|
|
||||
| checkout_system | Checkout System | system | | Overall checkout boundary |
|
||||
| frontend_area | Frontend Area | ui | checkout_system | Browser and screen side |
|
||||
| application_area | Application Area | application | checkout_system | Application handling |
|
||||
| data_area | Data Area | data | checkout_system | Data boundary |
|
||||
`;
|
||||
|
||||
const flowDomainSourceDiagramMarkdown = `---
|
||||
type: flow_diagram
|
||||
id: FLOW-ORDER-SCREEN-COMMUNICATION
|
||||
name: Order Screen Communication Flow
|
||||
kind: screen_communication
|
||||
---
|
||||
|
||||
## Domain Sources
|
||||
|
||||
| ref | notes |
|
||||
|---|---|
|
||||
| [[DOMAINS-FLOW-TEST]] | test domains |
|
||||
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | domain | notes |
|
||||
|---|---|---|---|---|---|
|
||||
| ORDER_ENTRY | Order Entry Screen | screen | | frontend_area | Parent screen |
|
||||
| ORDER_SUBMIT | Order Submit Process | app_process | | application_area | Submit process |
|
||||
| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | kind | trigger | data | condition | notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| F01 | ORDER_ENTRY | ORDER_SUBMIT | submit | click:Submit | Order draft | valid | Submit |
|
||||
| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |
|
||||
`;
|
||||
|
||||
test("flow_diagram parses Domain Sources and local Domains sections", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown.replace(
|
||||
"## Objects",
|
||||
"## Domains\n\n| id | name | kind | parent | description |\n|---|---|---|---|---|\n| local_area | Local Area | local | checkout_system | Local override area |\n\n## Objects"
|
||||
);
|
||||
const { file, warnings } = parseFlow(markdown);
|
||||
|
||||
assert.equal(warnings.some((warning) => warning.code === "invalid-table-column"), false);
|
||||
assert.equal(file.domainSources.length, 1);
|
||||
assert.equal(file.domainSources[0].ref, "[[DOMAINS-FLOW-TEST]]");
|
||||
assert.equal(file.domains?.length, 1);
|
||||
assert.equal(file.domains?.[0].id, "local_area");
|
||||
});
|
||||
|
||||
test("flow_diagram Domain Sources resolve Domain name, kind, and parent for nested groups", () => {
|
||||
const { resolved } = resolveFlow(flowDomainSourceDiagramMarkdown, [
|
||||
{ path: "DOMAINS-FLOW-TEST.md", content: flowDomainSourceMarkdown }
|
||||
]);
|
||||
const source = buildDfdMermaidSource(resolved, {
|
||||
id: "COLOR-FLOW-DOMAIN-TEST",
|
||||
name: "Flow Domain Test Colors",
|
||||
entries: [
|
||||
{ target: "domain", kind: "system", fill: "#f7f7f7", stroke: "#999999", text: "#222222", rowIndex: 0 },
|
||||
{ target: "domain", kind: "ui", fill: "#e8f1ff", stroke: "#3b73d9", text: "#102a5c", rowIndex: 1 },
|
||||
{ target: "domain", kind: "application", fill: "#e9fbe9", stroke: "#2f8f46", text: "#123d1b", rowIndex: 2 },
|
||||
{ target: "domain", kind: "data", fill: "#f3e8ff", stroke: "#7c3bbd", text: "#2d124c", rowIndex: 3 }
|
||||
],
|
||||
defaultStyle: { fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111" }
|
||||
});
|
||||
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system\["Checkout System"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area\["Frontend Area"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_frontend_area[\s\S]*ORDER_ENTRY/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_application_area[\s\S]*ORDER_SUBMIT/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_data_area[\s\S]*SESSION_STORE/);
|
||||
assert.match(source, /style DOMAIN_frontend_area fill:#e8f1ff,stroke:#3b73d9,color:#102a5c/);
|
||||
assert.match(source, /style DOMAIN_application_area fill:#e9fbe9,stroke:#2f8f46,color:#123d1b/);
|
||||
});
|
||||
|
||||
test("flow_diagram screen view preserves resolved Domain Sources for visible screens", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown
|
||||
.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen")
|
||||
.replace(
|
||||
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |",
|
||||
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |\n| CONFIRM_SCREEN | Confirmation Screen | screen | | frontend_area | Completion screen |"
|
||||
)
|
||||
.replace(
|
||||
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |",
|
||||
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |\n| F03 | ORDER_SUBMIT | CONFIRM_SCREEN | navigate | showComplete | Confirmation data | success | Show completion |"
|
||||
);
|
||||
const { resolved } = resolveFlow(markdown, [
|
||||
{ path: "DOMAINS-FLOW-TEST.md", content: flowDomainSourceMarkdown }
|
||||
]);
|
||||
const source = buildDfdMermaidSource(resolved, {
|
||||
id: "COLOR-FLOW-DOMAIN-TEST",
|
||||
name: "Flow Domain Test Colors",
|
||||
entries: [
|
||||
{ target: "domain", kind: "system", fill: "#f7f7f7", stroke: "#999999", text: "#222222", rowIndex: 0 },
|
||||
{ target: "domain", kind: "ui", fill: "#e8f1ff", stroke: "#3b73d9", text: "#102a5c", rowIndex: 1 },
|
||||
{ target: "domain", kind: "application", fill: "#e9fbe9", stroke: "#2f8f46", text: "#123d1b", rowIndex: 2 },
|
||||
{ target: "domain", kind: "data", fill: "#f3e8ff", stroke: "#7c3bbd", text: "#2d124c", rowIndex: 3 }
|
||||
],
|
||||
defaultStyle: { fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111" }
|
||||
}, "screen");
|
||||
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system\["Checkout System"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area\["Frontend Area"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_frontend_area[\s\S]*ORDER_ENTRY/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area[\s\S]*CONFIRM_SCREEN/);
|
||||
assert.doesNotMatch(source, /ORDER_SUBMIT@/);
|
||||
assert.doesNotMatch(source, /SESSION_STORE@/);
|
||||
assert.match(source, /ORDER_ENTRY -->\|success\| CONFIRM_SCREEN/);
|
||||
assert.match(source, /style DOMAIN_frontend_area fill:#e8f1ff,stroke:#3b73d9,color:#102a5c/);
|
||||
});
|
||||
test("flow_diagram local Domains resolve names, kinds, and nested parents", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown
|
||||
.replace("## Domain Sources\n\n| ref | notes |\n|---|---|\n| [[DOMAINS-FLOW-TEST]] | test domains |\n\n", "")
|
||||
.replace(
|
||||
"## Objects",
|
||||
"## Domains\n\n| id | name | kind | parent | description |\n|---|---|---|---|---|\n| checkout_system | Checkout System | system | | Overall checkout boundary |\n| frontend_area | Frontend Area | ui | checkout_system | Browser and screen side |\n| application_area | Application Area | application | checkout_system | Application handling |\n| data_area | Data Area | data | checkout_system | Data boundary |\n\n## Objects"
|
||||
);
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system\["Checkout System"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_frontend_area\["Frontend Area"\]/);
|
||||
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_data_area[\s\S]*SESSION_STORE/);
|
||||
});
|
||||
|
||||
test("flow_diagram without Domain Sources or local Domains keeps raw synthetic domain groups without warnings", () => {
|
||||
const { model, resolved } = resolveFlow(flowDomainMarkdown);
|
||||
const diagnostics = buildCurrentDiagramDiagnostics({ diagram: model }, [
|
||||
...(resolved.warnings ?? [])
|
||||
]);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
||||
assert.match(source, /subgraph DOMAIN_sales\["sales"\]/);
|
||||
assert.equal(
|
||||
diagnostics.some((warning) => warning.field === "Objects.domain" && /unknown Domain/.test(warning.message)),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("flow_diagram with Domain Sources warns for unknown Objects.domain", () => {
|
||||
const markdown = flowDomainSourceDiagramMarkdown.replaceAll("frontend_area", "missing_area");
|
||||
const { model, resolved } = resolveFlow(markdown, [
|
||||
{ path: "DOMAINS-FLOW-TEST.md", content: flowDomainSourceMarkdown }
|
||||
]);
|
||||
const diagnostics = buildCurrentDiagramDiagnostics({ diagram: model }, [
|
||||
...(resolved.warnings ?? [])
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
diagnostics.some((warning) =>
|
||||
warning.code === "unresolved-reference" &&
|
||||
warning.field === "Objects.domain" &&
|
||||
/Flow Diagram object "ORDER_ENTRY" references unknown Domain "missing_area"/.test(warning.message)
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("flow_diagram Objects.domain renders Mermaid domain subgraphs", () => {
|
||||
const { resolved } = resolveFlow(flowDomainMarkdown);
|
||||
const source = buildDfdMermaidSource(resolved);
|
||||
|
|
@ -728,7 +1100,9 @@ const flowDomainColorScheme = {
|
|||
name: "Flow Domain Colors",
|
||||
entries: [
|
||||
{ target: "domain", kind: "sales", fill: "#7ddea7", stroke: "#000000", text: "#ffffff", rowIndex: 0 },
|
||||
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 }
|
||||
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 },
|
||||
{ target: "flow_diagram", kind: "screen", fill: "#fff4cc", stroke: "#bf9000", text: "#111111", rowIndex: 2 },
|
||||
{ target: "flow_diagram", kind: "app_process", fill: "#e2f0d9", stroke: "#70ad47", text: "#111111", rowIndex: 3 }
|
||||
],
|
||||
defaultStyle: {
|
||||
fill: "#f5f5f5",
|
||||
|
|
@ -754,7 +1128,8 @@ test("flow_diagram Applied Color Scheme targets include used domain rows", () =>
|
|||
const targets = getDfdMermaidColorSchemeTargets(resolved);
|
||||
const rows = getAppliedColorSchemeRowsForTargets(flowDomainColorScheme, targets);
|
||||
|
||||
assert.deepEqual(targets, ["domain"]);
|
||||
assert.deepEqual(targets, ["flow_diagram", "domain"]);
|
||||
assert.equal(rows.some((row) => row.entry.target === "flow_diagram" && row.entry.kind === "screen"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "sales"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "application"), true);
|
||||
assert.equal(rows.some((row) => row.entry.target === "app_process"), false);
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@
|
|||
"0.1.16": "1.8.7",
|
||||
"0.1.17": "1.8.7",
|
||||
"0.1.18": "1.8.7",
|
||||
"0.1.19": "1.8.7"
|
||||
"0.1.19": "1.8.7",
|
||||
"0.1.20": "1.8.7"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue