@bpmn-nova/studio 0.3.1-preview → 0.3.3-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 +109 -4
- package/dist/canvas.js +5 -2
- package/dist/context-menu.js +1 -1
- package/dist/controller.js +82 -4
- package/dist/index.d.ts +100 -5
- package/dist/modules/export-svg/index.d.ts +2 -0
- package/dist/modules/export-svg/render.js +32 -8
- package/dist/modules/node-presentation/index.d.ts +25 -0
- package/dist/modules/node-presentation/index.js +51 -0
- package/dist/modules/palette/index.d.ts +14 -2
- package/dist/modules/palette/index.js +3 -3
- package/dist/modules/properties-bpmn/index.js +3 -1
- package/dist/modules/renderer-svg/index.d.ts +4 -0
- package/dist/modules/renderer-svg/index.js +80 -19
- package/dist/modules/viewer/index.d.ts +3 -0
- package/dist/modules/viewer/index.js +21 -4
- package/dist/shell.js +494 -171
- package/dist/styles.css +12 -3
- package/llms-full.txt +3322 -0
- package/llms.txt +236 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计
|
|
|
4
4
|
|
|
5
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.3-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -27,6 +27,8 @@ import '@bpmn-nova/studio/styles.css'
|
|
|
27
27
|
|
|
28
28
|
BPMN Nova 不会在 JavaScript 中隐式注入 CSS。视觉容器必须有明确高度。
|
|
29
29
|
|
|
30
|
+
使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
|
|
31
|
+
|
|
30
32
|
## 流程设计
|
|
31
33
|
|
|
32
34
|
```html
|
|
@@ -45,10 +47,16 @@ const model = createEmptyProcess('flowable')
|
|
|
45
47
|
model.id = 'Process_PurchaseApproval'
|
|
46
48
|
model.name = '采购申请审批流程'
|
|
47
49
|
|
|
48
|
-
const studio = createStudioController({
|
|
50
|
+
const studio = createStudioController({
|
|
51
|
+
model,
|
|
52
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
53
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
54
|
+
})
|
|
49
55
|
const shell = createStudioShell({
|
|
50
56
|
container: document.querySelector('#studio'),
|
|
51
57
|
studio,
|
|
58
|
+
mode: 'design',
|
|
59
|
+
allowedModes: ['design'],
|
|
52
60
|
theme: 'auto',
|
|
53
61
|
})
|
|
54
62
|
|
|
@@ -59,6 +67,103 @@ shell.destroy()
|
|
|
59
67
|
studio.destroy()
|
|
60
68
|
```
|
|
61
69
|
|
|
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
|
+
headerActions({ container, actions, getMode, subscribeMode }) {
|
|
89
|
+
// 替换默认“校验 / 导入 / 导出”,在这里挂载宿主的“校验 / 保存 / 发布”。
|
|
90
|
+
// 校验按钮调用 actions.validate();保存和发布继续调用宿主服务。
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
theme: 'auto',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
shell.setRegions({ right: 'default' })
|
|
97
|
+
shell.actions.undo()
|
|
98
|
+
shell.actions.redo()
|
|
99
|
+
shell.actions.fitView()
|
|
100
|
+
const issues = shell.actions.validate()
|
|
101
|
+
const xml = shell.actions.exportXml('flowable')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
|
|
105
|
+
|
|
106
|
+
`headerActions` 只替换 Header 最右侧动作组,`headerStart` 可与其同时使用,完整 `header` 的优先级更高。未提供 `headerActions` 时仍显示默认“校验 / 导入 / 导出”。顶部不再重复显示最佳视图;底部缩放区和 `fitView()` Interface 保持不变。
|
|
107
|
+
|
|
108
|
+
`shell.actions.validate()` 作为 Header 动作,以 `toolbar` 来源先更新 Nova 默认状态栏、再发送 Validation Event,最后返回 issues;程序化调用 `shell.validate()` 使用 `api` 来源。`valid` 表示没有 error,warning 不默认阻止发布:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const offValidation = shell.subscribeValidation((event) => {
|
|
112
|
+
console.log(event.source, event.valid, event.errorCount, event.warningCount, event.issues)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
shell.validate()
|
|
116
|
+
offValidation()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
宿主发布按钮可以复用 Toolbar 来源的完整流程;仅 error 默认阻止发布:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
async function publishProcess(actions) {
|
|
123
|
+
const issues = actions.validate()
|
|
124
|
+
if (issues.some((issue) => issue.level === 'error')) return
|
|
125
|
+
await publishXml(actions.exportXml())
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`shell.actions` 只封装撤销、重做、布局、视图、校验和 BPMN/SVG 导入导出。宿主的保存草稿、发布、权限、文件选择和服务端事务不属于 Nova Actions。
|
|
130
|
+
|
|
131
|
+
## Mode 与节点副标题投影
|
|
132
|
+
|
|
133
|
+
`design`、`viewer`、`instance` 分别表示流程设计、流程展示和审批轨迹。Shell 是唯一 Mode 状态源:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const unsubscribe = shell.subscribeMode((event) => {
|
|
137
|
+
console.log(event.mode, event.previousMode, event.source, event.allowedModes)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
shell.getMode()
|
|
141
|
+
shell.setMode('viewer')
|
|
142
|
+
shell.setAllowedModes(['design', 'viewer'])
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`setMode()` 仅在实际成功切换时返回 `true`。`setAllowedModes()` 拒绝空数组、重复值和未知值;若移除当前 Mode,会回退到新数组首项并发送一次 `allowed-modes` 事件。切换只改变 Shell 展示,不进入 Model、History 或 Studio State。
|
|
146
|
+
|
|
147
|
+
标准任务与容器卡片支持定义态副标题 Resolver:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const summaries = new Map()
|
|
151
|
+
const shell = createStudioShell({
|
|
152
|
+
container,
|
|
153
|
+
studio,
|
|
154
|
+
nodeSubtitleResolver({ node, mode, surface, defaultSubtitle }) {
|
|
155
|
+
return summaries.has(node.id) ? summaries.get(node.id) : undefined
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
summaries.set('ServiceTask_Archive', '归档到采购系统')
|
|
160
|
+
shell.refreshPresentation()
|
|
161
|
+
summaries.set('ServiceTask_Archive', null)
|
|
162
|
+
shell.refreshPresentation()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`undefined` 保留默认副标题,`null` 移除副标题行,字符串(包括空字符串)作为覆盖值。Resolver 同步应用于 Design、Viewer 与标准 SVG 降级视觉;完整自定义 Renderer 优先,Instance 始终使用 Runtime Presentation。`refreshPresentation()` 不写 XML、不创建历史、不改变选择、Scope 或视口。
|
|
166
|
+
|
|
62
167
|
## 只读 Viewer
|
|
63
168
|
|
|
64
169
|
```js
|
|
@@ -227,12 +332,12 @@ import { activitiProfile } from '@bpmn-nova/studio/activiti'
|
|
|
227
332
|
|
|
228
333
|
## 文档与 AI
|
|
229
334
|
|
|
335
|
+
- npm 包内 `llms.txt`:可执行安装入口
|
|
336
|
+
- npm 包内 `llms-full.txt`:完整 AI 上下文
|
|
230
337
|
- [项目首页](https://github.com/daxiangme/bpmn-nova)
|
|
231
338
|
- [快速开始](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/GETTING-STARTED.md)
|
|
232
339
|
- [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
|
|
233
340
|
- [自定义指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md)
|
|
234
|
-
- [AI 接入入口](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms.txt)
|
|
235
|
-
- [AI 完整上下文](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms-full.txt)
|
|
236
341
|
|
|
237
342
|
## 能力边界
|
|
238
343
|
|
package/dist/canvas.js
CHANGED
|
@@ -35,7 +35,7 @@ function toolbarIcon(id) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class BpmnCanvas {
|
|
38
|
-
constructor({ container, studio, rendererOptions = {}, interactions = null, selectionToolbar = null, contextMenu = null, contextMenuRegistry = createDefaultContextMenuRegistry(), pointerMode = 'select', themeController = null, theme = null, onThemeChange = null } = {}) {
|
|
38
|
+
constructor({ container, studio, rendererOptions = {}, nodeSubtitleResolver = rendererOptions.nodeSubtitleResolver, interactions = null, selectionToolbar = null, contextMenu = null, contextMenuRegistry = createDefaultContextMenuRegistry(), pointerMode = 'select', themeController = null, theme = null, onThemeChange = null } = {}) {
|
|
39
39
|
if (!container) throw new Error('BpmnCanvas requires a container.');
|
|
40
40
|
if (!studio) throw new Error('BpmnCanvas requires a studio controller.');
|
|
41
41
|
this.container = container;
|
|
@@ -51,6 +51,7 @@ export class BpmnCanvas {
|
|
|
51
51
|
const onViewportChange = rendererOptions.onViewportChange;
|
|
52
52
|
this.renderer = new DiagramRenderer(container, {
|
|
53
53
|
...rendererOptions,
|
|
54
|
+
nodeSubtitleResolver,
|
|
54
55
|
themeController,
|
|
55
56
|
theme,
|
|
56
57
|
onThemeChange,
|
|
@@ -80,6 +81,7 @@ export class BpmnCanvas {
|
|
|
80
81
|
},
|
|
81
82
|
onStartConnect: (node) => studio.startConnect(node.id),
|
|
82
83
|
canConnect: (source, target) => canCreateSequenceFlow(studio.model, source.id, target.id),
|
|
84
|
+
canCreateNodeType: (nodeType) => studio.allowsNodeType(nodeType),
|
|
83
85
|
onQuickAdd: (source, type, preset) => studio.commands.quickAdd(source.id, type, preset),
|
|
84
86
|
onDeleteNode: (node) => studio.commands.remove({ kind: 'node', id: node.id }),
|
|
85
87
|
onUngroup: (node) => studio.commands.ungroup(node.id),
|
|
@@ -521,7 +523,7 @@ export class BpmnCanvas {
|
|
|
521
523
|
{ id: 'distribute-vertical', iconId: 'ui.distributeVertical', title: '垂直等距', action: () => actions.distribute('vertical'), disabled: nodes.length < 3 },
|
|
522
524
|
];
|
|
523
525
|
this._selectionToolbar.append(
|
|
524
|
-
button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
|
|
526
|
+
button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: this.studio.allowsNodeType?.('group') === false || !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
|
|
525
527
|
separator(),
|
|
526
528
|
menu('align', 'ui.alignCenterHorizontal', '对齐选区', alignmentItems, nodes.length < 2),
|
|
527
529
|
menu('distribute', 'ui.distributeHorizontal', '等距分布', distributeItems, nodes.length < 3),
|
|
@@ -904,6 +906,7 @@ export class BpmnCanvas {
|
|
|
904
906
|
startConnect(nodeId) { this.renderer.closeTransientOverlays?.(); return this.studio.startConnect(nodeId); }
|
|
905
907
|
editEdgeName(edgeId) { return this.renderer.editEdgeName?.(edgeId) || false; }
|
|
906
908
|
fitView(padding, options) { this.renderer.fitView(padding, { minZoom: 0.7, ...(options || {}) }); }
|
|
909
|
+
refreshPresentation() { this.renderer.refreshPresentation(); }
|
|
907
910
|
exportSvg(options) { return this.renderer.exportSvg(options); }
|
|
908
911
|
openSvgExportPreview(options) { return this.renderer.openSvgExportPreview(options); }
|
|
909
912
|
fitSelection(padding = 72) {
|
package/dist/context-menu.js
CHANGED
|
@@ -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,
|
|
@@ -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;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './modules/palette/index.js'
|
|
|
14
14
|
import type {
|
|
15
15
|
BpmnEdge,
|
|
16
16
|
BpmnNode,
|
|
17
|
+
EdgeType,
|
|
17
18
|
ElementSelection,
|
|
18
19
|
EngineId,
|
|
19
20
|
LayoutOptions,
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
import type { IconRegistry } from './modules/icons/index.js'
|
|
26
27
|
import type { PaletteRegistry } from './modules/palette/index.js'
|
|
27
28
|
import type { PropertiesRegistry } from './modules/properties/index.js'
|
|
29
|
+
import type { NodeSubtitleResolver } from './modules/node-presentation/index.js'
|
|
28
30
|
import type { DiagramNodeRenderer, DiagramRendererOptions } from './modules/renderer-svg/index.js'
|
|
29
31
|
import type { RuntimeTimelineSvgRenderer, SvgExportArtifact, SvgExportOptions, SvgExportPreviewController, SvgNodeRenderer } from './modules/export-svg/index.js'
|
|
30
32
|
import type {
|
|
@@ -37,6 +39,8 @@ import type {
|
|
|
37
39
|
RuntimeTransitionDetailsRenderer,
|
|
38
40
|
} from './modules/viewer/index.js'
|
|
39
41
|
|
|
42
|
+
export type { NodeSubtitleResolver, NodeSubtitleResolverContext } from './modules/node-presentation/index.js'
|
|
43
|
+
|
|
40
44
|
// Aggregate compatibility surface. Studio re-exports the package-level types,
|
|
41
45
|
// and keeps these frequently consumed declarations visible at its own entry.
|
|
42
46
|
export interface ScopedElement { scopeId?: string }
|
|
@@ -140,6 +144,7 @@ export interface ViewerOptions {
|
|
|
140
144
|
iconRegistry?: IconRegistry
|
|
141
145
|
nodeRenderers?: Record<string, Function>
|
|
142
146
|
nodeRenderer?: Function
|
|
147
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
143
148
|
projection?: ViewerProjection
|
|
144
149
|
responsive?: boolean
|
|
145
150
|
timeline?: ViewerTimelineOptions
|
|
@@ -172,6 +177,7 @@ export class BpmnViewer {
|
|
|
172
177
|
traceProjection: RuntimeTraceProjection | null
|
|
173
178
|
setModel(model: ProcessModel): void
|
|
174
179
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
180
|
+
refreshPresentation(): void
|
|
175
181
|
setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: import('./modules/viewer/index.js').RuntimeAssetResolver | null }): void
|
|
176
182
|
setProjection(projection: ViewerProjection): void
|
|
177
183
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
@@ -201,9 +207,16 @@ export interface StudioState {
|
|
|
201
207
|
canRedo: boolean
|
|
202
208
|
engine: EngineId
|
|
203
209
|
propertiesProfile: 'business' | 'developer'
|
|
210
|
+
allowedNodeTypes: NodeType[] | null
|
|
211
|
+
allowedEdgeTypes: EdgeType[] | null
|
|
204
212
|
activeScopeId: string
|
|
205
213
|
scopePath: Array<{ id: string; name: string; kind: 'process' | 'subProcess' }>
|
|
206
214
|
}
|
|
215
|
+
export interface StudioValidationIssue {
|
|
216
|
+
level: 'error' | 'warning'
|
|
217
|
+
elementId?: string
|
|
218
|
+
message: string
|
|
219
|
+
}
|
|
207
220
|
export interface NodePreset extends Partial<Omit<BpmnNode, 'properties'>> { properties?: Record<string, unknown> }
|
|
208
221
|
export interface TemplateDefinition {
|
|
209
222
|
nodes: Array<{ key: string; nodeType: NodeType; offset?: Point; preset?: NodePreset }>
|
|
@@ -223,6 +236,8 @@ export interface StudioControllerOptions {
|
|
|
223
236
|
historyLimit?: number
|
|
224
237
|
extensions?: StudioExtension[]
|
|
225
238
|
propertiesProfile?: 'business' | 'developer'
|
|
239
|
+
allowedNodeTypes?: Iterable<NodeType> | null
|
|
240
|
+
allowedEdgeTypes?: Iterable<EdgeType> | null
|
|
226
241
|
}
|
|
227
242
|
export interface StudioCommands {
|
|
228
243
|
createNode(input: { nodeType: NodeType; point?: Point; preset?: NodePreset }): BpmnNode
|
|
@@ -289,6 +304,8 @@ export class BpmnStudioController {
|
|
|
289
304
|
navigateToScope(scopeId: string): boolean
|
|
290
305
|
setModel(model: ProcessModel, options?: { resetHistory?: boolean }): void
|
|
291
306
|
setPropertiesProfile(profile: 'business' | 'developer'): void
|
|
307
|
+
allowsNodeType(nodeType: NodeType): boolean
|
|
308
|
+
allowsEdgeType(edgeType: EdgeType): boolean
|
|
292
309
|
startConnect(sourceId: string): boolean
|
|
293
310
|
cancelConnect(): void
|
|
294
311
|
createNode(input: Parameters<StudioCommands['createNode']>[0]): BpmnNode
|
|
@@ -311,6 +328,7 @@ export class BpmnStudioController {
|
|
|
311
328
|
redo(): boolean
|
|
312
329
|
exportXml(engine?: EngineId): string
|
|
313
330
|
importXml(xml: string, engine?: EngineId): ProcessModel
|
|
331
|
+
validate(): StudioValidationIssue[]
|
|
314
332
|
registerExtension(extension: StudioExtension): () => void
|
|
315
333
|
destroy(): void
|
|
316
334
|
}
|
|
@@ -401,6 +419,7 @@ export interface BpmnCanvasOptions {
|
|
|
401
419
|
container: HTMLElement
|
|
402
420
|
studio: BpmnStudioController
|
|
403
421
|
rendererOptions?: DiagramRendererOptions
|
|
422
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
404
423
|
interactions?: InteractionController | null
|
|
405
424
|
selectionToolbar?: HTMLElement | SelectionToolbarSlot | null
|
|
406
425
|
contextMenu?: HTMLElement | ContextMenuSlot | null
|
|
@@ -414,6 +433,7 @@ export class BpmnCanvas {
|
|
|
414
433
|
clientToWorld(clientX: number, clientY: number): Point
|
|
415
434
|
getViewportCenterWorld(): Point
|
|
416
435
|
fitView(padding?: number, options?: Record<string, unknown>): void
|
|
436
|
+
refreshPresentation(): void
|
|
417
437
|
fitSelection(padding?: number): boolean
|
|
418
438
|
setPointerMode(mode: 'select' | 'marquee' | 'pan'): boolean
|
|
419
439
|
getPointerMode(): 'select' | 'marquee' | 'pan'
|
|
@@ -430,10 +450,59 @@ export class BpmnCanvas {
|
|
|
430
450
|
}
|
|
431
451
|
export function createBpmnCanvas(options: BpmnCanvasOptions): BpmnCanvas
|
|
432
452
|
|
|
453
|
+
export interface StudioShellActions {
|
|
454
|
+
undo(): boolean
|
|
455
|
+
redo(): boolean
|
|
456
|
+
beautify(options?: LayoutOptions): void
|
|
457
|
+
rerouteEdges(options?: LayoutOptions): void
|
|
458
|
+
fitView(): void
|
|
459
|
+
validate(): StudioValidationIssue[]
|
|
460
|
+
importXml(xml: string, engine?: EngineId): ProcessModel
|
|
461
|
+
exportXml(engine?: EngineId): string
|
|
462
|
+
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact>
|
|
463
|
+
openSvgExportPreview(options?: SvgExportOptions & {
|
|
464
|
+
previewTitle?: string
|
|
465
|
+
onDownload?: (artifact: SvgExportArtifact) => void
|
|
466
|
+
}): SvgExportPreviewController | null
|
|
467
|
+
}
|
|
468
|
+
export type StudioMode = 'design' | 'viewer' | 'instance'
|
|
469
|
+
export type StudioModeChangeSource = 'toolbar' | 'api' | 'allowed-modes'
|
|
470
|
+
export interface StudioModeChangeEvent {
|
|
471
|
+
readonly mode: StudioMode
|
|
472
|
+
readonly previousMode: StudioMode
|
|
473
|
+
readonly source: StudioModeChangeSource
|
|
474
|
+
readonly allowedModes: readonly StudioMode[]
|
|
475
|
+
}
|
|
476
|
+
export type StudioValidationSource = 'toolbar' | 'api'
|
|
477
|
+
export interface StudioValidationEvent {
|
|
478
|
+
readonly source: StudioValidationSource
|
|
479
|
+
readonly valid: boolean
|
|
480
|
+
readonly errorCount: number
|
|
481
|
+
readonly warningCount: number
|
|
482
|
+
readonly issues: readonly StudioValidationIssue[]
|
|
483
|
+
}
|
|
484
|
+
export interface StudioShellSlotContext {
|
|
485
|
+
studio: BpmnStudioController
|
|
486
|
+
shell: BpmnStudioShell
|
|
487
|
+
canvas: BpmnCanvas
|
|
488
|
+
actions: StudioShellActions
|
|
489
|
+
getState(): StudioState
|
|
490
|
+
subscribe(listener: (event: StudioEvent) => void): () => void
|
|
491
|
+
getMode(): StudioMode
|
|
492
|
+
getAllowedModes(): readonly StudioMode[]
|
|
493
|
+
subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
|
|
494
|
+
subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
|
|
495
|
+
}
|
|
496
|
+
export type StudioShellDomSlot =
|
|
497
|
+
| HTMLElement
|
|
498
|
+
| ((context: StudioShellSlotContext & { container: HTMLElement }) => void | (() => void))
|
|
433
499
|
export interface StudioShellSlots {
|
|
434
|
-
left?:
|
|
435
|
-
right?:
|
|
436
|
-
|
|
500
|
+
left?: StudioShellDomSlot
|
|
501
|
+
right?: StudioShellDomSlot
|
|
502
|
+
headerStart?: StudioShellDomSlot
|
|
503
|
+
headerActions?: StudioShellDomSlot
|
|
504
|
+
header?: StudioShellDomSlot
|
|
505
|
+
footer?: StudioShellDomSlot
|
|
437
506
|
contextMenu?: ContextMenuSlot
|
|
438
507
|
selectionToolbar?: SelectionToolbarSlot
|
|
439
508
|
runtimeDetails?: RuntimeDetailsRenderer
|
|
@@ -441,6 +510,19 @@ export interface StudioShellSlots {
|
|
|
441
510
|
runtimeTimeline?: RuntimeTimelineRenderer
|
|
442
511
|
[name: string]: HTMLElement | Function | undefined
|
|
443
512
|
}
|
|
513
|
+
export type StudioShellRegionMode = 'default' | 'hidden'
|
|
514
|
+
export interface StudioShellRegions {
|
|
515
|
+
header?: StudioShellRegionMode
|
|
516
|
+
left?: StudioShellRegionMode
|
|
517
|
+
right?: StudioShellRegionMode
|
|
518
|
+
footer?: StudioShellRegionMode
|
|
519
|
+
}
|
|
520
|
+
export interface StudioShellRegionState {
|
|
521
|
+
header: StudioShellRegionMode
|
|
522
|
+
left: StudioShellRegionMode
|
|
523
|
+
right: StudioShellRegionMode
|
|
524
|
+
footer: StudioShellRegionMode
|
|
525
|
+
}
|
|
444
526
|
export interface StudioShellOptions {
|
|
445
527
|
container: HTMLElement
|
|
446
528
|
studio: BpmnStudioController
|
|
@@ -453,10 +535,13 @@ export interface StudioShellOptions {
|
|
|
453
535
|
runtimeTraceOptions?: RuntimeTraceProjectionOptions
|
|
454
536
|
timeline?: ViewerTimelineOptions
|
|
455
537
|
}
|
|
538
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
456
539
|
slots?: StudioShellSlots
|
|
540
|
+
regions?: StudioShellRegions
|
|
457
541
|
layout?: (context: Record<string, unknown>) => void | (() => void)
|
|
458
542
|
runtime?: ProcessInstanceSnapshot | null
|
|
459
|
-
mode?:
|
|
543
|
+
mode?: StudioMode
|
|
544
|
+
allowedModes?: readonly StudioMode[]
|
|
460
545
|
projection?: ViewerProjection
|
|
461
546
|
responsive?: boolean
|
|
462
547
|
projectionOptions?: Array<{ value: ViewerProjection; label: string }>
|
|
@@ -469,9 +554,17 @@ export interface StudioShellOptions {
|
|
|
469
554
|
}
|
|
470
555
|
export class BpmnStudioShell {
|
|
471
556
|
constructor(options: StudioShellOptions)
|
|
557
|
+
readonly actions: StudioShellActions
|
|
472
558
|
canvas?: BpmnCanvas
|
|
473
559
|
viewer?: BpmnViewer
|
|
474
|
-
|
|
560
|
+
getMode(): StudioMode
|
|
561
|
+
getAllowedModes(): readonly StudioMode[]
|
|
562
|
+
subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
|
|
563
|
+
subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
|
|
564
|
+
setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
|
|
565
|
+
getRegions(): Readonly<StudioShellRegionState>
|
|
566
|
+
setRegions(regions: StudioShellRegions): Readonly<StudioShellRegionState>
|
|
567
|
+
setMode(mode: StudioMode): boolean
|
|
475
568
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
476
569
|
setProjection(projection: ViewerProjection): void
|
|
477
570
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
@@ -479,6 +572,8 @@ export class BpmnStudioShell {
|
|
|
479
572
|
getThemeState(): NovaThemeState
|
|
480
573
|
subscribeTheme(listener: (state: NovaThemeState) => void): () => void
|
|
481
574
|
setRuntimeAppearance(options: RuntimeAppearanceOptions | null): void
|
|
575
|
+
refreshPresentation(): void
|
|
576
|
+
validate(): StudioValidationIssue[]
|
|
482
577
|
fitView(): void
|
|
483
578
|
zoomBy(factor: number): void
|
|
484
579
|
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact>
|
|
@@ -2,6 +2,7 @@ import type { BpmnNode, ProcessModel } from '../core/index.js'
|
|
|
2
2
|
import type { IconRegistry, NodeVisualResolution } from '../icons/index.js'
|
|
3
3
|
import type { NodeRuntimePresentation, ProcessInstanceSnapshot, RuntimeApprovalActionPresentation, RuntimePresentation, RuntimeAssetRef } from '../runtime/index.js'
|
|
4
4
|
import type { NovaResolvedTheme, NovaThemeSnapshot, RuntimeAppearanceOptions, RuntimeAppearanceResolver, ThemeController } from '../theme/index.js'
|
|
5
|
+
import type { NodeSubtitleResolver } from '../node-presentation/index.js'
|
|
5
6
|
|
|
6
7
|
export type SvgExportTheme = 'current' | NovaResolvedTheme
|
|
7
8
|
export interface SvgExportOptions {
|
|
@@ -65,6 +66,7 @@ export interface DiagramSvgExportContext {
|
|
|
65
66
|
nodeRenderers?: Record<string, SvgNodeRenderer>
|
|
66
67
|
htmlNodeRenderers?: Record<string, Function>
|
|
67
68
|
htmlNodeRenderer?: Function
|
|
69
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
68
70
|
mode?: 'design' | 'viewer' | 'instance'
|
|
69
71
|
label?: string
|
|
70
72
|
}
|