@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.
Files changed (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
@@ -0,0 +1,518 @@
1
+ import { NODE_DEFINITIONS, resolveGatewayRole, resolveSwimlaneLabelPlacement } from '../core/index.js';
2
+ import { propertyEntry, propertyGroup } from '../properties-core/index.js';
3
+
4
+ const activityKinds = new Set(['task', 'container']);
5
+ const eventRefs = {
6
+ message: { resource: 'messages', label: '消息 Message' },
7
+ signal: { resource: 'signals', label: '信号 Signal' },
8
+ error: { resource: 'errors', label: '错误 Error' },
9
+ escalation: { resource: 'escalations', label: '升级 Escalation' },
10
+ };
11
+
12
+ function nodeTypeOptions(context) {
13
+ const currentKind = context.definition?.kind;
14
+ const compatible = currentKind === 'event' || currentKind === 'boundary'
15
+ ? ['event', 'boundary']
16
+ : currentKind === 'task' || currentKind === 'container'
17
+ ? ['task', 'container']
18
+ : [currentKind];
19
+ return Object.entries(NODE_DEFINITIONS)
20
+ .filter(([type, def]) => type !== 'generic'
21
+ && compatible.includes(def.kind)
22
+ && context.studio?.allowsNodeType?.(type) !== false)
23
+ .map(([value, def]) => ({ value, label: def.label }));
24
+ }
25
+
26
+ function idValidator(value, context) {
27
+ if (!value) return 'ID 不能为空';
28
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value)) return 'ID 只能包含字母、数字、_、-、.,且不能以数字开头';
29
+ const model = context.model;
30
+ const processConflict = context.kind !== 'process' && model.id === value;
31
+ const nodeConflict = model.nodes.some((item) => item.id === value && !(context.kind === 'node' && item === context.element));
32
+ const edgeConflict = model.edges.some((item) => item.id === value && !(context.kind === 'edge' && item === context.element));
33
+ if (processConflict || nodeConflict || edgeConflict) return 'ID 已存在;BPMN Element ID 必须在 Definitions 内唯一';
34
+ return null;
35
+ }
36
+
37
+ const basicProvider = {
38
+ id: 'bpmn-basic',
39
+ priority: 1000,
40
+ getGroups(context) {
41
+ if (context.kind === 'process') {
42
+ return [propertyGroup({
43
+ id: 'basic', label: '基本信息', collapsible: false,
44
+ entries: [
45
+ propertyEntry({ id: 'process-name', label: '流程名称', path: 'name', required: true }),
46
+ propertyEntry({ id: 'process-id', label: 'Process ID', path: 'id', required: true, validate: idValidator, note: '部署到流程引擎时作为 processDefinitionKey。', level: 'developer' }),
47
+ propertyEntry({ id: 'namespace', label: 'Target Namespace', path: 'targetNamespace', placeholder: 'urn:company:workflow', level: 'developer' }),
48
+ propertyEntry({ id: 'executable', type: 'switch', label: '可执行流程', path: 'isExecutable' }),
49
+ propertyEntry({
50
+ id: 'engine', type: 'select', label: '流程引擎',
51
+ options: [{ value: 'flowable', label: 'Flowable' }, { value: 'activiti', label: 'Activiti' }],
52
+ getValue: (ctx) => ctx.model.engine,
53
+ setValue: (ctx, value) => ctx.studio.setEngine(value),
54
+ }),
55
+ ],
56
+ })];
57
+ }
58
+ if (context.kind === 'edge') {
59
+ return [propertyGroup({
60
+ id: 'basic', label: context.element.type === 'sequenceFlow' ? '分支定义' : '连线信息', collapsible: false,
61
+ entries: [
62
+ propertyEntry({ id: 'edge-name', label: '名称', path: 'name', placeholder: '例如:同意 / 驳回 / 金额 ≥ 5万' }),
63
+ propertyEntry({ id: 'edge-id', label: 'Flow ID', path: 'id', required: true, validate: idValidator }),
64
+ propertyEntry({ id: 'edge-type', type: 'readonly', label: 'BPMN Type', getValue: (ctx) => ctx.element.type === 'messageFlow' ? 'bpmn:MessageFlow' : ctx.element.type === 'association' ? 'bpmn:Association' : 'bpmn:SequenceFlow' }),
65
+ ],
66
+ })];
67
+ }
68
+ const def = context.definition;
69
+ return [propertyGroup({
70
+ id: 'basic', label: '基本信息', collapsible: false,
71
+ entries: [
72
+ propertyEntry({ id: 'name', label: '名称', path: 'name', required: true }),
73
+ propertyEntry({ id: 'id', label: 'Element ID', path: 'id', required: true, validate: idValidator, level: 'developer' }),
74
+ propertyEntry({
75
+ id: 'node-type', type: 'select', label: '节点类型', options: nodeTypeOptions,
76
+ getValue: (ctx) => ctx.element.type,
77
+ setValue: (ctx, value) => ctx.designer.changeNodeType(ctx.element.id, value),
78
+ }),
79
+ propertyEntry({ id: 'bpmn-type', type: 'readonly', label: 'BPMN Type', getValue: (ctx) => ctx.definition?.bpmnType || ctx.element.bpmnType, level: 'developer' }),
80
+ ],
81
+ })];
82
+ },
83
+ };
84
+
85
+ const documentationProvider = {
86
+ id: 'bpmn-documentation',
87
+ priority: 200,
88
+ getGroups(context) {
89
+ return [propertyGroup({
90
+ id: 'documentation', label: '文档说明', collapsed: true, position: { append: true },
91
+ entries: [propertyEntry({
92
+ id: 'documentation', type: 'textarea', label: 'Documentation', path: context.kind === 'process' ? 'properties.documentation' : 'properties.documentation',
93
+ placeholder: '节点说明、业务规则、实现备注或流程文档。', rows: 5,
94
+ })],
95
+ })];
96
+ },
97
+ };
98
+
99
+ const processProvider = {
100
+ id: 'bpmn-process',
101
+ priority: 800,
102
+ appliesTo: { kind: 'process' },
103
+ getGroups() {
104
+ return [
105
+ propertyGroup({
106
+ id: 'process-start-permissions', label: '启动权限', collapsed: true, position: { after: 'basic' },
107
+ entries: [
108
+ propertyEntry({ id: 'starter-users', type: 'tags', label: '候选启动用户', path: 'properties.candidateStarterUsers', placeholder: 'user1,user2' }),
109
+ propertyEntry({ id: 'starter-groups', type: 'tags', label: '候选启动组', path: 'properties.candidateStarterGroups', placeholder: 'manager,finance' }),
110
+ ],
111
+ }),
112
+ propertyGroup({
113
+ id: 'global-resources', label: '全局事件资源', collapsed: true, position: { after: 'process-start-permissions' },
114
+ description: '集中管理 Message / Signal / Error / Escalation,事件节点通过 ID 引用。',
115
+ entries: [
116
+ propertyEntry({ id: 'messages', type: 'table', label: 'Messages', path: 'resources.messages', rowLabel: '消息', defaultRow: () => ({ id: `Message_${Date.now().toString(36)}`, name: '新消息' }), columns: [{ key: 'id', label: 'ID' }, { key: 'name', label: '名称' }] }),
117
+ propertyEntry({ id: 'signals', type: 'table', label: 'Signals', path: 'resources.signals', rowLabel: '信号', defaultRow: () => ({ id: `Signal_${Date.now().toString(36)}`, name: '新信号' }), columns: [{ key: 'id', label: 'ID' }, { key: 'name', label: '名称' }] }),
118
+ propertyEntry({ id: 'errors', type: 'table', label: 'Errors', path: 'resources.errors', rowLabel: '错误', defaultRow: () => ({ id: `Error_${Date.now().toString(36)}`, name: '业务错误', errorCode: 'BUSINESS_ERROR' }), columns: [{ key: 'id', label: 'ID' }, { key: 'name', label: '名称' }, { key: 'errorCode', label: 'Error Code' }] }),
119
+ propertyEntry({ id: 'escalations', type: 'table', label: 'Escalations', path: 'resources.escalations', rowLabel: '升级', defaultRow: () => ({ id: `Escalation_${Date.now().toString(36)}`, name: '业务升级', escalationCode: 'ESCALATE' }), columns: [{ key: 'id', label: 'ID' }, { key: 'name', label: '名称' }, { key: 'escalationCode', label: 'Code' }] }),
120
+ ],
121
+ }),
122
+ propertyGroup({
123
+ id: 'namespaces', label: '命名空间', collapsed: true, position: { after: 'global-resources' },
124
+ description: '自定义属性 / Extension Element 使用的 XML Namespace。填写前缀时使用 xmlns:company 形式。',
125
+ entries: [propertyEntry({
126
+ id: 'namespace-table', type: 'table', label: 'Namespace Declarations',
127
+ getValue: (ctx) => Object.entries(ctx.model.namespaceDeclarations || {}).map(([name, uri]) => ({ name, uri })),
128
+ setValue: (ctx, rows) => ctx.designer.updateProcess({ namespaceDeclarations: Object.fromEntries((rows || []).filter((row) => row.name && row.uri).map((row) => [row.name.startsWith('xmlns:') ? row.name : `xmlns:${row.name}`, row.uri])) }),
129
+ rowLabel: '命名空间', defaultRow: () => ({ name: 'xmlns:company', uri: 'urn:company:bpmn-extension' }), columns: [{ key: 'name', label: 'Prefix' }, { key: 'uri', label: 'URI' }],
130
+ })],
131
+ }),
132
+ propertyGroup({
133
+ id: 'canvas-display', label: '画布显示', position: { after: 'basic' },
134
+ entries: [
135
+ propertyEntry({ id: 'show-grid', type: 'switch', label: '显示网格', path: 'settings.showGrid', defaultValue: true }),
136
+ propertyEntry({ id: 'grid-size', type: 'range', label: '网格尺寸', path: 'settings.gridSize', defaultValue: 16, min: 8, max: 40, step: 4, unit: 'px' }),
137
+ propertyEntry({ id: 'snap-to-grid', type: 'switch', label: '吸附到网格', path: 'settings.snapToGrid', defaultValue: false }),
138
+ propertyEntry({ id: 'alignment-guides', type: 'switch', label: '智能对齐辅助线', path: 'settings.alignmentGuides', defaultValue: true, note: '关闭后不再进行磁性对齐,也不显示拖动辅助线。' }),
139
+ ],
140
+ }),
141
+ propertyGroup({
142
+ id: 'nova-layout', label: '布局与连线', position: { after: 'canvas-display' },
143
+ entries: [
144
+ propertyEntry({ id: 'direction', type: 'select', label: '默认布局方向', path: 'settings.direction', options: [['horizontal', '横向 Left → Right'], ['vertical', '纵向 Top → Bottom']] }),
145
+ propertyEntry({ id: 'density', type: 'select', label: '布局紧凑度', path: 'settings.layoutDensity', options: [['compact', '紧凑'], ['balanced', '标准'], ['spacious', '舒展']], note: '影响快捷添加和一键美化的节点间距。' }),
146
+ propertyEntry({ id: 'edge-style', type: 'select', label: '默认连线风格', path: 'settings.edgeStyle', options: [['rounded', '圆角折线'], ['smooth', '柔和曲线'], ['straight', '直角折线']] }),
147
+ propertyEntry({ id: 'beautify', type: 'action', label: '一键整理全部节点与连线', actionLabel: '一键美化', iconId: 'ui.magic', action: (ctx) => ctx.designer.beautify({ direction: ctx.model.settings?.direction, density: ctx.model.settings?.layoutDensity, edgeStyle: ctx.model.settings?.edgeStyle }) }),
148
+ ],
149
+ }),
150
+ ];
151
+ },
152
+ };
153
+
154
+ const sequenceFlowProvider = {
155
+ id: 'bpmn-sequence-flow',
156
+ priority: 850,
157
+ appliesTo: (ctx) => ctx.kind === 'edge' && (ctx.element.type || 'sequenceFlow') === 'sequenceFlow',
158
+ getGroups(context) {
159
+ const source = context.model.nodes.find((node) => node.id === context.element.source);
160
+ const sourceDef = NODE_DEFINITIONS[source?.type] || {};
161
+ return [
162
+ propertyGroup({
163
+ id: 'condition', label: '条件与默认出口', position: { after: 'basic' },
164
+ entries: [
165
+ propertyEntry({ id: 'condition-expression', type: 'expression', label: '条件表达式', path: 'condition', placeholder: '${amount >= 50000}', note: source?.type === 'parallelGateway' ? '并行网关不会根据条件选择出口;仅供开发者清理历史数据。' : '导出为 bpmn:conditionExpression。', visible: (ctx) => source?.type === 'parallelGateway' ? ctx.profile === 'developer' : sourceDef.kind === 'gateway' || !!context.element.condition }),
166
+ propertyEntry({
167
+ id: 'default-flow', type: 'switch', label: '默认线路', getValue: (ctx) => !!ctx.element.isDefault,
168
+ setValue: (ctx, value) => {
169
+ const patches = [];
170
+ if (value) {
171
+ for (const edge of ctx.model.edges.filter((edge) => edge.source === ctx.element.source && edge.id !== ctx.element.id && edge.isDefault)) patches.push({ id: edge.id, patch: { isDefault: false } });
172
+ }
173
+ patches.push({ id: ctx.element.id, patch: { isDefault: !!value } });
174
+ ctx.designer.updateEdges(patches);
175
+ },
176
+ note: source?.type === 'parallelGateway' ? '并行网关不支持默认出口;仅供开发者清理历史数据。' : '',
177
+ visible: (ctx) => ['exclusiveGateway', 'inclusiveGateway', 'complexGateway'].includes(source?.type) || (source?.type === 'parallelGateway' && ctx.profile === 'developer'),
178
+ }),
179
+ ],
180
+ }),
181
+ propertyGroup({
182
+ id: 'nova-edge-style', label: '流程图显示', collapsed: true, position: { after: 'condition' },
183
+ entries: [
184
+ propertyEntry({ id: 'route-style', type: 'select', label: '线路样式', path: 'routeStyle', options: [['rounded', '圆角折线'], ['smooth', '柔和曲线'], ['straight', '直角折线']] }),
185
+ propertyEntry({ id: 'corner-radius', type: 'range', label: '圆角半径', path: 'cornerRadius', min: 0, max: 28, step: 1, unit: 'px' }),
186
+ propertyEntry({ id: 'reset-route', type: 'action', label: '重算最佳路径', actionLabel: '重算线路', iconId: 'ui.routeSmooth', action: (ctx) => ctx.designer.resetEdgeRoute(ctx.element.id) }),
187
+ ],
188
+ }),
189
+ ];
190
+ },
191
+ };
192
+
193
+ const activityProvider = {
194
+ id: 'bpmn-activity',
195
+ priority: 700,
196
+ appliesTo: (ctx) => ctx.kind === 'node' && activityKinds.has(ctx.definition?.kind),
197
+ getGroups() {
198
+ return [
199
+ propertyGroup({
200
+ id: 'multi-instance', label: '循环 / 多实例', collapsed: true, position: { after: 'basic' },
201
+ entries: [
202
+ propertyEntry({ id: 'loop-type', type: 'select', label: '循环类型', path: 'properties.loopType', options: [['none', '无'], ['standard', '标准循环'], ['parallel', '并行多实例'], ['sequential', '串行多实例']] }),
203
+ propertyEntry({ id: 'loop-condition', type: 'expression', label: 'Loop Condition', path: 'properties.loopCondition', placeholder: '${retry == true}', visible: (ctx) => ctx.get('properties.loopType') === 'standard' }),
204
+ propertyEntry({ id: 'loop-test-before', type: 'switch', label: 'Test Before', path: 'properties.testBefore', visible: (ctx) => ctx.get('properties.loopType') === 'standard' }),
205
+ propertyEntry({ id: 'loop-maximum', type: 'number', label: 'Loop Maximum', path: 'properties.loopMaximum', min: 0, visible: (ctx) => ctx.get('properties.loopType') === 'standard' }),
206
+ propertyEntry({ id: 'collection', label: 'Collection', path: 'properties.collection', placeholder: '${approvers}', visible: (ctx) => ['parallel', 'sequential'].includes(ctx.get('properties.loopType')) }),
207
+ propertyEntry({ id: 'element-variable', label: 'Element Variable', path: 'properties.elementVariable', placeholder: 'approver', visible: (ctx) => ['parallel', 'sequential'].includes(ctx.get('properties.loopType')) }),
208
+ propertyEntry({ id: 'element-index-variable', label: 'Element Index Variable', path: 'properties.elementIndexVariable', placeholder: 'index', visible: (ctx) => ['parallel', 'sequential'].includes(ctx.get('properties.loopType')) }),
209
+ propertyEntry({ id: 'loop-cardinality', type: 'expression', label: 'Loop Cardinality', path: 'properties.loopCardinality', placeholder: '${3}', visible: (ctx) => ['parallel', 'sequential'].includes(ctx.get('properties.loopType')) }),
210
+ propertyEntry({ id: 'completion-condition', type: 'expression', label: 'Completion Condition', path: 'properties.completionCondition', placeholder: '${nrOfCompletedInstances >= 2}', visible: (ctx) => ['parallel', 'sequential'].includes(ctx.get('properties.loopType')) }),
211
+ propertyEntry({ id: 'compensation', type: 'switch', label: '补偿活动', path: 'properties.isForCompensation' }),
212
+ ],
213
+ }),
214
+ ];
215
+ },
216
+ };
217
+
218
+ const scriptProvider = {
219
+ id: 'bpmn-script-task', priority: 720, appliesTo: { kind: 'node', nodeType: 'scriptTask' },
220
+ getGroups() { return [propertyGroup({ id: 'script', label: '脚本', position: { after: 'basic' }, entries: [
221
+ propertyEntry({ id: 'script-format', label: 'Script Format', path: 'properties.scriptFormat', placeholder: 'javascript / groovy' }),
222
+ propertyEntry({ id: 'script-body', type: 'code', label: 'Script', path: 'properties.script', rows: 9, placeholder: '// script body' }),
223
+ propertyEntry({ id: 'script-result', label: 'Result Variable', path: 'properties.resultVariable', placeholder: 'result' }),
224
+ ] })]; },
225
+ };
226
+
227
+ const callActivityProvider = {
228
+ id: 'bpmn-call-activity', priority: 720, appliesTo: { kind: 'node', nodeType: 'callActivity' },
229
+ getGroups() { return [propertyGroup({ id: 'call-activity', label: '调用流程', position: { after: 'basic' }, entries: [
230
+ propertyEntry({ id: 'called-element', type: 'process-select', label: 'Called Element', path: 'properties.calledElement', placeholder: 'Process_Sub_1', dataProvider: 'processes', allowCustom: true }),
231
+ propertyEntry({ id: 'called-business-key', type: 'expression', label: 'Business Key', path: 'properties.businessKey', placeholder: '${businessKey}' }),
232
+ propertyEntry({ id: 'inherit-business-key', type: 'switch', label: '继承 Business Key', path: 'properties.inheritBusinessKey' }),
233
+ propertyEntry({ id: 'inherit-variables', type: 'switch', label: '继承流程变量', path: 'properties.inheritVariables' }),
234
+ propertyEntry({ id: 'input-mapping', type: 'table', label: '输入参数 In', path: 'properties.inParameters', rowLabel: '输入映射', defaultRow: () => ({ source: '', sourceExpression: '', target: '' }), columns: [{ key: 'source', label: 'Source' }, { key: 'sourceExpression', label: 'Expression' }, { key: 'target', label: 'Target' }] }),
235
+ propertyEntry({ id: 'output-mapping', type: 'table', label: '输出参数 Out', path: 'properties.outParameters', rowLabel: '输出映射', defaultRow: () => ({ source: '', sourceExpression: '', target: '' }), columns: [{ key: 'source', label: 'Source' }, { key: 'sourceExpression', label: 'Expression' }, { key: 'target', label: 'Target' }] }),
236
+ ] })]; },
237
+ };
238
+
239
+ const messageTaskProvider = {
240
+ id: 'bpmn-message-task', priority: 725,
241
+ appliesTo: (ctx) => ctx.kind === 'node' && ['sendTask', 'receiveTask'].includes(ctx.element.type),
242
+ getGroups(context) {
243
+ const entries = [
244
+ propertyEntry({
245
+ id: 'message-ref', type: 'resource-select', label: 'Message Ref', path: 'properties.messageRef', resource: 'messages', allowCustom: true,
246
+ options: (ctx) => (ctx.model.resources?.messages || []).map((item) => ({ value: item.id, label: `${item.name || item.id} · ${item.id}` })),
247
+ placeholder: '选择或输入 Message ID',
248
+ }),
249
+ propertyEntry({ id: 'operation-ref', label: 'Operation Ref', path: 'properties.operationRef', placeholder: 'Operation_1' }),
250
+ ];
251
+ if (context.element.type === 'receiveTask') entries.push(propertyEntry({ id: 'instantiate-receive', type: 'switch', label: 'Instantiate', path: 'properties.instantiate' }));
252
+ return [propertyGroup({ id: 'message-task', label: context.element.type === 'sendTask' ? '消息发送' : '消息接收', position: { after: 'basic' }, entries })];
253
+ },
254
+ };
255
+
256
+ const subProcessProvider = {
257
+ id: 'bpmn-sub-process', priority: 710,
258
+ appliesTo: (ctx) => ctx.kind === 'node' && ctx.definition?.kind === 'container',
259
+ getGroups(context) {
260
+ const entries = [];
261
+ if (context.element.type === 'eventSubProcess') entries.push(propertyEntry({ id: 'triggered-by-event', type: 'readonly', label: 'Triggered By Event', value: 'true' }));
262
+ if (context.element.type === 'adHocSubProcess') entries.push(
263
+ propertyEntry({ id: 'adhoc-ordering', type: 'select', label: 'Ordering', path: 'properties.ordering', options: [['Parallel', 'Parallel'], ['Sequential', 'Sequential']] }),
264
+ propertyEntry({ id: 'adhoc-completion', type: 'expression', label: 'Completion Condition', path: 'properties.adHocCompletionCondition', placeholder: '${completed}' }),
265
+ propertyEntry({ id: 'adhoc-cancel', type: 'switch', label: 'Cancel Remaining Instances', path: 'properties.cancelRemainingInstances' }),
266
+ );
267
+ if (!entries.length) return [];
268
+ return [propertyGroup({ id: 'sub-process', label: '子流程行为', position: { after: 'basic' }, entries })];
269
+ },
270
+ };
271
+
272
+ const gatewayProvider = {
273
+ id: 'bpmn-gateway', priority: 760, appliesTo: { kind: 'node', elementKind: 'gateway' },
274
+ getGroups(context) {
275
+ const role = resolveGatewayRole(context.model, context.element);
276
+ const isParallel = context.element.type === 'parallelGateway';
277
+ const entries = [];
278
+ if (isParallel && context.profile === 'business') entries.push(propertyEntry({
279
+ id: 'parallel-role', type: 'select', label: '并行角色', path: 'properties.gatewayDirection',
280
+ options: (ctx) => [
281
+ ['Unspecified', '自动判断'], ['Diverging', '并行分支'], ['Converging', '并行汇聚'],
282
+ ...(ctx.element.properties?.gatewayDirection === 'Mixed' ? [['Mixed', '混合(开发者配置)']] : []),
283
+ ],
284
+ note: role.effective === 'Converging' ? '并行汇聚会等待所有入口到达后再继续。' : '',
285
+ }));
286
+ if (context.profile === 'developer' || !isParallel) entries.push(propertyEntry({ id: 'gateway-direction', type: 'select', label: 'Gateway Direction', path: 'properties.gatewayDirection', options: [['Unspecified', 'Unspecified'], ['Diverging', 'Diverging / 分支'], ['Converging', 'Converging / 汇聚'], ['Mixed', 'Mixed']] }));
287
+ if (context.element.type === 'eventBasedGateway') entries.push(
288
+ propertyEntry({ id: 'instantiate', type: 'switch', label: 'Instantiate', path: 'properties.instantiate' }),
289
+ propertyEntry({ id: 'event-gateway-type', type: 'select', label: 'Event Gateway Type', path: 'properties.eventGatewayType', options: [['Exclusive', 'Exclusive'], ['Parallel', 'Parallel']] }),
290
+ );
291
+ if (context.element.type === 'complexGateway') entries.push(propertyEntry({ id: 'activation-condition', type: 'expression', label: 'Activation Condition', path: 'properties.activationCondition' }));
292
+ const groups = [];
293
+ if (entries.length) groups.push(propertyGroup({ id: 'gateway', label: '网关配置', position: { after: 'basic' }, entries }));
294
+ if (!isParallel) {
295
+ groups.push(propertyGroup({
296
+ id: 'gateway-branches', label: `分支线路 · ${role.outgoing.length}`, collapsed: false, position: { after: 'gateway' },
297
+ description: '直接在网关面板维护出口名称、条件和默认线路;点击定位可选中对应 Sequence Flow。',
298
+ entries: [propertyEntry({
299
+ id: 'branches', type: 'branchList', label: '',
300
+ getValue: () => role.outgoing.map((edge) => ({ id: edge.id, name: edge.name || '', condition: edge.condition || '', isDefault: !!edge.isDefault, target: edge.target })),
301
+ setValue: (ctx, rows) => {
302
+ const patches = rows.map((row) => ({ id: row.id, patch: { name: row.name, condition: row.condition, isDefault: !!row.isDefault } }));
303
+ const defaultRow = rows.find((row) => row.isDefault);
304
+ if (defaultRow) {
305
+ for (const edge of role.outgoing) if (edge.id !== defaultRow.id && edge.isDefault) patches.push({ id: edge.id, patch: { isDefault: false } });
306
+ }
307
+ ctx.designer.updateEdges(patches);
308
+ },
309
+ selectRow: (ctx, row) => ctx.designer.select({ kind: 'edge', id: row.id }),
310
+ })],
311
+ }));
312
+ return groups;
313
+ }
314
+
315
+ let structureAfter = 'gateway';
316
+ if (role.effective === 'Diverging') {
317
+ groups.push(propertyGroup({
318
+ id: 'gateway-branches', label: `并行出口 · ${role.outgoing.length}`, collapsed: false, position: { after: 'gateway' },
319
+ description: '并行分支会同时激活所有出口;可维护线路名称或定位对应 Sequence Flow。',
320
+ entries: [
321
+ propertyEntry({
322
+ id: 'branches', type: 'branchList', label: '', fields: ['name'],
323
+ getValue: () => role.outgoing.map((edge) => ({ id: edge.id, name: edge.name || '', condition: edge.condition || '', isDefault: !!edge.isDefault, target: edge.target })),
324
+ setValue: (ctx, rows) => ctx.designer.updateEdges(rows.map((row) => ({ id: row.id, patch: { name: row.name } }))),
325
+ selectRow: (ctx, row) => ctx.designer.select({ kind: 'edge', id: row.id }),
326
+ }),
327
+ propertyEntry({ id: 'connect-parallel-branch', type: 'action', label: '连接已有节点', actionLabel: '连接并行分支', iconId: 'ui.connect', action: (ctx) => ctx.designer.startConnect(ctx.element.id) }),
328
+ ],
329
+ }));
330
+ structureAfter = 'gateway-branches';
331
+ } else {
332
+ const isJoin = role.effective === 'Converging';
333
+ groups.push(propertyGroup({
334
+ id: 'parallel-incoming', label: `${isJoin ? '汇聚入口' : '入口线路'} · ${role.incoming.length}`, collapsed: false, position: { after: 'gateway' },
335
+ description: isJoin ? '并行汇聚会等待所有入口到达后再继续。' : '当前角色尚未确定,仅展示已有入口线路。',
336
+ entries: [propertyEntry({ id: 'incoming-flows', type: 'flow-list', label: '', relation: 'source', emptyText: '当前并行网关没有入口线路', getValue: () => role.incoming })],
337
+ }));
338
+ groups.push(propertyGroup({
339
+ id: 'parallel-outgoing', label: `${isJoin ? '后续出口' : '出口线路'} · ${role.outgoing.length}`, collapsed: false, position: { after: 'parallel-incoming' },
340
+ description: isJoin ? '并行汇聚通常只连接一个后续节点。' : '当前角色尚未确定,仅展示已有出口线路。',
341
+ entries: [
342
+ propertyEntry({ id: 'outgoing-flows', type: 'flow-list', label: '', relation: 'target', emptyText: '当前并行网关没有出口线路', getValue: () => role.outgoing }),
343
+ ...(isJoin && role.outgoing.length === 0 ? [propertyEntry({ id: 'connect-parallel-next', type: 'action', label: '连接后续节点', actionLabel: '连接后续节点', iconId: 'ui.connect', action: (ctx) => ctx.designer.startConnect(ctx.element.id) })] : []),
344
+ ],
345
+ }));
346
+ structureAfter = 'parallel-outgoing';
347
+ }
348
+
349
+ const statusLabels = { valid: '结构正常', incomplete: '结构未完成', conflict: '结构冲突', ambiguous: '角色待判断' };
350
+ groups.push(propertyGroup({
351
+ id: 'parallel-structure', label: '结构校验', collapsed: false, position: { after: structureAfter },
352
+ entries: [
353
+ propertyEntry({ id: 'parallel-structure-summary', type: 'readonly', label: '线路结构', value: `${role.incoming.length} 入 · ${role.outgoing.length} 出` }),
354
+ propertyEntry({ id: 'parallel-structure-status', type: 'readonly', label: '状态', variant: role.status, value: statusLabels[role.status] }),
355
+ ...(role.issues.length ? [propertyEntry({ id: 'parallel-structure-issues', type: 'readonly', label: '提示', variant: role.status, value: role.issues.join(';') })] : []),
356
+ ],
357
+ }));
358
+ return groups;
359
+ },
360
+ };
361
+
362
+ const eventProvider = {
363
+ id: 'bpmn-events', priority: 750,
364
+ appliesTo: (ctx) => ctx.kind === 'node' && ['event', 'boundary'].includes(ctx.definition?.kind),
365
+ getGroups(context) {
366
+ const def = context.definition;
367
+ const entries = [];
368
+ if (def.eventDefinition === 'timer') entries.push(
369
+ propertyEntry({ id: 'timer-type', type: 'select', label: '定时类型', path: 'properties.timerType', options: [['duration', 'Duration'], ['date', 'Date'], ['cycle', 'Cycle']] }),
370
+ propertyEntry({ id: 'timer-value', type: 'expression', label: '时间表达式', path: 'properties.timerValue', placeholder: 'PT1H / 2026-08-26T09:00 / R3/PT10M' }),
371
+ );
372
+ if (def.eventDefinition === 'conditional') entries.push(propertyEntry({ id: 'event-condition', type: 'expression', label: '条件表达式', path: 'properties.condition', placeholder: '${approved == true}' }));
373
+ if (eventRefs[def.eventDefinition]) {
374
+ const meta = eventRefs[def.eventDefinition];
375
+ entries.push(propertyEntry({
376
+ id: 'event-ref', type: 'resource-select', label: meta.label, path: 'properties.eventRef', resource: meta.resource, allowCustom: true,
377
+ options: (ctx) => (ctx.model.resources?.[meta.resource] || []).map((item) => ({ value: item.id, label: `${item.name || item.id} · ${item.id}` })),
378
+ placeholder: '选择或输入资源 ID',
379
+ }));
380
+ }
381
+ if (def.eventDefinition === 'link') entries.push(propertyEntry({ id: 'link-name', label: 'Link Name', path: 'properties.eventRef', placeholder: 'Link_A' }));
382
+ if (def.eventDefinition === 'compensate') entries.push(
383
+ propertyEntry({ id: 'activity-ref', type: 'node-select', label: 'Activity Ref', path: 'properties.activityRef', nodeFilter: (node) => activityKinds.has(NODE_DEFINITIONS[node.type]?.kind) }),
384
+ propertyEntry({ id: 'wait-for-completion', type: 'switch', label: 'Wait For Completion', path: 'properties.waitForCompletion' }),
385
+ );
386
+ if ((def.eventStage === 'start' || (def.eventStage === 'intermediate' && def.eventRole === 'catch')) && ['message', 'signal'].includes(def.eventDefinition)) {
387
+ entries.push(propertyEntry({ id: 'parallel-multiple', type: 'switch', label: 'Parallel Multiple', path: 'properties.parallelMultiple' }));
388
+ }
389
+ if (!entries.length) entries.push(propertyEntry({ id: 'event-info', type: 'readonly', label: '事件定义', getValue: () => def.eventDefinition ? def.eventDefinition : 'None' }));
390
+ const groups = [propertyGroup({ id: 'event-definition', label: '事件定义', position: { after: 'basic' }, entries })];
391
+ if (def.kind === 'boundary') groups.push(propertyGroup({
392
+ id: 'boundary', label: '边界事件', position: { after: 'event-definition' }, entries: [
393
+ propertyEntry({ id: 'attached-to', type: 'node-select', label: '附着到活动', path: 'properties.attachedToRef', nodeFilter: (node) => activityKinds.has(NODE_DEFINITIONS[node.type]?.kind) }),
394
+ propertyEntry({ id: 'cancel-activity', type: 'switch', label: '中断主活动', path: 'properties.cancelActivity' }),
395
+ ],
396
+ }));
397
+ return groups;
398
+ },
399
+ };
400
+
401
+ const artifactProvider = {
402
+ id: 'bpmn-artifacts', priority: 730,
403
+ appliesTo: (ctx) => ctx.kind === 'node' && ['data', 'dataStore', 'annotation', 'group', 'participant', 'lane'].includes(ctx.definition?.kind),
404
+ getGroups(context) {
405
+ const type = context.element.type;
406
+ const entries = [];
407
+ if (type === 'textAnnotation') entries.push(propertyEntry({ id: 'annotation-text', type: 'textarea', label: '文本', path: 'properties.text', rows: 6 }), propertyEntry({ id: 'annotation-format', label: 'Text Format', path: 'properties.textFormat', placeholder: 'text/plain' }));
408
+ if (type === 'dataObjectReference') entries.push(propertyEntry({ id: 'data-object-ref', label: 'Data Object Ref', path: 'properties.dataObjectRef' }), propertyEntry({ id: 'data-state', label: 'Data State', path: 'properties.dataState' }));
409
+ if (type === 'dataStoreReference') entries.push(propertyEntry({ id: 'data-store-ref', label: 'Data Store Ref', path: 'properties.dataStoreRef' }), propertyEntry({ id: 'data-state', label: 'Data State', path: 'properties.dataState' }));
410
+ if (type === 'participant') entries.push(
411
+ propertyEntry({
412
+ id: 'swimlane-label-placement', type: 'select', label: '标题位置', path: 'properties.swimlaneLabelPlacement', defaultValue: 'side',
413
+ options: [['side', '左侧竖排'], ['top', '上方横排']],
414
+ note: '挂载到此 Pool 的 Lane 会继承该标题布局。',
415
+ }),
416
+ propertyEntry({ id: 'process-ref', label: 'Process Ref', path: 'properties.processRef' }),
417
+ );
418
+ if (type === 'lane') entries.push(
419
+ context.element.containerId
420
+ ? propertyEntry({
421
+ id: 'swimlane-label-placement-inherited', type: 'readonly', label: '标题位置',
422
+ getValue: (ctx) => resolveSwimlaneLabelPlacement(ctx.model, ctx.element) === 'top' ? '上方横排(跟随 Pool)' : '左侧竖排(跟随 Pool)',
423
+ note: '挂载 Lane 的标题布局由所属 Pool 统一控制。',
424
+ })
425
+ : propertyEntry({
426
+ id: 'swimlane-label-placement', type: 'select', label: '标题位置', path: 'properties.swimlaneLabelPlacement', defaultValue: 'side',
427
+ options: [['side', '左侧竖排'], ['top', '上方横排']],
428
+ note: '该设置仅影响编辑器展示,不写入标准 BPMN XML。',
429
+ }),
430
+ propertyEntry({ id: 'lane-members', type: 'node-list', label: '包含节点', getValue: (ctx) => ctx.element.properties?.flowNodeRefs || [], note: '节点拖入 Lane 后可由布局/containment 模块维护;此处用于检查当前引用。' }),
431
+ );
432
+ if (type === 'group') entries.push(propertyEntry({ id: 'category-value', label: 'Category Value', path: 'properties.categoryValue', placeholder: context.element.name }));
433
+ return entries.length ? [propertyGroup({ id: 'artifact', label: '元素配置', position: { after: 'basic' }, entries })] : [];
434
+ },
435
+ };
436
+
437
+
438
+
439
+ const nonSequenceEdgeProvider = {
440
+ id: 'bpmn-non-sequence-edge', priority: 845,
441
+ appliesTo: (ctx) => ctx.kind === 'edge' && ['messageFlow', 'association'].includes(ctx.element.type),
442
+ getGroups(context) {
443
+ const entries = context.element.type === 'messageFlow'
444
+ ? [propertyEntry({
445
+ id: 'message-flow-ref', type: 'resource-select', label: 'Message Ref', path: 'properties.messageRef', resource: 'messages', allowCustom: true,
446
+ options: (ctx) => (ctx.model.resources?.messages || []).map((item) => ({ value: item.id, label: `${item.name || item.id} · ${item.id}` })),
447
+ })]
448
+ : [propertyEntry({ id: 'association-direction', type: 'select', label: 'Association Direction', path: 'properties.associationDirection', options: [['None', 'None'], ['One', 'One'], ['Both', 'Both']] })];
449
+ return [propertyGroup({ id: 'edge-semantics', label: context.element.type === 'messageFlow' ? '消息流' : '关联', position: { after: 'basic' }, entries })];
450
+ },
451
+ };
452
+
453
+ const extensionElementsProvider = {
454
+ id: 'bpmn-extension-elements', priority: 110,
455
+ getGroups(context) {
456
+ return [propertyGroup({
457
+ id: 'extension-elements', label: 'Extension Elements', collapsed: true, position: { append: true },
458
+ description: '原始扩展 XML 会无损保留。推荐通过 Properties Provider + Engine/Extension serializer 管理结构化扩展;此处适合查看或维护少量未知扩展。',
459
+ entries: [propertyEntry({
460
+ id: 'raw-extension-elements', type: 'table', label: 'Raw XML',
461
+ getValue: (ctx) => (ctx.get('extensionElements', []) || []).map((xml) => ({ xml })),
462
+ setValue: (ctx, rows) => ctx.set('extensionElements', (rows || []).map((row) => row.xml).filter(Boolean)),
463
+ rowLabel: 'Extension Element', defaultRow: () => ({ xml: '<company:property value="" />' }), columns: [{ key: 'xml', label: 'XML' }],
464
+ })],
465
+ })];
466
+ },
467
+ };
468
+
469
+ const extensionAttributesProvider = {
470
+ id: 'bpmn-extension-attributes', priority: 120,
471
+ getGroups(context) {
472
+ const rootPath = 'properties.extensionAttributes';
473
+ return [propertyGroup({
474
+ id: 'extension-attributes', label: '扩展属性', collapsed: true, position: { append: true },
475
+ description: '用于企业自定义 namespace attribute。请同时在 Process 的 namespaceDeclarations 中注册对应 xmlns 前缀。未知 Extension Elements 在导入/导出时仍会保留。',
476
+ entries: [propertyEntry({
477
+ id: 'extension-attribute-table', type: 'table', label: 'Custom Attributes',
478
+ getValue: (ctx) => Object.entries(ctx.get(rootPath, {}) || {}).map(([name, value]) => ({ name, value })),
479
+ setValue: (ctx, rows) => ctx.set(rootPath, Object.fromEntries((rows || []).filter((row) => row.name).map((row) => [row.name, row.value || '']))),
480
+ rowLabel: '属性', defaultRow: () => ({ name: 'company:property', value: '' }), columns: [{ key: 'name', label: 'QName' }, { key: 'value', label: 'Value' }],
481
+ })],
482
+ })];
483
+ },
484
+ };
485
+
486
+ const destructiveActionsProvider = {
487
+ id: 'bpmn-actions', priority: 20,
488
+ appliesTo: (ctx) => ctx.kind === 'node' || ctx.kind === 'edge',
489
+ getGroups() {
490
+ return [propertyGroup({
491
+ id: 'element-actions', label: '元素操作', collapsed: true, position: { append: true },
492
+ entries: [propertyEntry({ id: 'delete-element', type: 'action', label: '删除当前元素', actionLabel: '删除元素', variant: 'danger', action: (ctx) => ctx.studio.commands.remove() })],
493
+ })];
494
+ },
495
+ };
496
+ export const bpmnPropertiesProviders = [
497
+ basicProvider,
498
+ processProvider,
499
+ sequenceFlowProvider,
500
+ gatewayProvider,
501
+ eventProvider,
502
+ scriptProvider,
503
+ callActivityProvider,
504
+ messageTaskProvider,
505
+ subProcessProvider,
506
+ artifactProvider,
507
+ activityProvider,
508
+ documentationProvider,
509
+ nonSequenceEdgeProvider,
510
+ extensionAttributesProvider,
511
+ extensionElementsProvider,
512
+ destructiveActionsProvider,
513
+ ];
514
+
515
+ export function registerBpmnProperties(registry) {
516
+ const disposers = bpmnPropertiesProviders.map((provider) => registry.registerProvider(provider.priority ?? 500, provider));
517
+ return () => disposers.forEach((dispose) => dispose());
518
+ }