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