@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -20
- package/dist/canvas.js +7 -4
- package/dist/context-menu.js +2 -2
- package/dist/controller.js +84 -6
- package/dist/index.d.ts +138 -38
- package/dist/index.js +12 -11
- package/dist/interactions.js +1 -1
- package/dist/modules/bpmn-model/index.d.ts +11 -0
- package/dist/modules/bpmn-model/index.js +703 -0
- package/dist/modules/core/containment.js +590 -0
- package/dist/modules/core/gateway.js +72 -0
- package/dist/modules/core/history.js +32 -0
- package/dist/modules/core/index.d.ts +268 -0
- package/dist/modules/core/index.js +7 -0
- package/dist/modules/core/layout.js +644 -0
- package/dist/modules/core/model.js +287 -0
- package/dist/modules/core/runtime-transition-route.js +360 -0
- package/dist/modules/core/scope.js +99 -0
- package/dist/modules/designer/index.d.ts +79 -0
- package/dist/modules/designer/index.js +607 -0
- package/dist/modules/engine-activiti/index.d.ts +19 -0
- package/dist/modules/engine-activiti/index.js +160 -0
- package/dist/modules/engine-flowable/index.d.ts +19 -0
- package/dist/modules/engine-flowable/index.js +160 -0
- package/dist/modules/export-svg/index.d.ts +112 -0
- package/dist/modules/export-svg/index.js +2 -0
- package/dist/modules/export-svg/preview.js +327 -0
- package/dist/modules/export-svg/render.js +718 -0
- package/dist/modules/icons/index.d.ts +24 -0
- package/dist/modules/icons/index.js +264 -0
- package/dist/modules/palette/index.d.ts +74 -0
- package/dist/modules/palette/index.js +99 -0
- package/dist/modules/palette/panel.js +99 -0
- package/dist/modules/properties/index.d.ts +20 -0
- package/dist/modules/properties/index.js +19 -0
- package/dist/modules/properties-activiti/index.d.ts +3 -0
- package/dist/modules/properties-activiti/index.js +97 -0
- package/dist/modules/properties-bpmn/index.d.ts +3 -0
- package/dist/modules/properties-bpmn/index.js +518 -0
- package/dist/modules/properties-core/index.d.ts +124 -0
- package/dist/modules/properties-core/index.js +312 -0
- package/dist/modules/properties-flowable/index.d.ts +3 -0
- package/dist/modules/properties-flowable/index.js +114 -0
- package/dist/modules/properties-renderer/index.d.ts +25 -0
- package/dist/modules/properties-renderer/index.js +491 -0
- package/dist/modules/renderer-svg/index.d.ts +118 -0
- package/dist/modules/renderer-svg/index.js +1460 -0
- package/dist/modules/runtime/index.d.ts +169 -0
- package/dist/modules/runtime/index.js +535 -0
- package/dist/modules/theme/index.d.ts +95 -0
- package/dist/modules/theme/index.js +368 -0
- package/dist/modules/viewer/index.d.ts +265 -0
- package/dist/modules/viewer/index.js +1011 -0
- package/dist/modules/viewer/runtime-content.js +123 -0
- package/dist/modules/viewer/runtime-details-motion.js +228 -0
- package/dist/modules/viewer/runtime-trace.js +574 -0
- package/dist/modules/viewer/timeline.js +276 -0
- package/dist/selection-layout.js +1 -1
- package/dist/shell.js +210 -26
- package/dist/styles.css +116 -7
- package/llms-full.txt +3082 -0
- package/llms.txt +225 -0
- package/package.json +39 -16
package/README.md
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
# @bpmn-nova/studio
|
|
2
2
|
|
|
3
|
-
BPMN Nova
|
|
3
|
+
BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计、只读展示、审批轨迹、主题、属性面板和纯 SVG 导出。
|
|
4
4
|
|
|
5
|
-
> **English summary:** The
|
|
5
|
+
> **English summary:** The complete framework-neutral BPMN Nova package for process design, viewing, approval traces, themes, properties, and pure SVG export.
|
|
6
6
|
|
|
7
|
-
> 当前版本为 `0.3.
|
|
7
|
+
> 当前版本为 `0.3.2-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 什么时候使用
|
|
12
|
+
|
|
13
|
+
- Vanilla JavaScript 或 TypeScript 项目安装本包。
|
|
14
|
+
- React 18+ 项目安装 `@bpmn-nova/react`。
|
|
15
|
+
- Vue 3.3+ 项目安装 `@bpmn-nova/vue`。
|
|
16
|
+
- 一个项目只需要选择一个公开包,不需要安装 Core、Viewer、Runtime、Theme 等内部 Module。
|
|
8
17
|
|
|
9
18
|
## 安装
|
|
10
19
|
|
|
11
20
|
```bash
|
|
12
|
-
npm install @bpmn-nova/studio
|
|
21
|
+
npm install @bpmn-nova/studio@preview
|
|
13
22
|
```
|
|
14
23
|
|
|
15
24
|
```js
|
|
16
25
|
import '@bpmn-nova/studio/styles.css'
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
JavaScript
|
|
28
|
+
BPMN Nova 不会在 JavaScript 中隐式注入 CSS。视觉容器必须有明确高度。
|
|
29
|
+
|
|
30
|
+
使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
|
|
20
31
|
|
|
21
|
-
##
|
|
32
|
+
## 流程设计
|
|
22
33
|
|
|
23
|
-
```
|
|
34
|
+
```html
|
|
35
|
+
<div id="studio" style="height: 720px"></div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```js
|
|
24
39
|
import {
|
|
25
40
|
createEmptyProcess,
|
|
26
41
|
createStudioController,
|
|
@@ -29,31 +44,241 @@ import {
|
|
|
29
44
|
import '@bpmn-nova/studio/styles.css'
|
|
30
45
|
|
|
31
46
|
const model = createEmptyProcess('flowable')
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
model.id = 'Process_PurchaseApproval'
|
|
48
|
+
model.name = '采购申请审批流程'
|
|
49
|
+
|
|
50
|
+
const studio = createStudioController({
|
|
51
|
+
model,
|
|
52
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
53
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
54
|
+
})
|
|
55
|
+
const shell = createStudioShell({
|
|
56
|
+
container: document.querySelector('#studio'),
|
|
57
|
+
studio,
|
|
58
|
+
mode: 'design',
|
|
59
|
+
allowedModes: ['design'],
|
|
60
|
+
theme: 'auto',
|
|
61
|
+
})
|
|
34
62
|
|
|
35
63
|
const xml = studio.exportXml()
|
|
64
|
+
|
|
65
|
+
// 页面卸载时释放实例。
|
|
36
66
|
shell.destroy()
|
|
37
67
|
studio.destroy()
|
|
38
68
|
```
|
|
39
69
|
|
|
40
|
-
|
|
70
|
+
`allowedNodeTypes` 与 `allowedEdgeTypes` 同时约束 XML 导入、Palette、连接、快捷新增、模板和节点类型转换;不传时开放 Nova 当前支持的全部图元。`allowedModes` 控制默认工作台显示和允许切换的模式。不传 Runtime 时实例模式保持空运行事实,不会注入演示审批数据。
|
|
71
|
+
|
|
72
|
+
## 嵌入宿主工作台
|
|
73
|
+
|
|
74
|
+
默认 `BpmnStudioShell` 仍是包含 Header、Palette、Canvas、Properties 和 Statusbar 的完整工作台。宿主要复用自己的业务属性面板时,应显式隐藏 Nova 右侧区域,并把业务面板作为 Nova 根节点的外部兄弟区域:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const shell = createStudioShell({
|
|
78
|
+
container: novaHost,
|
|
79
|
+
studio,
|
|
80
|
+
mode: 'design',
|
|
81
|
+
allowedModes: ['design'],
|
|
82
|
+
regions: { right: 'hidden' },
|
|
83
|
+
slots: {
|
|
84
|
+
headerStart({ container, actions, getState, subscribe }) {
|
|
85
|
+
// 在 Nova Header 左侧挂载宿主的返回入口、业务图标、流程名称和类型。
|
|
86
|
+
// 完整替换 Header 时改用 slots.header,并继续调用 actions。
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
theme: 'auto',
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
shell.setRegions({ right: 'default' })
|
|
93
|
+
shell.actions.undo()
|
|
94
|
+
shell.actions.redo()
|
|
95
|
+
shell.actions.fitView()
|
|
96
|
+
const issues = shell.actions.validate()
|
|
97
|
+
const xml = shell.actions.exportXml('flowable')
|
|
98
|
+
```
|
|
41
99
|
|
|
42
|
-
|
|
43
|
-
- `createStudioShell / BpmnStudioShell`
|
|
44
|
-
- `BpmnCanvas 与 InteractionController`
|
|
45
|
-
- `ContextMenu、Template、Palette、Properties Registries`
|
|
46
|
-
- `Studio 聚合导出的 Core、XML、Runtime、Theme、Designer 与 Viewer API`
|
|
100
|
+
`regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
|
|
47
101
|
|
|
48
|
-
|
|
102
|
+
`shell.actions` 只封装撤销、重做、布局、视图、校验和 BPMN/SVG 导入导出。宿主的保存草稿、发布、权限、文件选择和服务端事务不属于 Nova Actions。
|
|
103
|
+
|
|
104
|
+
## 只读 Viewer
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { importBpmn } from '@bpmn-nova/studio'
|
|
108
|
+
import { BpmnViewer } from '@bpmn-nova/studio/viewer'
|
|
109
|
+
import '@bpmn-nova/studio/styles.css'
|
|
110
|
+
|
|
111
|
+
const viewer = new BpmnViewer({
|
|
112
|
+
container: document.querySelector('#viewer'),
|
|
113
|
+
model: importBpmn(xml, 'flowable'),
|
|
114
|
+
projection: 'standard',
|
|
115
|
+
theme: 'auto',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
viewer.fitView()
|
|
119
|
+
viewer.destroy()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
也可以从 `@bpmn-nova/studio` 根入口导入所有公开 Interface;子路径用于明确表达接入场景。
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
## 审批轨迹
|
|
127
|
+
|
|
128
|
+
Viewer 接收引擎中立的 Runtime Snapshot。三种投影使用相同数据:
|
|
129
|
+
|
|
130
|
+
| 投影 | 配置 | 适用场景 |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| 实际路径 | `projection: 'approval'` | 只展示实际发生的有效路径 |
|
|
133
|
+
| 移动时间线 | `projection: 'compact'` | 按访问轮次展示动作、图片和附件 |
|
|
134
|
+
| 完整 BPMN | `projection: 'standard'` + `runtime` | 完整结构叠加运行状态 |
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const runtime = {
|
|
138
|
+
processInstanceId: 'purchase-20260824',
|
|
139
|
+
status: 'running',
|
|
140
|
+
activities: [{
|
|
141
|
+
id: 'activity-manager-1',
|
|
142
|
+
elementId: 'UserTask_Manager',
|
|
143
|
+
visitId: 'visit-manager-1',
|
|
144
|
+
status: 'completed',
|
|
145
|
+
assignee: '李经理',
|
|
146
|
+
endTime: '2026-08-24T10:12:00+08:00',
|
|
147
|
+
}],
|
|
148
|
+
actions: [{
|
|
149
|
+
id: 'action-manager-approve',
|
|
150
|
+
type: 'approve',
|
|
151
|
+
elementId: 'UserTask_Manager',
|
|
152
|
+
visitId: 'visit-manager-1',
|
|
153
|
+
activityId: 'activity-manager-1',
|
|
154
|
+
actor: { id: 'manager-li', name: '李经理' },
|
|
155
|
+
occurredAt: '2026-08-24T10:12:00+08:00',
|
|
156
|
+
content: {
|
|
157
|
+
plainText: '资料完整,同意提交总经理审批。',
|
|
158
|
+
blocks: [
|
|
159
|
+
{ type: 'paragraph', text: '资料完整,同意提交总经理审批。' },
|
|
160
|
+
{ type: 'image', assetId: 'quotation-preview' },
|
|
161
|
+
{ type: 'file', assetId: 'purchase-checklist' },
|
|
162
|
+
],
|
|
163
|
+
assets: [
|
|
164
|
+
{
|
|
165
|
+
id: 'quotation-preview',
|
|
166
|
+
name: '现场报价单.png',
|
|
167
|
+
mediaType: 'image/png',
|
|
168
|
+
width: 1280,
|
|
169
|
+
height: 720,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'purchase-checklist',
|
|
173
|
+
name: '采购核验清单.txt',
|
|
174
|
+
mediaType: 'text/plain',
|
|
175
|
+
size: 248,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
}],
|
|
180
|
+
visitedEdges: [],
|
|
181
|
+
}
|
|
49
182
|
|
|
50
|
-
|
|
183
|
+
const runtimeAssetResolver = async (asset, { purpose, signal }) => {
|
|
184
|
+
const response = await fetch(
|
|
185
|
+
`/api/runtime-assets/${encodeURIComponent(asset.id)}?purpose=${purpose}`,
|
|
186
|
+
{ signal },
|
|
187
|
+
)
|
|
188
|
+
if (!response.ok) return null
|
|
189
|
+
return response.url
|
|
190
|
+
}
|
|
51
191
|
|
|
192
|
+
viewer.setRuntime(runtime)
|
|
193
|
+
viewer.setProjection('compact')
|
|
194
|
+
viewer.setDisplayOptions({ runtimeAssetResolver })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Snapshot 只保存资源引用,不保存 URL、Base64、Blob 或 HTML。上传、权限签发和资源存储由宿主业务系统负责。
|
|
198
|
+
|
|
199
|
+
| 实际路径 | 移动时间线 |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
|  |  |
|
|
202
|
+
|
|
203
|
+

|
|
204
|
+
|
|
205
|
+
## 主题与运行态配色
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
shell.setTheme({
|
|
209
|
+
mode: 'auto',
|
|
210
|
+
dark: {
|
|
211
|
+
colors: {
|
|
212
|
+
canvas: '#0b1018',
|
|
213
|
+
surface: '#18202b',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
shell.setRuntimeAppearance({
|
|
219
|
+
statuses: { completed: 'success', active: 'primary' },
|
|
220
|
+
actions: {
|
|
221
|
+
approve: { label: '通过', tone: 'success' },
|
|
222
|
+
reject: { label: '驳回', tone: 'danger' },
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
主题限定在实例根节点;`auto` 会跟随并监听浏览器系统主题变化,不修改宿主页面的 `<html>`。
|
|
228
|
+
|
|
229
|
+

|
|
230
|
+
|
|
231
|
+
## SVG 导出
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
// 打开确认预览,用户确认后下载。
|
|
235
|
+
shell.openSvgExportPreview({
|
|
236
|
+
theme: 'current',
|
|
237
|
+
transparentBackground: false,
|
|
238
|
+
filename: '采购申请审批流程.svg',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// 或只创建 Artifact,不触发下载。
|
|
242
|
+
const artifact = await shell.exportSvg({ theme: 'dark' })
|
|
243
|
+
console.log(artifact.svg, artifact.warnings)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
导出完整业务内容,不受当前缩放、平移或滚动影响。图片会通过 Resolver 嵌入 SVG,普通附件只展示摘要。
|
|
247
|
+
|
|
248
|
+

|
|
249
|
+
|
|
250
|
+
## 支持的子路径
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import { BpmnDesigner } from '@bpmn-nova/studio/designer'
|
|
254
|
+
import { BpmnViewer } from '@bpmn-nova/studio/viewer'
|
|
255
|
+
import { createRuntimePresentation } from '@bpmn-nova/studio/runtime'
|
|
256
|
+
import { createThemeController } from '@bpmn-nova/studio/theme'
|
|
257
|
+
import { exportDiagramSvg } from '@bpmn-nova/studio/export-svg'
|
|
258
|
+
import { flowableProfile } from '@bpmn-nova/studio/flowable'
|
|
259
|
+
import { activitiProfile } from '@bpmn-nova/studio/activiti'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## 常见问题
|
|
263
|
+
|
|
264
|
+
- **界面为空或高度为 0:** 为 Studio/Viewer 的父容器设置明确高度。
|
|
265
|
+
- **界面没有样式:** 导入一次 `@bpmn-nova/studio/styles.css`。
|
|
266
|
+
- **附件不可用:** 提供 `runtimeAssetResolver`,并为不同 purpose 返回当前有效地址。
|
|
267
|
+
- **SSR 报错:** 在浏览器挂载阶段创建实例,服务端只准备 XML 和 Runtime 数据。
|
|
268
|
+
- **重复安装:** 不直接安装任何未列出的 `@bpmn-nova/*` 内部 Module。
|
|
269
|
+
|
|
270
|
+
## 文档与 AI
|
|
271
|
+
|
|
272
|
+
- npm 包内 `llms.txt`:可执行安装入口
|
|
273
|
+
- npm 包内 `llms-full.txt`:完整 AI 上下文
|
|
274
|
+
- [项目首页](https://github.com/daxiangme/bpmn-nova)
|
|
52
275
|
- [快速开始](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/GETTING-STARTED.md)
|
|
53
|
-
- [
|
|
54
|
-
- [API](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
|
|
276
|
+
- [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
|
|
55
277
|
- [自定义指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md)
|
|
56
|
-
|
|
278
|
+
|
|
279
|
+
## 能力边界
|
|
280
|
+
|
|
281
|
+
BPMN Nova 负责建模、展示、审批轨迹、主题和 SVG 导出;不负责流程引擎执行、审批提交、附件上传存储、权限签发或 PNG/PDF 导出。当前 Preview 不声明 BPMN Schema 100% 覆盖。
|
|
57
282
|
|
|
58
283
|
## License
|
|
59
284
|
|
package/dist/canvas.js
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
NODE_DEFINITIONS,
|
|
9
9
|
resolveContainment,
|
|
10
10
|
snapNodePosition,
|
|
11
|
-
} from '
|
|
12
|
-
import { DiagramRenderer } from '
|
|
13
|
-
import { hydrateIcons } from '
|
|
11
|
+
} from './modules/core/index.js';
|
|
12
|
+
import { DiagramRenderer } from './modules/renderer-svg/index.js';
|
|
13
|
+
import { hydrateIcons } from './modules/icons/index.js';
|
|
14
14
|
import { canCreateSequenceFlow } from './controller.js';
|
|
15
15
|
import { createDefaultContextMenuRegistry } from './context-menu.js';
|
|
16
16
|
import { CREATE_INTENT_MIME } from './interactions.js';
|
|
@@ -80,6 +80,7 @@ export class BpmnCanvas {
|
|
|
80
80
|
},
|
|
81
81
|
onStartConnect: (node) => studio.startConnect(node.id),
|
|
82
82
|
canConnect: (source, target) => canCreateSequenceFlow(studio.model, source.id, target.id),
|
|
83
|
+
canCreateNodeType: (nodeType) => studio.allowsNodeType(nodeType),
|
|
83
84
|
onQuickAdd: (source, type, preset) => studio.commands.quickAdd(source.id, type, preset),
|
|
84
85
|
onDeleteNode: (node) => studio.commands.remove({ kind: 'node', id: node.id }),
|
|
85
86
|
onUngroup: (node) => studio.commands.ungroup(node.id),
|
|
@@ -521,7 +522,7 @@ export class BpmnCanvas {
|
|
|
521
522
|
{ id: 'distribute-vertical', iconId: 'ui.distributeVertical', title: '垂直等距', action: () => actions.distribute('vertical'), disabled: nodes.length < 3 },
|
|
522
523
|
];
|
|
523
524
|
this._selectionToolbar.append(
|
|
524
|
-
button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
|
|
525
|
+
button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: this.studio.allowsNodeType?.('group') === false || !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
|
|
525
526
|
separator(),
|
|
526
527
|
menu('align', 'ui.alignCenterHorizontal', '对齐选区', alignmentItems, nodes.length < 2),
|
|
527
528
|
menu('distribute', 'ui.distributeHorizontal', '等距分布', distributeItems, nodes.length < 3),
|
|
@@ -904,6 +905,8 @@ export class BpmnCanvas {
|
|
|
904
905
|
startConnect(nodeId) { this.renderer.closeTransientOverlays?.(); return this.studio.startConnect(nodeId); }
|
|
905
906
|
editEdgeName(edgeId) { return this.renderer.editEdgeName?.(edgeId) || false; }
|
|
906
907
|
fitView(padding, options) { this.renderer.fitView(padding, { minZoom: 0.7, ...(options || {}) }); }
|
|
908
|
+
exportSvg(options) { return this.renderer.exportSvg(options); }
|
|
909
|
+
openSvgExportPreview(options) { return this.renderer.openSvgExportPreview(options); }
|
|
907
910
|
fitSelection(padding = 72) {
|
|
908
911
|
const bounds = this.studio.getSelectionBounds();
|
|
909
912
|
if (!bounds) return false;
|
package/dist/context-menu.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getEdge,
|
|
4
4
|
getNode,
|
|
5
5
|
isEmbeddedSubProcess,
|
|
6
|
-
} from '
|
|
6
|
+
} from './modules/core/index.js';
|
|
7
7
|
import { selectedDiagramNodes, selectedGroupableNodes, selectedLayoutNodes } from './selection-layout.js';
|
|
8
8
|
|
|
9
9
|
function insert(actions, action) {
|
|
@@ -104,7 +104,7 @@ function multiActions(context) {
|
|
|
104
104
|
execute: ({ studio }) => studio.arrangeSelection({ type: 'align', alignment }),
|
|
105
105
|
}));
|
|
106
106
|
actions.push(
|
|
107
|
-
action({ id: 'selection.group', label: '创建分组', iconId: 'ui.layoutSelection', group: 'layout', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length, execute: ({ studio }) => studio.commands.groupSelection() }),
|
|
107
|
+
action({ id: 'selection.group', label: '创建分组', iconId: 'ui.layoutSelection', group: 'layout', disabled: context.studio.allowsNodeType?.('group') === false || !groupableNodes.length || groupableNodes.length !== diagramNodes.length, execute: ({ studio }) => studio.commands.groupSelection() }),
|
|
108
108
|
action({ id: 'selection.distribute.horizontal', label: '水平等距', iconId: 'ui.distributeHorizontal', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'horizontal' }) }),
|
|
109
109
|
action({ id: 'selection.distribute.vertical', label: '垂直等距', iconId: 'ui.distributeVertical', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'vertical' }) }),
|
|
110
110
|
action({ id: 'selection.layout', label: '美化选区', iconId: 'ui.layoutSelection', group: 'layout', disabled: nodes.length < 2, execute: ({ studio }) => studio.arrangeSelection({ type: 'layout' }) }),
|
package/dist/controller.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyContainmentOperation,
|
|
3
3
|
CONTAINMENT_LIMITS,
|
|
4
|
+
EDGE_DEFINITIONS,
|
|
4
5
|
HistoryStack,
|
|
5
6
|
LAYOUT_DENSITIES,
|
|
6
7
|
NODE_DEFINITIONS,
|
|
@@ -24,8 +25,8 @@ import {
|
|
|
24
25
|
resolveSwimlaneLabelPlacement,
|
|
25
26
|
scopeExists,
|
|
26
27
|
validateParallelGateway,
|
|
27
|
-
} from '
|
|
28
|
-
import { exportBpmn, importBpmn } from '
|
|
28
|
+
} from './modules/core/index.js';
|
|
29
|
+
import { exportBpmn, importBpmn } from './modules/bpmn-model/index.js';
|
|
29
30
|
import {
|
|
30
31
|
applySelectionArrangement,
|
|
31
32
|
mergeSelectionRefs,
|
|
@@ -58,9 +59,38 @@ function mergeNodePreset(preset = {}) {
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function normalizeAllowedNodeTypes(allowedNodeTypes) {
|
|
63
|
+
if (allowedNodeTypes === undefined || allowedNodeTypes === null) return null;
|
|
64
|
+
if (typeof allowedNodeTypes === 'string' || typeof allowedNodeTypes[Symbol.iterator] !== 'function') {
|
|
65
|
+
throw new Error('allowedNodeTypes must be an iterable of BPMN node types.');
|
|
66
|
+
}
|
|
67
|
+
const normalized = new Set(allowedNodeTypes);
|
|
68
|
+
for (const nodeType of normalized) {
|
|
69
|
+
if (!NODE_DEFINITIONS[nodeType] || nodeType === 'generic') {
|
|
70
|
+
throw new Error(`Unknown allowed BPMN node type: ${nodeType}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeAllowedEdgeTypes(allowedEdgeTypes) {
|
|
77
|
+
if (allowedEdgeTypes === undefined || allowedEdgeTypes === null) return null;
|
|
78
|
+
if (typeof allowedEdgeTypes === 'string' || typeof allowedEdgeTypes[Symbol.iterator] !== 'function') {
|
|
79
|
+
throw new Error('allowedEdgeTypes must be an iterable of BPMN edge types.');
|
|
80
|
+
}
|
|
81
|
+
const normalized = new Set(allowedEdgeTypes);
|
|
82
|
+
for (const edgeType of normalized) {
|
|
83
|
+
if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown allowed BPMN edge type: ${edgeType}`);
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
export class BpmnStudioController {
|
|
62
|
-
constructor({ model, historyLimit = 80, extensions = [], propertiesProfile = 'business' } = {}) {
|
|
89
|
+
constructor({ model, historyLimit = 80, extensions = [], propertiesProfile = 'business', allowedNodeTypes = null, allowedEdgeTypes = null } = {}) {
|
|
63
90
|
if (!model) throw new Error('BpmnStudioController requires a model.');
|
|
91
|
+
this._allowedNodeTypes = normalizeAllowedNodeTypes(allowedNodeTypes);
|
|
92
|
+
this._allowedEdgeTypes = normalizeAllowedEdgeTypes(allowedEdgeTypes);
|
|
93
|
+
this._assertAllowedModel(model);
|
|
64
94
|
this._model = model;
|
|
65
95
|
this.history = new HistoryStack(historyLimit);
|
|
66
96
|
this.selection = { kind: 'process', id: model.id };
|
|
@@ -108,6 +138,32 @@ export class BpmnStudioController {
|
|
|
108
138
|
|
|
109
139
|
get model() { return this._model; }
|
|
110
140
|
|
|
141
|
+
allowsNodeType(nodeType) {
|
|
142
|
+
return Boolean(NODE_DEFINITIONS[nodeType])
|
|
143
|
+
&& nodeType !== 'generic'
|
|
144
|
+
&& (!this._allowedNodeTypes || this._allowedNodeTypes.has(nodeType));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
allowsEdgeType(edgeType) {
|
|
148
|
+
return Boolean(EDGE_DEFINITIONS[edgeType])
|
|
149
|
+
&& (!this._allowedEdgeTypes || this._allowedEdgeTypes.has(edgeType));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_assertAllowedModel(model) {
|
|
153
|
+
if (this._allowedNodeTypes) {
|
|
154
|
+
const unsupportedNode = model?.nodes?.find((node) => !this._allowedNodeTypes.has(node.type));
|
|
155
|
+
if (unsupportedNode) {
|
|
156
|
+
throw new Error(`BPMN node type is not allowed: ${unsupportedNode.type} (${unsupportedNode.id})`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (this._allowedEdgeTypes) {
|
|
160
|
+
const unsupportedEdge = model?.edges?.find((edge) => !this._allowedEdgeTypes.has(edge.type || 'sequenceFlow'));
|
|
161
|
+
if (unsupportedEdge) {
|
|
162
|
+
throw new Error(`BPMN edge type is not allowed: ${unsupportedEdge.type || 'sequenceFlow'} (${unsupportedEdge.id})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
111
167
|
getState() {
|
|
112
168
|
return {
|
|
113
169
|
model: cloneModel(this._model),
|
|
@@ -118,6 +174,8 @@ export class BpmnStudioController {
|
|
|
118
174
|
canRedo: this.history.canRedo,
|
|
119
175
|
engine: this._model.engine,
|
|
120
176
|
propertiesProfile: this.propertiesProfile,
|
|
177
|
+
allowedNodeTypes: this._allowedNodeTypes ? [...this._allowedNodeTypes] : null,
|
|
178
|
+
allowedEdgeTypes: this._allowedEdgeTypes ? [...this._allowedEdgeTypes] : null,
|
|
121
179
|
activeScopeId: this.activeScopeId,
|
|
122
180
|
scopePath: getScopePath(this._model, this.activeScopeId),
|
|
123
181
|
};
|
|
@@ -143,6 +201,7 @@ export class BpmnStudioController {
|
|
|
143
201
|
}
|
|
144
202
|
|
|
145
203
|
setModel(model, { resetHistory = true } = {}) {
|
|
204
|
+
this._assertAllowedModel(model);
|
|
146
205
|
const previousScopeId = this.activeScopeId;
|
|
147
206
|
this._model = model;
|
|
148
207
|
applyContainmentOperation(this._model, { type: 'reconcile' });
|
|
@@ -192,6 +251,7 @@ export class BpmnStudioController {
|
|
|
192
251
|
}
|
|
193
252
|
|
|
194
253
|
groupSelection() {
|
|
254
|
+
if (!this.allowsNodeType('group')) return null;
|
|
195
255
|
const selected = selectedDiagramNodes(this._model, this.selection, this.activeScopeId);
|
|
196
256
|
const groupable = selectedGroupableNodes(this._model, this.selection, this.activeScopeId);
|
|
197
257
|
if (!groupable.length || groupable.length !== selected.length) return null;
|
|
@@ -303,6 +363,7 @@ export class BpmnStudioController {
|
|
|
303
363
|
createNode({ nodeType, point, preset = {} } = {}) {
|
|
304
364
|
const def = NODE_DEFINITIONS[nodeType];
|
|
305
365
|
if (!def) throw new Error(`Unknown BPMN node type: ${nodeType}`);
|
|
366
|
+
if (!this.allowsNodeType(nodeType)) throw new Error(`BPMN node type is not allowed: ${nodeType}`);
|
|
306
367
|
const position = point || { x: 100, y: 100 };
|
|
307
368
|
let node;
|
|
308
369
|
this._commit('create-node', () => {
|
|
@@ -331,9 +392,15 @@ export class BpmnStudioController {
|
|
|
331
392
|
for (const spec of nodeSpecs) {
|
|
332
393
|
if (!spec.key || keys.has(spec.key)) throw new Error('Template node keys must be unique.');
|
|
333
394
|
if (!NODE_DEFINITIONS[spec.nodeType]) throw new Error(`Unknown BPMN node type: ${spec.nodeType}`);
|
|
395
|
+
if (!this.allowsNodeType(spec.nodeType)) throw new Error(`BPMN node type is not allowed: ${spec.nodeType}`);
|
|
334
396
|
keys.add(spec.key);
|
|
335
397
|
}
|
|
336
|
-
for (const edge of template.edges || [])
|
|
398
|
+
for (const edge of template.edges || []) {
|
|
399
|
+
if (!keys.has(edge.source) || !keys.has(edge.target)) throw new Error('Template edge references an unknown node key.');
|
|
400
|
+
const edgeType = edge.preset?.type || 'sequenceFlow';
|
|
401
|
+
if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown BPMN edge type: ${edgeType}`);
|
|
402
|
+
if (!this.allowsEdgeType(edgeType)) throw new Error(`BPMN edge type is not allowed: ${edgeType}`);
|
|
403
|
+
}
|
|
337
404
|
const result = { nodes: [], edges: [] };
|
|
338
405
|
const byKey = new Map();
|
|
339
406
|
this._commit('create-template', () => {
|
|
@@ -355,6 +422,9 @@ export class BpmnStudioController {
|
|
|
355
422
|
}
|
|
356
423
|
|
|
357
424
|
connect(source, target, overrides = {}) {
|
|
425
|
+
const edgeType = overrides.type || 'sequenceFlow';
|
|
426
|
+
if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown BPMN edge type: ${edgeType}`);
|
|
427
|
+
if (!this.allowsEdgeType(edgeType)) throw new Error(`BPMN edge type is not allowed: ${edgeType}`);
|
|
358
428
|
if (!canCreateSequenceFlow(this._model, source, target)) return null;
|
|
359
429
|
let edge;
|
|
360
430
|
this._commit('connect', () => {
|
|
@@ -366,6 +436,7 @@ export class BpmnStudioController {
|
|
|
366
436
|
}
|
|
367
437
|
|
|
368
438
|
startConnect(sourceId) {
|
|
439
|
+
if (!this.allowsEdgeType('sequenceFlow')) return false;
|
|
369
440
|
const source = getNode(this._model, sourceId);
|
|
370
441
|
const definition = source && (NODE_DEFINITIONS[source.type] || NODE_DEFINITIONS.generic);
|
|
371
442
|
if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !SEQUENCE_FLOW_KINDS.has(definition.kind) || definition.eventStage === 'end') return false;
|
|
@@ -407,6 +478,9 @@ export class BpmnStudioController {
|
|
|
407
478
|
updateNode(id, patch = {}) {
|
|
408
479
|
const node = getNode(this._model, id);
|
|
409
480
|
if (!node) return false;
|
|
481
|
+
if (patch.type !== undefined && patch.type !== node.type) {
|
|
482
|
+
throw new Error('Node type changes must use changeNodeType().');
|
|
483
|
+
}
|
|
410
484
|
const previousLabelPlacement = resolveSwimlaneLabelPlacement(this._model, node);
|
|
411
485
|
this._commit('update-node', () => {
|
|
412
486
|
if (patch.properties) node.properties = { ...node.properties, ...patch.properties };
|
|
@@ -431,6 +505,9 @@ export class BpmnStudioController {
|
|
|
431
505
|
updateEdge(id, patch = {}) {
|
|
432
506
|
const edge = getEdge(this._model, id);
|
|
433
507
|
if (!edge) return false;
|
|
508
|
+
if (patch.type !== undefined && patch.type !== (edge.type || 'sequenceFlow')) {
|
|
509
|
+
throw new Error('Edge type cannot be changed through updateEdge().');
|
|
510
|
+
}
|
|
434
511
|
this._commit('update-edge', () => {
|
|
435
512
|
if (patch.properties) edge.properties = { ...(edge.properties || {}), ...patch.properties };
|
|
436
513
|
const rest = { ...patch }; delete rest.properties;
|
|
@@ -500,7 +577,7 @@ export class BpmnStudioController {
|
|
|
500
577
|
changeNodeType(id, type) {
|
|
501
578
|
const node = getNode(this._model, id);
|
|
502
579
|
const def = NODE_DEFINITIONS[type];
|
|
503
|
-
if (!node || !def || node.type === type) return false;
|
|
580
|
+
if (!node || !def || !this.allowsNodeType(type) || node.type === type) return false;
|
|
504
581
|
if (isEmbeddedSubProcess(node) && !isEmbeddedSubProcess(type) && hasScopeChildren(this._model, id)) return false;
|
|
505
582
|
this._commit('change-node-type', () => {
|
|
506
583
|
const next = createNode(type, node.x, node.y, { id: node.id, name: node.name });
|
|
@@ -514,9 +591,10 @@ export class BpmnStudioController {
|
|
|
514
591
|
}
|
|
515
592
|
|
|
516
593
|
quickAdd(sourceId, type = 'userTask', preset = {}) {
|
|
594
|
+
if (!this.allowsEdgeType('sequenceFlow')) return null;
|
|
517
595
|
const source = getNode(this._model, sourceId);
|
|
518
596
|
const def = NODE_DEFINITIONS[type];
|
|
519
|
-
if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !def) return null;
|
|
597
|
+
if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !def || !this.allowsNodeType(type)) return null;
|
|
520
598
|
const direction = this._model.settings?.direction || 'horizontal';
|
|
521
599
|
const spacing = LAYOUT_DENSITIES[this._model.settings?.layoutDensity || 'balanced'] || LAYOUT_DENSITIES.balanced;
|
|
522
600
|
const outgoingCount = this._model.edges.filter((edge) => edge.source === sourceId).length;
|