diff --git a/src/cloud-element-templates/CreateHelper.js b/src/cloud-element-templates/CreateHelper.js index 1affadc2..62f301e1 100644 --- a/src/cloud-element-templates/CreateHelper.js +++ b/src/cloud-element-templates/CreateHelper.js @@ -97,6 +97,18 @@ export function createZeebeProperty(binding, value = '', bpmnFactory) { }); } +/** + * Create a called element representing the given value. + * + * @param {object} attrs + * @param {BpmnFactory} bpmnFactory + * + * @return {ModdleElement} + */ +export function createCalledElement(attrs = {}, bpmnFactory) { + return bpmnFactory.create('zeebe:CalledElement', attrs); +} + /** * Retrieves whether an element should be updated for a given property. * diff --git a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js index e720b6c8..f704570a 100644 --- a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js +++ b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js @@ -11,6 +11,7 @@ import { } from '../Helper'; import { + createCalledElement, createInputParameter, createOutputParameter, createTaskDefinition, @@ -29,7 +30,8 @@ import { MESSAGE_BINDING_TYPES, MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, - TASK_DEFINITION_TYPES + TASK_DEFINITION_TYPES, + ZEEBE_CALLED_ELEMENT } from '../util/bindingTypes'; import { @@ -125,6 +127,8 @@ export default class ChangeElementTemplateHandler { this._updateZeebePropertyProperties(element, oldTemplate, newTemplate); this._updateMessage(element, oldTemplate, newTemplate); + + this._updateCalledElement(element, oldTemplate, newTemplate); } } @@ -902,6 +906,107 @@ export default class ChangeElementTemplateHandler { } + + /** + * Update `zeebe:CalledElement` properties of specified business object. This + * can only exist in `bpmn:ExtensionElements`. + * + * @param {djs.model.Base} element + * @param {Object} oldTemplate + * @param {Object} newTemplate + */ + _updateCalledElement(element, oldTemplate, newTemplate) { + const bpmnFactory = this._bpmnFactory, + commandStack = this._commandStackWrapper; + + const newProperties = newTemplate.properties.filter((newProperty) => { + const newBinding = newProperty.binding, + newBindingType = newBinding.type; + + return newBindingType === ZEEBE_CALLED_ELEMENT; + }); + + const businessObject = this._getOrCreateExtensionElements(element); + let calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // (1) remove old called element if no new properties specified + if (!newProperties.length) { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: businessObject, + properties: { + values: without(businessObject.get('values'), calledElement) + } + }); + + return; + } + + + newProperties.forEach((newProperty) => { + const oldProperty = findOldProperty(oldTemplate, newProperty), + newPropertyValue = getDefaultValue(newProperty), + propertyName = newProperty.binding.property; + + // (2) update old called element + if (calledElement) { + + if (!shouldKeepValue(calledElement, oldProperty, newProperty)) { + const properties = { + [propertyName]: newPropertyValue + }; + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: calledElement, + properties + }); + } + } + + // (3) add new called element + else { + const properties = { + [propertyName]: newPropertyValue + }; + + calledElement = createCalledElement(properties, bpmnFactory); + + calledElement.$parent = businessObject; + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: businessObject, + properties: { + values: [ ...businessObject.get('values'), calledElement ] + } + }); + } + }); + + // (4) remove properties no longer templated + const oldProperties = oldTemplate && oldTemplate.properties.filter((oldProperty) => { + const oldBinding = oldProperty.binding, + oldBindingType = oldBinding.type; + + return oldBindingType === ZEEBE_CALLED_ELEMENT && !newProperties.find( + (newProperty) => newProperty.binding.property === oldProperty.binding.property + ); + }) || []; + + oldProperties.forEach((oldProperty) => { + const properties = { + [oldProperty.binding.property]: undefined + }; + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: calledElement, + properties + }); + }); + } + /** * Replaces the element with the specified elementType. * Takes into account the eventDefinition for events. diff --git a/src/cloud-element-templates/create/CalledElementBindingProvider.js b/src/cloud-element-templates/create/CalledElementBindingProvider.js new file mode 100644 index 00000000..101d995c --- /dev/null +++ b/src/cloud-element-templates/create/CalledElementBindingProvider.js @@ -0,0 +1,33 @@ +import { + ensureExtension +} from '../CreateHelper'; +import { getDefaultValue } from '../Helper'; + +import { ensureNoPropagation } from '../util/calledElement'; + + +export class CalledElementBindingProvider { + static create(element, options) { + const { + property, + bpmnFactory + } = options; + + const { + binding + } = property; + + const { + property: propertyName + } = binding; + + const value = getDefaultValue(property); + + const calledElement = ensureExtension(element, 'zeebe:CalledElement', bpmnFactory); + + // TODO(@barmac): remove if we decide to support propagation in templates + ensureNoPropagation(calledElement); + + calledElement.set(propertyName, value); + } +} \ No newline at end of file diff --git a/src/cloud-element-templates/create/TemplateElementFactory.js b/src/cloud-element-templates/create/TemplateElementFactory.js index 663ecef8..add94066 100644 --- a/src/cloud-element-templates/create/TemplateElementFactory.js +++ b/src/cloud-element-templates/create/TemplateElementFactory.js @@ -12,6 +12,7 @@ import TaskHeaderBindingProvider from './TaskHeaderBindingProvider'; import ZeebePropertiesProvider from './ZeebePropertiesProvider'; import { MessagePropertyBindingProvider } from './MessagePropertyBindingProvider'; import { MessageZeebeSubscriptionBindingProvider } from './MessageZeebeSubscriptionBindingProvider'; +import { CalledElementBindingProvider } from './CalledElementBindingProvider'; import { MESSAGE_PROPERTY_TYPE, @@ -22,7 +23,8 @@ import { ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE, ZEEBE_TASK_HEADER_TYPE, - ZEBBE_PROPERTY_TYPE + ZEBBE_PROPERTY_TYPE, + ZEEBE_CALLED_ELEMENT } from '../util/bindingTypes'; import { @@ -44,7 +46,8 @@ export default class TemplateElementFactory { [ZEEBE_OUTPUT_TYPE]: OutputBindingProvider, [ZEEBE_TASK_HEADER_TYPE]: TaskHeaderBindingProvider, [MESSAGE_PROPERTY_TYPE]: MessagePropertyBindingProvider, - [MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]: MessageZeebeSubscriptionBindingProvider + [MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]: MessageZeebeSubscriptionBindingProvider, + [ZEEBE_CALLED_ELEMENT]: CalledElementBindingProvider }; } diff --git a/src/cloud-element-templates/util/bindingTypes.js b/src/cloud-element-templates/util/bindingTypes.js index 406d95c1..77bd8edc 100644 --- a/src/cloud-element-templates/util/bindingTypes.js +++ b/src/cloud-element-templates/util/bindingTypes.js @@ -9,6 +9,7 @@ export const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition'; export const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader'; export const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property'; export const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property'; +export const ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement'; export const EXTENSION_BINDING_TYPES = [ MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, @@ -17,7 +18,8 @@ export const EXTENSION_BINDING_TYPES = [ ZEEBE_PROPERTY_TYPE, ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION, - ZEEBE_TASK_HEADER_TYPE + ZEEBE_TASK_HEADER_TYPE, + ZEEBE_CALLED_ELEMENT ]; export const TASK_DEFINITION_TYPES = [ diff --git a/src/cloud-element-templates/util/calledElement.js b/src/cloud-element-templates/util/calledElement.js new file mode 100644 index 00000000..e39683dc --- /dev/null +++ b/src/cloud-element-templates/util/calledElement.js @@ -0,0 +1,4 @@ +export function ensureNoPropagation(calledElement) { + calledElement.set('propagateAllChildVariables', false); + calledElement.set('propagateAllParentVariables', false); +} diff --git a/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js b/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js index 1a3b5752..7f002090 100644 --- a/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js +++ b/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js @@ -26,6 +26,8 @@ import updateTemplates from './fixtures/condition-update.json'; import messageTemplates from './fixtures/condition-message.json'; import messageCorrelationTemplate from './fixtures/message-correlation-key.json'; +import calledElementTemplate from './fixtures/condition-called-element.json'; + import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; import { findExtension, findMessage, findZeebeSubscription } from 'src/cloud-element-templates/Helper'; import ElementTemplatesConditionChecker from 'src/cloud-element-templates/ElementTemplatesConditionChecker'; @@ -1108,6 +1110,140 @@ describe('provider/cloud-element-templates - ElementTemplatesConditionChecker', }); + describe('update zeebe:calledElement', function() { + + it('should add conditional entries', inject( + async function(elementRegistry, modeling) { + + // given + const element = elementRegistry.get('Task_1'); + changeTemplate(element, calledElementTemplate); + + const businessObject = getBusinessObject(element); + + // when + modeling.updateProperties(element, { + name: 'foo' + }); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + processId: 'one', + propagateAllChildVariables: false, + propagateAllParentVariables: false + }); + }) + ); + + + it('should remove conditional entries', inject( + async function(elementRegistry, modeling) { + + // given + const element = elementRegistry.get('Task_1'); + changeTemplate(element, calledElementTemplate); + + const businessObject = getBusinessObject(element); + modeling.updateProperties(element, { + name: 'foo' + }); + + // when + modeling.updateProperties(element, { + name: '' + }); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).not.to.exist; + }) + ); + + + it('should switch between conditional properties', inject( + async function(elementRegistry, modeling) { + + // given + const element = elementRegistry.get('Task_1'); + changeTemplate(element, calledElementTemplate); + + const businessObject = getBusinessObject(element); + modeling.updateProperties(element, { + name: 'foo' + }); + + // when + modeling.updateProperties(element, { + name: 'bar' + }); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + processId: 'two', + propagateAllChildVariables: false, + propagateAllParentVariables: false + }); + }) + ); + + + it('undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + const element = elementRegistry.get('Task_1'); + changeTemplate(element, calledElementTemplate); + + const businessObject = getBusinessObject(element); + modeling.updateProperties(element, { + name: 'foo' + }); + + // when + commandStack.undo(); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).not.to.exist; + })); + + + it('redo', inject(function(commandStack, elementRegistry, modeling) { + + // given + const element = elementRegistry.get('Task_1'); + changeTemplate(element, calledElementTemplate); + + const businessObject = getBusinessObject(element); + modeling.updateProperties(element, { + name: 'foo' + }); + commandStack.undo(); + + // when + commandStack.redo(); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + processId: 'one', + propagateAllChildVariables: false, + propagateAllParentVariables: false + }); + })); + + }); + + describe('update referenced element', function() { const template = messageTemplates[2]; diff --git a/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js b/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js index 29adc248..e8397a68 100644 --- a/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js +++ b/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js @@ -1700,6 +1700,85 @@ describe('cloud-element-templates/cmd - ChangeElementTemplateHandler', function( }); + describe.only('update zeebe:calledElement', function() { + + beforeEach(bootstrap(require('./task.bpmn').default)); + + const newTemplate = require('./called-element.json'); + + + it('execute', inject(function(elementRegistry) { + + // given + let task = elementRegistry.get('Task_1'); + + // when + changeTemplate(task, newTemplate); + + // then + task = elementRegistry.get('Task_1'); + expectElementTemplate(task, 'calledElement'); + + const calledElement = findExtension(task, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + processId: 'paymentProcess', + propagateChildVariables: false, + propagateParentVariables: false + }); + })); + + + it('undo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('Task_1'); + + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + + // then + task = elementRegistry.get('Task_1'); + expectNoElementTemplate(task); + + const calledElement = findExtension(task, 'zeebe:CalledElement'); + + expect(calledElement).not.to.exist; + })); + + + it('redo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('Task_1'); + + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + commandStack.redo(); + + // then + task = elementRegistry.get('Task_1'); + expectElementTemplate(task, 'calledElement'); + + const calledElement = findExtension(task, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + processId: 'paymentProcess', + propagateChildVariables: false, + propagateParentVariables: false + }); + })); + }); + + describe('create message with zeebe:modelerTemplate', function() { beforeEach(bootstrap(require('./event.bpmn').default)); diff --git a/test/spec/cloud-element-templates/cmd/called-element.json b/test/spec/cloud-element-templates/cmd/called-element.json new file mode 100644 index 00000000..a42c8101 --- /dev/null +++ b/test/spec/cloud-element-templates/cmd/called-element.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "calledElement", + "name": "Payment", + "description": "Payment process call activity", + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "type": "Hidden", + "value": "paymentProcess", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + } + } + ] +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/create/TemplateElementFactory.spec.js b/test/spec/cloud-element-templates/create/TemplateElementFactory.spec.js index e08af3d8..c55b45ab 100644 --- a/test/spec/cloud-element-templates/create/TemplateElementFactory.spec.js +++ b/test/spec/cloud-element-templates/create/TemplateElementFactory.spec.js @@ -464,6 +464,28 @@ describe('provider/cloud-element-templates - TemplateElementFactory', function() correlationKey: '=variable' }); })); + + + it('should handle ', inject(function(templateElementFactory) { + + // given + const elementTemplate = findTemplate('calledElement'); + + // when + const element = templateElementFactory.create(elementTemplate); + + // then + const bo = getBusinessObject(element); + const calledElement = findExtension(bo, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.jsonEqual({ + $type: 'zeebe:CalledElement', + propagateAllChildVariables: false, + propagateAllParentVariables: false, + processId: 'paymentProcess' + }); + })); }); diff --git a/test/spec/cloud-element-templates/create/TemplatesElementFactory.json b/test/spec/cloud-element-templates/create/TemplatesElementFactory.json index b84a5dd1..32a4e265 100644 --- a/test/spec/cloud-element-templates/create/TemplatesElementFactory.json +++ b/test/spec/cloud-element-templates/create/TemplatesElementFactory.json @@ -512,5 +512,27 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "calledElement", + "name": "Payment", + "description": "Payment process call activity", + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "type": "Hidden", + "value": "paymentProcess", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + } + } + ] } ] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/fixtures/called-element.json b/test/spec/cloud-element-templates/fixtures/called-element.json new file mode 100644 index 00000000..0b74240f --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/called-element.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "io.camunda.examples.Payment", + "name": "Payment", + "description": "Payment process call activity", + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "type": "Hidden", + "value": "paymentProcess", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + } + }, + { + "label": "Payment ID", + "type": "String", + "binding": { + "type": "zeebe:input", + "name": "paymentID" + } + }, + { + "label": "Amount", + "type": "String", + "binding": { + "type": "zeebe:input", + "name": "amount" + } + }, + { + "label": "Outcome", + "type": "String", + "description": "Name of variable to store the result data in.", + "value": "paymentOutcome", + "binding": { + "type": "zeebe:output", + "source": "=outcome" + } + } + ] +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/fixtures/condition-called-element.json b/test/spec/cloud-element-templates/fixtures/condition-called-element.json new file mode 100644 index 00000000..ab84faeb --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/condition-called-element.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "io.camunda.examples.Payment", + "name": "Payment", + "description": "Payment process call activity", + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "id": "nameProp", + "label": "name", + "type": "String", + "binding": { + "type": "property", + "name": "name" + } + }, + { + "type": "Hidden", + "value": "one", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + }, + "condition": { + "equals": "foo", + "property": "nameProp" + } + }, + { + "type": "Hidden", + "value": "two", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + }, + "condition": { + "equals": "bar", + "property": "nameProp" + } + } + ] +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.bpmn b/test/spec/cloud-element-templates/properties/CustomProperties.bpmn index ad333f5d..3d9df6d9 100644 --- a/test/spec/cloud-element-templates/properties/CustomProperties.bpmn +++ b/test/spec/cloud-element-templates/properties/CustomProperties.bpmn @@ -1,5 +1,5 @@ - + @@ -63,6 +63,11 @@ + + + + + @@ -132,6 +137,10 @@ + + + + diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.json b/test/spec/cloud-element-templates/properties/CustomProperties.json index a55a66b7..fd028a6d 100644 --- a/test/spec/cloud-element-templates/properties/CustomProperties.json +++ b/test/spec/cloud-element-templates/properties/CustomProperties.json @@ -706,5 +706,27 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "calledElement", + "name": "Payment", + "description": "Payment process call activity", + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "type": "String", + "value": "paymentProcess", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + } + } + ] } ] diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.spec.js b/test/spec/cloud-element-templates/properties/CustomProperties.spec.js index 054a48ed..6d34603f 100644 --- a/test/spec/cloud-element-templates/properties/CustomProperties.spec.js +++ b/test/spec/cloud-element-templates/properties/CustomProperties.spec.js @@ -928,6 +928,98 @@ describe('provider/cloud-element-templates - CustomProperties', function() { }); + describe.skip('zeebe:calledProcess', function() { + + + it('should display', async function() { + + // when + await expectSelected('MessageEvent'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('name'); + }); + + + it('should NOT display (type=hidden)', async function() { + + // when + await expectSelected('MessageEvent_hidden'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate_hidden-0', container); + + expect(entry).to.not.exist; + }); + + + it('should change, setting bpmn:Message#property (plain)', async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulMessageName'); + + // then + const message = findMessage(businessObject); + + expect(message).to.exist; + expect(message).to.have.property('name', 'meaningfulMessageName'); + }); + + + it('should change, creating bpmn:Message if non-existing', async function() { + + // given + const event = await expectSelected('MessageEvent_noData'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulMessageName'); + + // then + const message = findMessage(businessObject); + + // then + expect(message).to.exist; + expect(message).to.have.property('name', 'meaningfulMessageName'); + }); + + + it('should NOT remove bpmn:Message when changed to empty value', inject(async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const message = findMessage(businessObject); + + expect(message).to.exist; + expect(message).to.have.property('name', ''); + })); + }); + + describe('types', function() { describe('Dropdown', function() {