@pbinitiative/zenbpm-js-properties-panel 0.2.0 → 0.3.1

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/dist/index.mjs CHANGED
@@ -3,15 +3,14 @@ import { createElement } from '@bpmn-io/properties-panel/preact';
3
3
  import { useService } from 'bpmn-js-properties-panel';
4
4
 
5
5
  function ZenFormProps(element) {
6
- if (element.type !== 'bpmn:UserTask') {
6
+ if (element.type !== 'bpmn:UserTask')
7
7
  return [];
8
- }
9
8
  return [
10
9
  {
11
10
  id: 'zenFormDesignButton',
12
11
  component: ZenFormDesignButtonEntry,
13
12
  isEdited: () => false,
14
- }
13
+ },
15
14
  ];
16
15
  }
17
16
  function getZenFormValue(element) {
@@ -25,7 +24,6 @@ function getZenFormValue(element) {
25
24
  const input = (ioMapping.inputParameters || []).find((p) => p.target === 'ZEN_FORM');
26
25
  if (!input?.source)
27
26
  return '';
28
- // Parse FEEL string literal: ="..." → raw JSON
29
27
  const src = input.source;
30
28
  if (src.startsWith('="') && src.endsWith('"')) {
31
29
  return src.slice(2, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
@@ -49,6 +47,137 @@ function ZenFormDesignButtonEntry(props) {
49
47
  'font-size: 13px; font-weight: 500;',
50
48
  }, translate('Design Form')));
51
49
  }
50
+ // ─── Form variable scanning ──────────────────────────────────────────────────
51
+ function extractFormKeys(components) {
52
+ const keys = [];
53
+ for (const comp of components || []) {
54
+ if (comp.key)
55
+ keys.push(comp.key);
56
+ if (comp.components)
57
+ keys.push(...extractFormKeys(comp.components));
58
+ if (comp.rows) {
59
+ for (const row of comp.rows) {
60
+ if (Array.isArray(row))
61
+ keys.push(...extractFormKeys(row));
62
+ }
63
+ }
64
+ if (comp.columns) {
65
+ for (const col of comp.columns) {
66
+ if (col.components)
67
+ keys.push(...extractFormKeys(col.components));
68
+ }
69
+ }
70
+ }
71
+ return keys;
72
+ }
73
+ function scanFormVariables(formJson) {
74
+ try {
75
+ const schema = JSON.parse(formJson);
76
+ return extractFormKeys(schema.components || []);
77
+ }
78
+ catch {
79
+ console.warn('[ZenBPM] Failed to parse form JSON for variable scanning');
80
+ return [];
81
+ }
82
+ }
83
+ /**
84
+ * Sync output mappings with current form fields.
85
+ * - Form fields without an existing output get a default one.
86
+ * - Existing outputs with the same source are kept, preserving user's target.
87
+ * - Outputs for removed form fields are dropped.
88
+ */
89
+ function syncOutputMappings(element, injector, variableKeys) {
90
+ const commandStack = injector.get('commandStack');
91
+ const bpmnFactory = injector.get('bpmnFactory');
92
+ const bo = element.businessObject;
93
+ let extensionElements = bo.extensionElements;
94
+ const commands = [];
95
+ if (!extensionElements) {
96
+ extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
97
+ values: [],
98
+ });
99
+ extensionElements.$parent = bo;
100
+ commands.push({
101
+ cmd: 'element.updateModdleProperties',
102
+ context: {
103
+ element,
104
+ moddleElement: bo,
105
+ properties: { extensionElements },
106
+ },
107
+ });
108
+ }
109
+ let ioMapping = (extensionElements.values || []).find((e) => e.$instanceOf('zenbpm:IoMapping'));
110
+ if (!ioMapping) {
111
+ ioMapping = bpmnFactory.create('zenbpm:IoMapping', {
112
+ inputParameters: [],
113
+ outputParameters: [],
114
+ });
115
+ ioMapping.$parent = extensionElements;
116
+ commands.push({
117
+ cmd: 'element.updateModdleProperties',
118
+ context: {
119
+ element,
120
+ moddleElement: extensionElements,
121
+ properties: {
122
+ values: [...(extensionElements.values || []), ioMapping],
123
+ },
124
+ },
125
+ });
126
+ }
127
+ // Index existing outputs by source
128
+ const existingBySource = new Map((ioMapping.outputParameters || []).map((o) => [o.source, o]));
129
+ // For each form field, produce an output — reusing existing one if available
130
+ const outputs = variableKeys.map((key) => {
131
+ const source = `=${key}`;
132
+ const existing = existingBySource.get(source);
133
+ if (existing)
134
+ return existing;
135
+ const output = bpmnFactory.create('zenbpm:Output', {
136
+ source,
137
+ target: key,
138
+ });
139
+ output.$parent = ioMapping;
140
+ return output;
141
+ });
142
+ commands.push({
143
+ cmd: 'element.updateModdleProperties',
144
+ context: {
145
+ element,
146
+ moddleElement: ioMapping,
147
+ properties: { outputParameters: outputs },
148
+ },
149
+ });
150
+ commandStack.execute('properties-panel.multi-command-executor', commands);
151
+ }
152
+ // ─── Form save handler ───────────────────────────────────────────────────────
153
+ const lastFormValueByElement = new Map();
154
+ function setupFormSaveHandler(injector) {
155
+ const eventBus = injector.get('eventBus');
156
+ eventBus.on('commandStack.element.updateModdleProperties.executed', (event) => {
157
+ const { context } = event;
158
+ if (!context)
159
+ return;
160
+ const { moddleElement, properties, element } = context;
161
+ if (moddleElement?.$type !== 'zenbpm:Input' ||
162
+ moddleElement.target !== 'ZEN_FORM' ||
163
+ properties?.source === undefined) {
164
+ return;
165
+ }
166
+ if (!element || element.type !== 'bpmn:UserTask')
167
+ return;
168
+ // Defer to avoid nested commandStack.execute() while stack is mid-execution
169
+ setTimeout(() => {
170
+ const formJson = getZenFormValue(element);
171
+ if (!formJson)
172
+ return;
173
+ if (lastFormValueByElement.get(element.id) === formJson)
174
+ return;
175
+ lastFormValueByElement.set(element.id, formJson);
176
+ const variableKeys = scanFormVariables(formJson);
177
+ syncOutputMappings(element, injector, variableKeys);
178
+ }, 0);
179
+ });
180
+ }
52
181
 
53
182
  /**
54
183
  * Return the first extension element of `type` from the given business object,
@@ -331,11 +460,11 @@ function bindingEntries(idPrefix, bindingTypeComponent, versionTagComponent, ele
331
460
  return entries;
332
461
  }
333
462
 
334
- const TYPE$2 = 'zenbpm:CalledElement';
335
- const ID$1 = 'zenbpm-calledEl';
463
+ const TYPE$3 = 'zenbpm:CalledElement';
464
+ const ID$2 = 'zenbpm-calledEl';
336
465
  // Module-level component instances — stable references, never recreated on render.
337
- const BindingTypeEntry$1 = makeBindingTypeEntry(ID$1, TYPE$2);
338
- const BindingVersionTagEntry$1 = makeVersionTagEntry(ID$1, TYPE$2);
466
+ const BindingTypeEntry$1 = makeBindingTypeEntry(ID$2, TYPE$3);
467
+ const BindingVersionTagEntry$1 = makeVersionTagEntry(ID$2, TYPE$3);
339
468
  // ─── entry components ────────────────────────────────────────────────────────
340
469
  function ProcessIdEntry(props) {
341
470
  const { element } = props;
@@ -344,9 +473,9 @@ function ProcessIdEntry(props) {
344
473
  const translate = useService('translate');
345
474
  const debounce = useService('debounceInput');
346
475
  const bo = element.businessObject;
347
- const getValue = () => getExtensionElement(bo, TYPE$2)?.processId ?? '';
348
- const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$2, { processId: value }, bpmnFactory, commandStack);
349
- return TextFieldEntry({ element, id: `${ID$1}-processId`, label: translate('Process ID'), getValue, setValue, debounce });
476
+ const getValue = () => getExtensionElement(bo, TYPE$3)?.processId ?? '';
477
+ const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$3, { processId: value }, bpmnFactory, commandStack);
478
+ return TextFieldEntry({ element, id: `${ID$2}-processId`, label: translate('Process ID'), getValue, setValue, debounce });
350
479
  }
351
480
  function PropagateAllChildVarsEntry(props) {
352
481
  const { element } = props;
@@ -354,9 +483,9 @@ function PropagateAllChildVarsEntry(props) {
354
483
  const bpmnFactory = useService('bpmnFactory');
355
484
  const translate = useService('translate');
356
485
  const bo = element.businessObject;
357
- const getValue = () => getExtensionElement(bo, TYPE$2)?.propagateAllChildVariables ?? false;
358
- const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$2, { propagateAllChildVariables: value }, bpmnFactory, commandStack);
359
- return ToggleSwitchEntry({ element, id: `${ID$1}-propagateAllChildVariables`, label: translate('Propagate all child variables'), getValue, setValue });
486
+ const getValue = () => getExtensionElement(bo, TYPE$3)?.propagateAllChildVariables ?? false;
487
+ const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$3, { propagateAllChildVariables: value }, bpmnFactory, commandStack);
488
+ return ToggleSwitchEntry({ element, id: `${ID$2}-propagateAllChildVariables`, label: translate('Propagate all child variables'), getValue, setValue });
360
489
  }
361
490
  function PropagateAllParentVarsEntry(props) {
362
491
  const { element } = props;
@@ -364,27 +493,27 @@ function PropagateAllParentVarsEntry(props) {
364
493
  const bpmnFactory = useService('bpmnFactory');
365
494
  const translate = useService('translate');
366
495
  const bo = element.businessObject;
367
- const getValue = () => getExtensionElement(bo, TYPE$2)?.propagateAllParentVariables ?? true;
368
- const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$2, { propagateAllParentVariables: value }, bpmnFactory, commandStack);
369
- return ToggleSwitchEntry({ element, id: `${ID$1}-propagateAllParentVariables`, label: translate('Propagate all parent variables'), getValue, setValue });
496
+ const getValue = () => getExtensionElement(bo, TYPE$3)?.propagateAllParentVariables ?? true;
497
+ const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$3, { propagateAllParentVariables: value }, bpmnFactory, commandStack);
498
+ return ToggleSwitchEntry({ element, id: `${ID$2}-propagateAllParentVariables`, label: translate('Propagate all parent variables'), getValue, setValue });
370
499
  }
371
500
  // ─── exported entry list ─────────────────────────────────────────────────────
372
501
  function CalledElementProps(element) {
373
502
  if (element.type !== 'bpmn:CallActivity')
374
503
  return [];
375
504
  return [
376
- { id: `${ID$1}-processId`, component: ProcessIdEntry, isEdited: isTextFieldEntryEdited },
377
- ...bindingEntries(ID$1, BindingTypeEntry$1, BindingVersionTagEntry$1, element, TYPE$2),
378
- { id: `${ID$1}-propagateAllChildVariables`, component: PropagateAllChildVarsEntry, isEdited: isToggleSwitchEntryEdited },
379
- { id: `${ID$1}-propagateAllParentVariables`, component: PropagateAllParentVarsEntry, isEdited: isToggleSwitchEntryEdited },
505
+ { id: `${ID$2}-processId`, component: ProcessIdEntry, isEdited: isTextFieldEntryEdited },
506
+ ...bindingEntries(ID$2, BindingTypeEntry$1, BindingVersionTagEntry$1, element, TYPE$3),
507
+ { id: `${ID$2}-propagateAllChildVariables`, component: PropagateAllChildVarsEntry, isEdited: isToggleSwitchEntryEdited },
508
+ { id: `${ID$2}-propagateAllParentVariables`, component: PropagateAllParentVarsEntry, isEdited: isToggleSwitchEntryEdited },
380
509
  ];
381
510
  }
382
511
 
383
- const TYPE$1 = 'zenbpm:CalledDecision';
384
- const ID = 'zenbpm-calledDecision';
512
+ const TYPE$2 = 'zenbpm:CalledDecision';
513
+ const ID$1 = 'zenbpm-calledDecision';
385
514
  // Module-level component instances — stable references, never recreated on render.
386
- const BindingTypeEntry = makeBindingTypeEntry(ID, TYPE$1);
387
- const BindingVersionTagEntry = makeVersionTagEntry(ID, TYPE$1);
515
+ const BindingTypeEntry = makeBindingTypeEntry(ID$1, TYPE$2);
516
+ const BindingVersionTagEntry = makeVersionTagEntry(ID$1, TYPE$2);
388
517
  // ─── entry components ────────────────────────────────────────────────────────
389
518
  function DecisionIdEntry(props) {
390
519
  const { element } = props;
@@ -393,9 +522,9 @@ function DecisionIdEntry(props) {
393
522
  const translate = useService('translate');
394
523
  const debounce = useService('debounceInput');
395
524
  const bo = element.businessObject;
396
- const getValue = () => getExtensionElement(bo, TYPE$1)?.decisionId ?? '';
397
- const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$1, { decisionId: value }, bpmnFactory, commandStack);
398
- return TextFieldEntry({ element, id: `${ID}-decisionId`, label: translate('Decision ID'), getValue, setValue, debounce });
525
+ const getValue = () => getExtensionElement(bo, TYPE$2)?.decisionId ?? '';
526
+ const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$2, { decisionId: value }, bpmnFactory, commandStack);
527
+ return TextFieldEntry({ element, id: `${ID$1}-decisionId`, label: translate('Decision ID'), getValue, setValue, debounce });
399
528
  }
400
529
  function ResultVariableEntry(props) {
401
530
  const { element } = props;
@@ -404,18 +533,18 @@ function ResultVariableEntry(props) {
404
533
  const translate = useService('translate');
405
534
  const debounce = useService('debounceInput');
406
535
  const bo = element.businessObject;
407
- const getValue = () => getExtensionElement(bo, TYPE$1)?.resultVariable ?? '';
408
- const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$1, { resultVariable: value }, bpmnFactory, commandStack);
409
- return TextFieldEntry({ element, id: `${ID}-resultVariable`, label: translate('Result variable'), getValue, setValue, debounce });
536
+ const getValue = () => getExtensionElement(bo, TYPE$2)?.resultVariable ?? '';
537
+ const setValue = (value) => updateExtensionElementProps(element, bo, TYPE$2, { resultVariable: value }, bpmnFactory, commandStack);
538
+ return TextFieldEntry({ element, id: `${ID$1}-resultVariable`, label: translate('Result variable'), getValue, setValue, debounce });
410
539
  }
411
540
  // ─── exported entry list ─────────────────────────────────────────────────────
412
541
  function CalledDecisionProps(element) {
413
542
  if (element.type !== 'bpmn:BusinessRuleTask')
414
543
  return [];
415
544
  return [
416
- { id: `${ID}-decisionId`, component: DecisionIdEntry, isEdited: isTextFieldEntryEdited },
417
- ...bindingEntries(ID, BindingTypeEntry, BindingVersionTagEntry, element, TYPE$1),
418
- { id: `${ID}-resultVariable`, component: ResultVariableEntry, isEdited: isTextFieldEntryEdited },
545
+ { id: `${ID$1}-decisionId`, component: DecisionIdEntry, isEdited: isTextFieldEntryEdited },
546
+ ...bindingEntries(ID$1, BindingTypeEntry, BindingVersionTagEntry, element, TYPE$2),
547
+ { id: `${ID$1}-resultVariable`, component: ResultVariableEntry, isEdited: isTextFieldEntryEdited },
419
548
  ];
420
549
  }
421
550
 
@@ -499,7 +628,7 @@ function VersionTagProps(element) {
499
628
  ];
500
629
  }
501
630
 
502
- const TYPE = 'zenbpm:LoopCharacteristics';
631
+ const TYPE$1 = 'zenbpm:LoopCharacteristics';
503
632
  // ─── helpers ─────────────────────────────────────────────────────────────────
504
633
  /**
505
634
  * Return the bpmn:MultiInstanceLoopCharacteristics of an element, or null.
@@ -512,7 +641,7 @@ function getMultiInstanceLoopCharacteristics(element) {
512
641
  }
513
642
  function getZenbpmLoopCharacteristics(element) {
514
643
  const lc = getMultiInstanceLoopCharacteristics(element);
515
- return lc ? getExtensionElement(lc, TYPE) : undefined;
644
+ return lc ? getExtensionElement(lc, TYPE$1) : undefined;
516
645
  }
517
646
  // ─── entry components ────────────────────────────────────────────────────────
518
647
  /**
@@ -526,7 +655,7 @@ function InputCollectionEntry(props) {
526
655
  const debounce = useService('debounceInput');
527
656
  const lc = getMultiInstanceLoopCharacteristics(element);
528
657
  const getValue = () => getZenbpmLoopCharacteristics(element)?.inputCollection ?? '';
529
- const setValue = (value) => updateExtensionElementProps(element, lc, TYPE, { inputCollection: value }, bpmnFactory, commandStack);
658
+ const setValue = (value) => updateExtensionElementProps(element, lc, TYPE$1, { inputCollection: value }, bpmnFactory, commandStack);
530
659
  return FeelEntry({
531
660
  element,
532
661
  id: 'zenbpm-multiInstance-inputCollection',
@@ -548,7 +677,7 @@ function InputElementEntry(props) {
548
677
  const debounce = useService('debounceInput');
549
678
  const lc = getMultiInstanceLoopCharacteristics(element);
550
679
  const getValue = () => getZenbpmLoopCharacteristics(element)?.inputElement ?? '';
551
- const setValue = (value) => updateExtensionElementProps(element, lc, TYPE, { inputElement: value }, bpmnFactory, commandStack);
680
+ const setValue = (value) => updateExtensionElementProps(element, lc, TYPE$1, { inputElement: value }, bpmnFactory, commandStack);
552
681
  return TextFieldEntry({
553
682
  element,
554
683
  id: 'zenbpm-multiInstance-inputElement',
@@ -569,7 +698,7 @@ function OutputCollectionEntry(props) {
569
698
  const debounce = useService('debounceInput');
570
699
  const lc = getMultiInstanceLoopCharacteristics(element);
571
700
  const getValue = () => getZenbpmLoopCharacteristics(element)?.outputCollection ?? '';
572
- const setValue = (value) => updateExtensionElementProps(element, lc, TYPE, { outputCollection: value }, bpmnFactory, commandStack);
701
+ const setValue = (value) => updateExtensionElementProps(element, lc, TYPE$1, { outputCollection: value }, bpmnFactory, commandStack);
573
702
  return TextFieldEntry({
574
703
  element,
575
704
  id: 'zenbpm-multiInstance-outputCollection',
@@ -590,7 +719,7 @@ function OutputElementEntry(props) {
590
719
  const debounce = useService('debounceInput');
591
720
  const lc = getMultiInstanceLoopCharacteristics(element);
592
721
  const getValue = () => getZenbpmLoopCharacteristics(element)?.outputElement ?? '';
593
- const setValue = (value) => updateExtensionElementProps(element, lc, TYPE, { outputElement: value }, bpmnFactory, commandStack);
722
+ const setValue = (value) => updateExtensionElementProps(element, lc, TYPE$1, { outputElement: value }, bpmnFactory, commandStack);
594
723
  return FeelEntry({
595
724
  element,
596
725
  id: 'zenbpm-multiInstance-outputElement',
@@ -788,6 +917,107 @@ function ConditionExpressionProps(element) {
788
917
  ];
789
918
  }
790
919
 
920
+ const TYPE = 'zenbpm:Subscription';
921
+ const ID = 'zenbpm-messageSubscriptionCorrelationKey';
922
+ // ─── helpers ─────────────────────────────────────────────────────────────────
923
+ /**
924
+ * Return the bpmn:Message associated with the given diagram element, or
925
+ * undefined if the element has no message (and therefore no subscription).
926
+ *
927
+ * ZenBPM only considers the following elements to be message subscribers:
928
+ * - bpmn:IntermediateCatchEvent (with bpmn:MessageEventDefinition)
929
+ * - bpmn:BoundaryEvent (with bpmn:MessageEventDefinition)
930
+ * - bpmn:StartEvent (only inside an event sub-process)
931
+ * ReceiveTask / EndEvent / IntermediateThrowEvent are not subscription points.
932
+ */
933
+ function getMessage(element) {
934
+ const bo = element.businessObject;
935
+ if (!bo) {
936
+ return undefined;
937
+ }
938
+ const eventDefinitions = bo.eventDefinitions || [];
939
+ for (const def of eventDefinitions) {
940
+ if (def.$type === 'bpmn:MessageEventDefinition') {
941
+ return def.get('messageRef');
942
+ }
943
+ }
944
+ return undefined;
945
+ }
946
+ /**
947
+ * Eligibility for the subscription correlation key field, derived from how the
948
+ * ZenBPM engine actually consumes the value at runtime:
949
+ *
950
+ * - bpmn:IntermediateCatchEvent / bpmn:BoundaryEvent → yes
951
+ * Engine creates a TokenMessageSubscription that uses the key for matching.
952
+ *
953
+ * - bpmn:StartEvent inside an event sub-process only → yes
954
+ * Engine creates an InstanceMessageSubscription that uses the key.
955
+ *
956
+ * - bpmn:StartEvent at the process root → no
957
+ * Engine creates a DefinitionMessageSubscription that ignores the key.
958
+ *
959
+ * - bpmn:ReceiveTask → no
960
+ * Not supported by the ZenBPM engine (deployment error).
961
+ *
962
+ * - bpmn:EndEvent / bpmn:IntermediateThrowEvent → no
963
+ * Throw events are job-based, not subscription-based.
964
+ */
965
+ function canHaveSubscriptionCorrelationKey(element) {
966
+ const bo = element.businessObject;
967
+ if (!bo) {
968
+ return false;
969
+ }
970
+ if (bo.$type === 'bpmn:IntermediateCatchEvent' || bo.$type === 'bpmn:BoundaryEvent') {
971
+ return !!getMessage(element);
972
+ }
973
+ if (bo.$type === 'bpmn:StartEvent') {
974
+ const parentBo = element.parent?.businessObject;
975
+ return !!parentBo && parentBo.$type === 'bpmn:SubProcess' && !!parentBo.triggeredByEvent;
976
+ }
977
+ return false;
978
+ }
979
+ // ─── entry component ────────────────────────────────────────────────────────
980
+ function MessageSubscriptionCorrelationKeyEntry(props) {
981
+ const { element } = props;
982
+ const commandStack = useService('commandStack');
983
+ const bpmnFactory = useService('bpmnFactory');
984
+ const translate = useService('translate');
985
+ const debounce = useService('debounceInput');
986
+ // The subscription lives on the referenced bpmn:Message, not on the
987
+ // diagram element itself — this matches the zeebe:Subscription behaviour.
988
+ // `message` can become undefined at render time if the user unlinks the
989
+ // message after the entry is already mounted, so guard every access.
990
+ const message = getMessage(element);
991
+ const getValue = () => message
992
+ ? getFeelValue(getExtensionElement(message, TYPE)?.correlationKey)
993
+ : '';
994
+ const setValue = (value) => {
995
+ if (!message) {
996
+ return;
997
+ }
998
+ updateExtensionElementProps(element, message, TYPE, { correlationKey: value }, bpmnFactory, commandStack);
999
+ };
1000
+ return FeelEntry({
1001
+ element,
1002
+ id: ID,
1003
+ label: translate('Subscription correlation key'),
1004
+ feel: 'required',
1005
+ getValue,
1006
+ setValue,
1007
+ debounce,
1008
+ });
1009
+ }
1010
+ // ─── exported entry list ─────────────────────────────────────────────────────
1011
+ function CorrelationKeyProps(element) {
1012
+ if (!canHaveSubscriptionCorrelationKey(element))
1013
+ return [];
1014
+ if (!getMessage(element))
1015
+ return [];
1016
+ return [
1017
+ { id: ID, component: MessageSubscriptionCorrelationKeyEntry, isEdited: isFeelEntryEdited },
1018
+ ];
1019
+ }
1020
+
791
1021
  const PROVIDER_PRIORITY = 500;
792
1022
  class ZenBpmPropertiesProvider {
793
1023
  static $inject = ['propertiesPanel', 'injector'];
@@ -795,6 +1025,9 @@ class ZenBpmPropertiesProvider {
795
1025
  constructor(propertiesPanel, injector) {
796
1026
  this._injector = injector;
797
1027
  propertiesPanel.registerProvider(PROVIDER_PRIORITY, this);
1028
+ // When the Zen Form editor is submitted, scan form field variables
1029
+ // and automatically add them to the output mapping.
1030
+ setupFormSaveHandler(injector);
798
1031
  }
799
1032
  getGroups(element) {
800
1033
  return (groups) => {
@@ -875,6 +1108,24 @@ class ZenBpmPropertiesProvider {
875
1108
  });
876
1109
  }
877
1110
  }
1111
+ // ── Message subscription correlation key ────────────────────────────
1112
+ // Appended to the standard 'message' group (created by bpmn-js-properties-panel)
1113
+ // so it sits right under the message name, mirroring the zeebe:Subscription UX.
1114
+ const correlationKeyEntries = CorrelationKeyProps(element);
1115
+ if (correlationKeyEntries.length) {
1116
+ const messageGroup = groups.find((g) => g.id === 'message');
1117
+ if (messageGroup) {
1118
+ messageGroup.entries = [...messageGroup.entries, ...correlationKeyEntries];
1119
+ }
1120
+ else {
1121
+ groups.push({
1122
+ id: 'message',
1123
+ label: translate('Message'),
1124
+ entries: correlationKeyEntries,
1125
+ component: Group,
1126
+ });
1127
+ }
1128
+ }
878
1129
  // ── Condition expression ─────────────────────────────────────────────
879
1130
  // The standard bpmn-js-properties-panel already adds a 'conditionExpression'
880
1131
  // entry to the 'condition' group. We replace the entire group so that only