From 63ccae1bddc57f92a6115937abfef57bbc33b339 Mon Sep 17 00:00:00 2001
From: Philipp Fromme
Date: Wed, 24 Jul 2024 16:25:35 +0200
Subject: [PATCH] feat: add _Binding_ entry
Related to https://github.com/camunda/camunda-modeler/issues/4385
---
src/contextProvider/zeebe/TooltipProvider.js | 17 +++
src/provider/HOCs/index.js | 1 +
src/provider/HOCs/withProps.js | 5 +
.../zeebe/properties/CalledDecisionProps.js | 14 ++-
src/provider/zeebe/properties/FormProps.js | 13 ++
src/provider/zeebe/properties/TargetProps.js | 14 ++-
.../zeebe/properties/shared/Binding.js | 112 ++++++++++++++++++
src/provider/zeebe/utils/CalledElementUtil.js | 6 +
.../zeebe/CalledDecisionProps.spec.js | 98 +++++++++++++++
test/spec/provider/zeebe/Forms.spec.js | 76 ++++++++++++
test/spec/provider/zeebe/TargetProps.spec.js | 92 ++++++++++++++
11 files changed, 444 insertions(+), 4 deletions(-)
create mode 100644 src/provider/HOCs/withProps.js
create mode 100644 src/provider/zeebe/properties/shared/Binding.js
diff --git a/src/contextProvider/zeebe/TooltipProvider.js b/src/contextProvider/zeebe/TooltipProvider.js
index 4bcee49ea..69cec39b3 100644
--- a/src/contextProvider/zeebe/TooltipProvider.js
+++ b/src/contextProvider/zeebe/TooltipProvider.js
@@ -278,6 +278,23 @@ const TooltipProvider = {
);
+ },
+ 'bindingType': (element) => {
+
+ const translate = useService('translate');
+
+ return (
+
+
+
{ translate('Latest binding') }
+ { translate('Uses the most recent deployed resource.') }
+
+
+
{ translate('Deployment binding') }
+ { translate('Uses the resource found in the same deployment.') }
+
+
+ );
}
};
diff --git a/src/provider/HOCs/index.js b/src/provider/HOCs/index.js
index 11b425820..6cc2f0eb3 100644
--- a/src/provider/HOCs/index.js
+++ b/src/provider/HOCs/index.js
@@ -1,2 +1,3 @@
+export { withProps } from './withProps';
export { withVariableContext } from './withVariableContext';
export { withTooltipContainer } from './withTooltipContainer';
\ No newline at end of file
diff --git a/src/provider/HOCs/withProps.js b/src/provider/HOCs/withProps.js
new file mode 100644
index 000000000..a54301acb
--- /dev/null
+++ b/src/provider/HOCs/withProps.js
@@ -0,0 +1,5 @@
+export function withProps(Component, otherProps) {
+ return props => {
+ return ;
+ };
+}
\ No newline at end of file
diff --git a/src/provider/zeebe/properties/CalledDecisionProps.js b/src/provider/zeebe/properties/CalledDecisionProps.js
index da7bdd355..83609ed7f 100644
--- a/src/provider/zeebe/properties/CalledDecisionProps.js
+++ b/src/provider/zeebe/properties/CalledDecisionProps.js
@@ -4,10 +4,14 @@ import {
} from 'bpmn-js/lib/util/ModelUtil';
import {
- TextFieldEntry, isTextFieldEntryEdited,
- isFeelEntryEdited
+ isFeelEntryEdited,
+ isSelectEntryEdited,
+ isTextFieldEntryEdited,
+ TextFieldEntry
} from '@bpmn-io/properties-panel';
+import Binding from './shared/Binding';
+
import {
getExtensionElementsList
} from '../../../utils/ExtensionElementsUtil';
@@ -20,6 +24,7 @@ import { useService } from '../../../hooks';
import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext';
+import { withProps } from '../../HOCs/withProps.js';
export function CalledDecisionProps(props) {
@@ -37,6 +42,11 @@ export function CalledDecisionProps(props) {
component: DecisionID,
isEdited: isFeelEntryEdited
},
+ {
+ id: 'bindingType',
+ component: withProps(Binding, { type: 'zeebe:CalledDecision' }),
+ isEdited: isSelectEntryEdited
+ },
{
id: 'resultVariable',
component: ResultVariable,
diff --git a/src/provider/zeebe/properties/FormProps.js b/src/provider/zeebe/properties/FormProps.js
index 531a02b27..d863317d8 100644
--- a/src/provider/zeebe/properties/FormProps.js
+++ b/src/provider/zeebe/properties/FormProps.js
@@ -13,10 +13,13 @@ import {
TextFieldEntry,
TextAreaEntry,
isFeelEntryEdited,
+ isSelectEntryEdited,
isTextFieldEntryEdited,
isTextAreaEntryEdited
} from '@bpmn-io/properties-panel';
+import Binding from './shared/Binding';
+
import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext';
import { createElement } from '../../../utils/ElementUtil';
@@ -34,6 +37,8 @@ import {
userTaskFormIdToFormKey
} from '../utils/FormUtil';
+import { withProps } from '../../HOCs';
+
const NONE_VALUE = 'none';
@@ -78,6 +83,14 @@ export function FormProps(props) {
});
}
+ if (formType === FORM_TYPES.CAMUNDA_FORM_LINKED) {
+ entries.push({
+ id: 'bindingType',
+ component: withProps(Binding, { type: 'zeebe:FormDefinition' }),
+ isEdited: isSelectEntryEdited
+ });
+ }
+
return entries;
}
diff --git a/src/provider/zeebe/properties/TargetProps.js b/src/provider/zeebe/properties/TargetProps.js
index 2cbca801e..98dce5e86 100644
--- a/src/provider/zeebe/properties/TargetProps.js
+++ b/src/provider/zeebe/properties/TargetProps.js
@@ -4,9 +4,12 @@ import {
} from 'bpmn-js/lib/util/ModelUtil';
import {
- isFeelEntryEdited
+ isFeelEntryEdited,
+ isSelectEntryEdited
} from '@bpmn-io/properties-panel';
+import Binding from './shared/Binding';
+
import {
createElement
} from '../../../utils/ElementUtil';
@@ -20,6 +23,8 @@ import { useService } from '../../../hooks';
import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext';
+import { withProps } from '../../HOCs/withProps.js';
+
export function TargetProps(props) {
const {
@@ -35,6 +40,11 @@ export function TargetProps(props) {
id: 'targetProcessId',
component: TargetProcessId,
isEdited: isFeelEntryEdited
+ },
+ {
+ id: 'bindingType',
+ component: withProps(Binding, { type: 'zeebe:CalledElement' }),
+ isEdited: isSelectEntryEdited
}
];
}
@@ -128,4 +138,4 @@ function TargetProcessId(props) {
setValue,
debounce
});
-}
+}
\ No newline at end of file
diff --git a/src/provider/zeebe/properties/shared/Binding.js b/src/provider/zeebe/properties/shared/Binding.js
new file mode 100644
index 000000000..7a563d889
--- /dev/null
+++ b/src/provider/zeebe/properties/shared/Binding.js
@@ -0,0 +1,112 @@
+import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil';
+
+import { SelectEntry } from '@bpmn-io/properties-panel';
+
+import { createElement } from '../../../../utils/ElementUtil';
+
+import { useService } from '../../../../hooks';
+
+import { getExtensionElementsList } from '../../../../utils/ExtensionElementsUtil';
+
+export default function Binding(props) {
+ const {
+ element,
+ type
+ } = props;
+
+ const bpmnFactory = useService('bpmnFactory'),
+ commandStack = useService('commandStack'),
+ translate = useService('translate');
+
+ const getValue = () => {
+ const businessObject = getBusinessObject(element);
+
+ const extensionElement = getExtensionElementsList(businessObject, type)[ 0 ];
+
+ if (!extensionElement) {
+ return 'latest';
+ }
+
+ return extensionElement.get('bindingType');
+ };
+
+ const setValue = value => {
+ const commands = [];
+
+ const businessObject = getBusinessObject(element);
+
+ // (1) ensure extension elements
+ let extensionElements = businessObject.get('extensionElements');
+
+ if (!extensionElements) {
+ extensionElements = createElement(
+ 'bpmn:ExtensionElements',
+ { values: [] },
+ businessObject,
+ bpmnFactory
+ );
+
+ commands.push({
+ cmd: 'element.updateModdleProperties',
+ context: {
+ element,
+ moddleElement: businessObject,
+ properties: { extensionElements }
+ }
+ });
+ }
+
+ // (2) ensure extension element
+ let extensionElement = getExtensionElementsList(businessObject, type)[ 0 ];
+
+ if (!extensionElement) {
+ extensionElement = createElement(
+ type,
+ {},
+ extensionElements,
+ bpmnFactory
+ );
+
+ commands.push({
+ cmd: 'element.updateModdleProperties',
+ context: {
+ element,
+ moddleElement: extensionElements,
+ properties: {
+ values: [ ...extensionElements.get('values'), extensionElement ]
+ }
+ }
+ });
+
+ }
+
+ // (3) Update bindingType attribute
+ commands.push({
+ cmd: 'element.updateModdleProperties',
+ context: {
+ element,
+ moddleElement: extensionElement,
+ properties: {
+ bindingType: value
+ }
+ }
+ });
+
+ // (4) Execute the commands
+ commandStack.execute('properties-panel.multi-command-executor', commands);
+ };
+
+ const getOptions = () => ([
+ { value: 'latest', label: translate('latest') },
+ { value: 'deployment', label: translate('deployment') }
+ ]);
+
+ return ;
+}
\ No newline at end of file
diff --git a/src/provider/zeebe/utils/CalledElementUtil.js b/src/provider/zeebe/utils/CalledElementUtil.js
index 5e1990523..90be6254c 100644
--- a/src/provider/zeebe/utils/CalledElementUtil.js
+++ b/src/provider/zeebe/utils/CalledElementUtil.js
@@ -25,6 +25,12 @@ export function getProcessId(element) {
return calledElement ? calledElement.get('processId') : '';
}
+export function getBindingType(element) {
+ const calledElement = getCalledElement(element);
+
+ return calledElement ? calledElement.get('bindingType') : '';
+}
+
export function getCalledElement(element) {
const calledElements = getCalledElements(element);
return calledElements[0];
diff --git a/test/spec/provider/zeebe/CalledDecisionProps.spec.js b/test/spec/provider/zeebe/CalledDecisionProps.spec.js
index ecbd9d603..f92724327 100644
--- a/test/spec/provider/zeebe/CalledDecisionProps.spec.js
+++ b/test/spec/provider/zeebe/CalledDecisionProps.spec.js
@@ -153,6 +153,98 @@ describe('provider/zeebe - CalledDecisionProps', function() {
});
+ describe('#calledDecision.bindingType', function() {
+
+ it('should display', inject(async function(elementRegistry, selection) {
+
+ // given
+ const businessRuleTask = elementRegistry.get('BusinessRuleTask_1');
+
+ // assume
+ const bindingType = getBindingType(businessRuleTask);
+
+ expect(bindingType).to.equal('latest');
+
+ // when
+ await act(() => {
+ selection.select(businessRuleTask);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // then
+ expect(bindingTypeSelect).to.exist;
+
+ expect(bindingTypeSelect.value).to.equal('latest');
+ }));
+
+
+ it('should not display', inject(async function(elementRegistry, selection) {
+
+ // given
+ const task = elementRegistry.get('Task_1');
+
+ // when
+ await act(() => {
+ selection.select(task);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // then
+ expect(bindingTypeSelect).not.to.exist;
+ }));
+
+
+ it('should update', inject(async function(elementRegistry, selection) {
+
+ // given
+ const businessRuleTask = elementRegistry.get('BusinessRuleTask_1');
+
+ await act(() => {
+ selection.select(businessRuleTask);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // when
+ changeInput(bindingTypeSelect, 'deployment');
+
+ // then
+ const bindingType = getBindingType(businessRuleTask);
+
+ expect(bindingType).to.equal('deployment');
+ }));
+
+
+ it('should update on external change',
+ inject(async function(elementRegistry, selection, commandStack) {
+
+ // given
+ const businessRuleTask = elementRegistry.get('BusinessRuleTask_1'),
+ originalValue = getBindingType(businessRuleTask);
+
+ await act(() => {
+ selection.select(businessRuleTask);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ changeInput(bindingTypeSelect, 'deployment');
+
+ // when
+ await act(() => {
+ commandStack.undo();
+ });
+
+ // then
+ expect(getBindingType(businessRuleTask)).to.eql(originalValue);
+ })
+ );
+
+ });
+
+
describe('#calledDecision.resultVariable', function() {
it('should display', inject(async function(elementRegistry, selection) {
@@ -255,6 +347,12 @@ export function getDecisionId(element) {
return calledDecision ? calledDecision.get('decisionId') : '';
}
+export function getBindingType(element) {
+ const calledDecision = getCalledDecision(element);
+
+ return calledDecision ? calledDecision.get('bindingType') : '';
+}
+
export function getResultVariable(element) {
const calledDecision = getCalledDecision(element);
diff --git a/test/spec/provider/zeebe/Forms.spec.js b/test/spec/provider/zeebe/Forms.spec.js
index 207f40a70..f1baf1b54 100644
--- a/test/spec/provider/zeebe/Forms.spec.js
+++ b/test/spec/provider/zeebe/Forms.spec.js
@@ -476,6 +476,70 @@ describe('provider/zeebe - Forms', function() {
expectFormId(userTask, initialFormId);
}));
+
+ describe('binding type', function() {
+
+ it('should display', inject(async function(elementRegistry, selection) {
+
+ // given
+ const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED');
+
+ // when
+ await act(() => {
+ selection.select(userTask);
+ });
+
+ const bindingTypeSelect = getBindingTypeSelect(container);
+
+ // then
+ expect(bindingTypeSelect).to.exist;
+ }));
+
+
+ it('should update', inject(async function(elementRegistry, selection) {
+
+ // given
+ const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED');
+
+ await act(() => {
+ selection.select(userTask);
+ });
+
+ const bindingTypeSelect = getBindingTypeSelect(container);
+
+ // when
+ changeInput(bindingTypeSelect, 'deployment');
+
+ // then
+ expectBindingType(userTask, 'deployment');
+ }));
+
+
+ it('should update on external change', inject(async function(commandStack, elementRegistry, selection) {
+
+ // given
+ const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED');
+
+ await act(() => {
+ selection.select(userTask);
+ });
+
+ const bindingTypeSelect = getBindingTypeSelect(container);
+ const initialBindingType = bindingTypeSelect.value;
+
+ changeInput(bindingTypeSelect, 'deployment');
+ expectBindingType(userTask, 'deployment');
+
+ // when
+ await act(() => {
+ commandStack.undo();
+ });
+
+ expectBindingType(userTask, initialBindingType);
+ }));
+
+ });
+
});
@@ -675,6 +739,7 @@ describe('provider/zeebe - Forms', function() {
expectExternalReference(userTask, initialFormKey);
}));
});
+
});
@@ -700,6 +765,10 @@ function getFormTypeSelect(container) {
return domQuery('select[name=formType]', container);
}
+function getBindingTypeSelect(container) {
+ return domQuery('select[name=bindingType]', container);
+}
+
function expectFormId(element, expected) {
const formDefinition = getFormDefinition(element);
@@ -707,6 +776,13 @@ function expectFormId(element, expected) {
expect(formDefinition.get('formId')).to.eql(expected);
}
+function expectBindingType(element, expected) {
+ const formDefinition = getFormDefinition(element);
+
+ expect(formDefinition).to.exist;
+ expect(formDefinition.get('bindingType')).to.eql(expected);
+}
+
function expectFormKey(element, expected) {
const formDefinition = getFormDefinition(element);
diff --git a/test/spec/provider/zeebe/TargetProps.spec.js b/test/spec/provider/zeebe/TargetProps.spec.js
index ac39d2a71..78156e561 100644
--- a/test/spec/provider/zeebe/TargetProps.spec.js
+++ b/test/spec/provider/zeebe/TargetProps.spec.js
@@ -23,6 +23,7 @@ import {
} from 'src/utils/ExtensionElementsUtil.js';
import {
+ getBindingType,
getCalledElement,
getProcessId
} from 'src/provider/zeebe/utils/CalledElementUtil.js';
@@ -224,4 +225,95 @@ describe('provider/zeebe - TargetProps', function() {
});
+
+ describe('bpmn:CallActivity#calledElement.bindingType', function() {
+
+ it('should display', inject(async function(elementRegistry, selection) {
+
+ // given
+ const callActivity = elementRegistry.get('CallActivity_1');
+
+ // assume
+ const bindingType = getBindingType(callActivity);
+
+ expect(bindingType).to.equal('latest');
+
+ // when
+ await act(() => {
+ selection.select(callActivity);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // then
+ expect(bindingTypeSelect).to.exist;
+
+ expect(bindingTypeSelect.value).to.equal('latest');
+ }));
+
+
+ it('should not display', inject(async function(elementRegistry, selection) {
+
+ // given
+ const task = elementRegistry.get('Task_1');
+
+ // when
+ await act(() => {
+ selection.select(task);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // then
+ expect(bindingTypeSelect).not.to.exist;
+ }));
+
+
+ it('should update', inject(async function(elementRegistry, selection) {
+
+ // given
+ const callActivity = elementRegistry.get('CallActivity_1');
+
+ await act(() => {
+ selection.select(callActivity);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+
+ // when
+ changeInput(bindingTypeSelect, 'deployment');
+
+ // then
+ const bindingType = getBindingType(callActivity);
+
+ expect(bindingType).to.equal('deployment');
+ }));
+
+
+ it('should update on external change',
+ inject(async function(elementRegistry, selection, commandStack) {
+
+ // given
+ const callActivity = elementRegistry.get('CallActivity_1'),
+ originalValue = getBindingType(callActivity);
+
+ await act(() => {
+ selection.select(callActivity);
+ });
+
+ const bindingTypeSelect = domQuery('select[name=bindingType]', container);
+ changeInput(bindingTypeSelect, 'deployment');
+
+ // when
+ await act(() => {
+ commandStack.undo();
+ });
+
+ // then
+ expect(getBindingType(callActivity)).to.eql(originalValue);
+ })
+ );
+
+ });
+
});