Skip to content

Commit c4b12bc

Browse files
maffbarmac
authored andcommitted
feat: add completion configuration support for ad-hoc subprocesses
Related to camunda/camunda-modeler#4850
1 parent 0a17b8c commit c4b12bc

File tree

11 files changed

+1247
-1
lines changed

11 files changed

+1247
-1
lines changed

src/contextProvider/zeebe/TooltipProvider.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,18 @@ const TooltipProvider = {
349349
</div>
350350
);
351351
},
352+
'group-adHocCompletion': (element) => {
353+
const translate = useService('translate');
354+
355+
return (
356+
<div>
357+
{translate('Define the completion behavior of an ad-hoc subprocess. If no completion condition is set, it will be completed after all active elements have been completed. ')}
358+
<a href="https://docs.camunda.io/docs/components/modeler/bpmn/ad-hoc" target="_blank" rel="noopener noreferrer" title={ translate('Ad-hoc subprocess documentation') }>
359+
{ translate('Learn more.') }
360+
</a>
361+
</div>
362+
);
363+
},
352364
'group-activeElements': (element) => {
353365
const translate = useService('translate');
354366

src/provider/bpmn/BpmnPropertiesProvider.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Group } from '@bpmn-io/properties-panel';
22

33
import {
4+
AdHocCompletionProps,
45
CompensationProps,
56
DocumentationProps,
67
ErrorProps,
@@ -195,6 +196,24 @@ function MultiInstanceGroup(element, injector) {
195196
return null;
196197
}
197198

199+
function AdHocCompletionGroup(element, injector) {
200+
const translate = injector.get('translate');
201+
const group = {
202+
label: translate('Completion'),
203+
id: 'adHocCompletion',
204+
component: Group,
205+
entries: [
206+
...AdHocCompletionProps({ element })
207+
]
208+
};
209+
210+
if (group.entries.length) {
211+
return group;
212+
}
213+
214+
return null;
215+
}
216+
198217
function getGroups(element, injector) {
199218

200219
const groups = [
@@ -205,6 +224,7 @@ function getGroups(element, injector) {
205224
LinkGroup(element, injector),
206225
MessageGroup(element, injector),
207226
MultiInstanceGroup(element, injector),
227+
AdHocCompletionGroup(element, injector),
208228
SignalGroup(element, injector),
209229
EscalationGroup(element, injector),
210230
TimerGroup(element, injector)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { is, getBusinessObject } from 'bpmn-js/lib/util/ModelUtil';
2+
3+
import {
4+
CheckboxEntry,
5+
isTextFieldEntryEdited,
6+
TextFieldEntry,
7+
} from '@bpmn-io/properties-panel';
8+
9+
import { createElement } from '../../../utils/ElementUtil';
10+
11+
import { useService } from '../../../hooks';
12+
13+
/**
14+
* @typedef { import('@bpmn-io/properties-panel').EntryDefinition } Entry
15+
*/
16+
17+
/**
18+
* @returns {Array<Entry>} entries
19+
*/
20+
export function AdHocCompletionProps(props) {
21+
const { element } = props;
22+
23+
if (!is(element, 'bpmn:AdHocSubProcess')) {
24+
return [];
25+
}
26+
27+
return [
28+
{
29+
id: 'completionCondition',
30+
component: CompletionCondition,
31+
isEdited: isTextFieldEntryEdited,
32+
},
33+
{
34+
id: 'cancelRemainingInstances',
35+
component: CancelRemainingInstances,
36+
isEdited: (node) => node && !node.checked // the default value is true
37+
},
38+
];
39+
}
40+
41+
function CompletionCondition(props) {
42+
const { element } = props;
43+
44+
const bpmnFactory = useService('bpmnFactory');
45+
const debounce = useService('debounceInput');
46+
const commandStack = useService('commandStack');
47+
const translate = useService('translate');
48+
49+
const getValue = () => {
50+
const expression = getBusinessObject(element).get('completionCondition');
51+
return expression && expression.get('body');
52+
};
53+
54+
const setValue = (value) => {
55+
return commandStack.execute(
56+
'element.updateModdleProperties',
57+
updateFormalExpression(element, 'completionCondition', value, bpmnFactory)
58+
);
59+
};
60+
61+
return TextFieldEntry({
62+
element,
63+
id: 'completionCondition',
64+
label: translate('Completion condition'),
65+
getValue,
66+
setValue,
67+
debounce
68+
});
69+
}
70+
71+
function CancelRemainingInstances(props) {
72+
const { element } = props;
73+
74+
const commandStack = useService('commandStack');
75+
const translate = useService('translate');
76+
77+
const businessObject = getBusinessObject(element);
78+
79+
const getValue = () => {
80+
return businessObject.get('cancelRemainingInstances');
81+
};
82+
83+
const setValue = (value) => {
84+
commandStack.execute('element.updateModdleProperties', {
85+
element,
86+
moddleElement: businessObject,
87+
properties: {
88+
cancelRemainingInstances: value,
89+
},
90+
});
91+
};
92+
93+
return CheckboxEntry({
94+
element,
95+
id: 'cancelRemainingInstances',
96+
label: translate('Cancel remaining instances'),
97+
getValue,
98+
setValue,
99+
});
100+
}
101+
102+
// helper ////////////////////////////
103+
104+
// formal expression /////////////////
105+
106+
/**
107+
* updateFormalExpression - updates a specific formal expression
108+
*
109+
* @param {djs.model.Base} element
110+
* @param {string} propertyName
111+
* @param {string} newValue
112+
* @param {BpmnFactory} bpmnFactory
113+
*/
114+
function updateFormalExpression(element, propertyName, newValue, bpmnFactory) {
115+
const businessObject = getBusinessObject(element);
116+
const expressionProps = {};
117+
118+
if (!newValue) {
119+
120+
// remove formal expression
121+
expressionProps[propertyName] = undefined;
122+
123+
return {
124+
element,
125+
moddleElement: businessObject,
126+
properties: expressionProps,
127+
};
128+
}
129+
130+
const existingExpression = businessObject.get(propertyName);
131+
if (!existingExpression) {
132+
133+
// add formal expression
134+
expressionProps[propertyName] = createElement(
135+
'bpmn:FormalExpression',
136+
{ body: newValue },
137+
businessObject,
138+
bpmnFactory
139+
);
140+
141+
return {
142+
element,
143+
moddleElement: businessObject,
144+
properties: expressionProps,
145+
};
146+
}
147+
148+
// edit existing formal expression
149+
return {
150+
element,
151+
moddleElement: existingExpression,
152+
properties: {
153+
body: newValue,
154+
},
155+
};
156+
}

src/provider/bpmn/properties/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { AdHocCompletionProps } from './AdHocCompletionProps';
12
export { CompensationProps } from './CompensationProps';
23
export { DocumentationProps } from './DocumentationProps';
34
export { ErrorProps } from './ErrorProps';

src/provider/zeebe/ZeebePropertiesProvider.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { findIndex } from 'min-dash';
44

55
import {
66
ActiveElementsProps,
7+
AdHocCompletionProps,
78
AssignmentDefinitionProps,
89
BusinessRuleImplementationProps,
910
CalledDecisionProps,
@@ -86,6 +87,7 @@ export default class ZeebePropertiesProvider {
8687
updateSignalGroup(groups, element);
8788
updateTimerGroup(groups, element, this._injector);
8889
updateMultiInstanceGroup(groups, element);
90+
updateAdHocCompletionGroup(groups, element);
8991

9092
// (3) remove message group when not applicable
9193
groups = removeMessageGroup(groups, element);
@@ -467,6 +469,25 @@ function updateMultiInstanceGroup(groups, element) {
467469
];
468470
}
469471

472+
// overwrite bpmn generic adHoc completion condition with zeebe-specific one
473+
function updateAdHocCompletionGroup(groups, element) {
474+
const adHocCompletionGroup = findGroup(groups, 'adHocCompletion');
475+
if (!adHocCompletionGroup) {
476+
return;
477+
}
478+
479+
adHocCompletionGroup.entries = replaceEntriesPreservingOrder(
480+
adHocCompletionGroup.entries,
481+
AdHocCompletionProps({ element })
482+
);
483+
484+
// reorder groups to move active elements before adHoc completion
485+
const activeElementsGroup = findGroup(groups, 'activeElements');
486+
if (activeElementsGroup) {
487+
reorderGroupsIfNecessary(groups, activeElementsGroup, adHocCompletionGroup);
488+
}
489+
}
490+
470491
// remove message group from Message End Event & Message Throw Event
471492
function removeMessageGroup(groups, element) {
472493
const messageGroup = findGroup(groups, 'message');
@@ -485,9 +506,28 @@ function findGroup(groups, id) {
485506
return groups.find(g => g.id === id);
486507
}
487508

509+
/**
510+
* Put groups into the defined order if necessary.
511+
*
512+
* @param {Group[]} groups
513+
* @param {Group} firstGroup
514+
* @param {Group} secondGroup
515+
*/
516+
function reorderGroupsIfNecessary(groups, firstGroup, secondGroup) {
517+
const firstGroupIndex = groups.indexOf(firstGroup);
518+
const secondGroupIndex = groups.indexOf(secondGroup);
519+
520+
if (firstGroupIndex === -1 || secondGroupIndex === -1 || firstGroupIndex <= secondGroupIndex) {
521+
return;
522+
}
523+
524+
groups[firstGroupIndex] = secondGroup;
525+
groups[secondGroupIndex] = firstGroup;
526+
}
527+
488528
/**
489529
* Replace entries with the same ID.
490-
*s
530+
*
491531
* @param {Entry[]} oldEntries
492532
* @param {Entry[]} newEntries
493533
*
@@ -503,4 +543,45 @@ function replaceEntries(oldEntries, newEntries) {
503543
...filteredEntries,
504544
...newEntries
505545
];
546+
}
547+
548+
/**
549+
* Replace entries with the same ID, preserving original order and adding new entries at the end.
550+
*
551+
* @param {Entry[]} oldEntries
552+
* @param {Entry[]} newEntries
553+
*
554+
* @returns {Entry[]} combined entries
555+
*/
556+
function replaceEntriesPreservingOrder(oldEntries, newEntries) {
557+
const indexedNewEntries = indexEntriesById(newEntries);
558+
const indexedOldEntries = indexEntriesById(oldEntries);
559+
560+
const result = [];
561+
562+
// iterate original entries and replace with new ones if ID matches
563+
oldEntries.forEach((oldEntry) => {
564+
const newEntryIndex = indexedNewEntries[oldEntry.id];
565+
if (newEntryIndex !== undefined) {
566+
result.push(newEntries[newEntryIndex]);
567+
} else {
568+
result.push(oldEntry);
569+
}
570+
});
571+
572+
// append remaining new entries
573+
newEntries.forEach((newEntry) => {
574+
if (indexedOldEntries[newEntry.id] === undefined) {
575+
result.push(newEntry);
576+
}
577+
});
578+
579+
return result;
580+
}
581+
582+
function indexEntriesById(entries) {
583+
return entries.reduce((index, entry, idx) => {
584+
index[entry.id] = idx;
585+
return index;
586+
}, {});
506587
}

0 commit comments

Comments
 (0)