From 165f3adfaf7ead5db823a8bee757c20f12dc3567 Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Mon, 4 Dec 2023 13:26:48 +0100 Subject: [PATCH] feat: support `zeebe:calledElement` templating Related to https://github.com/camunda/camunda-modeler/issues/3006 --- .../CalledElementBehavior.js | 61 +++++++ src/cloud-element-templates/CreateHelper.js | 12 ++ .../cmd/ChangeElementTemplateHandler.js | 107 +++++++++++- .../create/CalledElementBindingProvider.js | 36 +++++ .../create/TemplateElementFactory.js | 7 +- src/cloud-element-templates/index.js | 7 +- .../util/bindingTypes.js | 4 +- .../util/propertyUtil.js | 42 ++++- .../CalledElementBehavior.bpmn | 37 +++++ .../CalledElementBehavior.json | 50 ++++++ .../CalledElementBehavior.spec.js | 152 ++++++++++++++++++ .../ElementTemplateConditionChecker.spec.js | 115 +++++++++++++ .../cmd/ChangeElementTemplateHandler.spec.js | 73 +++++++++ .../cmd/called-element.json | 23 +++ .../create/TemplateElementFactory.spec.js | 22 +++ .../create/TemplatesElementFactory.json | 23 +++ .../fixtures/called-element.json | 49 ++++++ .../fixtures/condition-called-element.json | 48 ++++++ .../properties/CustomProperties.bpmn | 16 +- .../properties/CustomProperties.json | 36 +++++ .../properties/CustomProperties.spec.js | 79 +++++++++ 21 files changed, 991 insertions(+), 8 deletions(-) create mode 100644 src/cloud-element-templates/CalledElementBehavior.js create mode 100644 src/cloud-element-templates/create/CalledElementBindingProvider.js create mode 100644 test/spec/cloud-element-templates/CalledElementBehavior.bpmn create mode 100644 test/spec/cloud-element-templates/CalledElementBehavior.json create mode 100644 test/spec/cloud-element-templates/CalledElementBehavior.spec.js create mode 100644 test/spec/cloud-element-templates/cmd/called-element.json create mode 100644 test/spec/cloud-element-templates/fixtures/called-element.json create mode 100644 test/spec/cloud-element-templates/fixtures/condition-called-element.json diff --git a/src/cloud-element-templates/CalledElementBehavior.js b/src/cloud-element-templates/CalledElementBehavior.js new file mode 100644 index 0000000..8d826a6 --- /dev/null +++ b/src/cloud-element-templates/CalledElementBehavior.js @@ -0,0 +1,61 @@ +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import { findExtension } from './Helper'; + +/** + * Enforces no variable propagation for templated call activities. + */ +export class CalledElementBehavior extends CommandInterceptor { + + /** + * @param {*} eventBus + * @param {*} modeling + * @param {import('./ElementTemplates').default} elementTemplates + */ + constructor(eventBus, modeling, elementTemplates) { + super(eventBus); + + this._modeling = modeling; + this._elementTemplates = elementTemplates; + + this.postExecuted([ + 'element.updateProperties', 'element.updateModdleProperties' + ], this._ensureNoPropagation, true, this); + } + + _ensureNoPropagation(context) { + const { element } = context; + + if (!this._elementTemplates.get(element)) { + return; + } + + if (!is(element, 'bpmn:CallActivity')) { + return; + } + + const calledElement = findExtension(element, 'zeebe:CalledElement'); + + if (!calledElement) { + return; + } + + for (const property of [ + 'propagateAllChildVariables', + 'propagateAllParentVariables' + ]) { + if (calledElement.get(property) !== false) { + this._modeling.updateModdleProperties(element, calledElement, { + [property]: false + }); + } + } + } +} + +CalledElementBehavior.$inject = [ + 'eventBus', + 'modeling', + 'elementTemplates' +]; diff --git a/src/cloud-element-templates/CreateHelper.js b/src/cloud-element-templates/CreateHelper.js index 1affadc..62f301e 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 e720b6c..f704570 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 0000000..f02476f --- /dev/null +++ b/src/cloud-element-templates/create/CalledElementBindingProvider.js @@ -0,0 +1,36 @@ +import { + ensureExtension +} from '../CreateHelper'; +import { getDefaultValue } from '../Helper'; + + +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); + } +} + +function ensureNoPropagation(calledElement) { + calledElement.set('propagateAllChildVariables', false); + calledElement.set('propagateAllParentVariables', false); +} diff --git a/src/cloud-element-templates/create/TemplateElementFactory.js b/src/cloud-element-templates/create/TemplateElementFactory.js index 663ecef..add9406 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/index.js b/src/cloud-element-templates/index.js index 9dd3229..14fcc67 100644 --- a/src/cloud-element-templates/index.js +++ b/src/cloud-element-templates/index.js @@ -10,6 +10,7 @@ import ElementTemplatesPropertiesProvider from './ElementTemplatesPropertiesProv import UpdateTemplatePropertiesOrder from './UpdateTemplatePropertiesOrder'; import { ReferencedElementBehavior } from './ReferencedElementBehavior'; import { GeneratedValueBehavior } from './GeneratedValueBehavior'; +import { CalledElementBehavior } from './CalledElementBehavior'; export default { @@ -25,7 +26,8 @@ export default { 'elementTemplatesConditionChecker', 'generatedValueBehavior', 'referencedElementBehavior', - 'updateTemplatePropertiesOrder' + 'updateTemplatePropertiesOrder', + 'calledElementBehavior' ], elementTemplates: [ 'type', ElementTemplates ], elementTemplatesLoader: [ 'type', ElementTemplatesLoader ], @@ -34,5 +36,6 @@ export default { elementTemplatesConditionChecker: [ 'type', ElementTemplatesConditionChecker ], generatedValueBehavior: [ 'type', GeneratedValueBehavior ], referencedElementBehavior: [ 'type', ReferencedElementBehavior ], - updateTemplatePropertiesOrder: [ 'type', UpdateTemplatePropertiesOrder ] + updateTemplatePropertiesOrder: [ 'type', UpdateTemplatePropertiesOrder ], + calledElementBehavior: [ 'type', CalledElementBehavior ] }; diff --git a/src/cloud-element-templates/util/bindingTypes.js b/src/cloud-element-templates/util/bindingTypes.js index 406d95c..77bd8ed 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/propertyUtil.js b/src/cloud-element-templates/util/propertyUtil.js index 60b98c1..401efde 100644 --- a/src/cloud-element-templates/util/propertyUtil.js +++ b/src/cloud-element-templates/util/propertyUtil.js @@ -25,7 +25,8 @@ import { ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE, ZEEBE_PROPERTY_TYPE, - ZEEBE_TASK_HEADER_TYPE + ZEEBE_TASK_HEADER_TYPE, + ZEEBE_CALLED_ELEMENT } from './bindingTypes'; import { @@ -194,6 +195,13 @@ export function getPropertyValue(element, property, scope) { return defaultValue; } + // zeebe:calledElement + if (type === ZEEBE_CALLED_ELEMENT) { + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + return calledElement ? calledElement.get(bindingProperty) : defaultValue; + } + // should never throw as templates are validated beforehand throw unknownBindingError(element, property); } @@ -528,6 +536,38 @@ export function setPropertyValue(bpmnFactory, commandStack, element, property, v } } + // zeebe:calledElement + if (type === ZEEBE_CALLED_ELEMENT) { + let calledElement = findExtension(element, 'zeebe:CalledElement'); + const propertyName = binding.property; + + const properties = { + [ propertyName ]: value || '' + }; + + if (calledElement) { + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + properties, + moddleElement: calledElement + } + }); + } else { + calledElement = createElement('zeebe:CalledElement', properties, extensionElements, bpmnFactory); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + ...context, + moddleElement: extensionElements, + properties: { values: [ ...extensionElements.get('values'), calledElement ] } + } + }); + } + } + if (commands.length) { const commandsToExecute = commands.filter((command) => command !== NO_OP); diff --git a/test/spec/cloud-element-templates/CalledElementBehavior.bpmn b/test/spec/cloud-element-templates/CalledElementBehavior.bpmn new file mode 100644 index 0000000..12d6cb6 --- /dev/null +++ b/test/spec/cloud-element-templates/CalledElementBehavior.bpmn @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/cloud-element-templates/CalledElementBehavior.json b/test/spec/cloud-element-templates/CalledElementBehavior.json new file mode 100644 index 0000000..5f2dc66 --- /dev/null +++ b/test/spec/cloud-element-templates/CalledElementBehavior.json @@ -0,0 +1,50 @@ +[ + { + "$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", + "bpmn:CallActivity" + ], + "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/CalledElementBehavior.spec.js b/test/spec/cloud-element-templates/CalledElementBehavior.spec.js new file mode 100644 index 0000000..b9ecbfc --- /dev/null +++ b/test/spec/cloud-element-templates/CalledElementBehavior.spec.js @@ -0,0 +1,152 @@ +import TestContainer from 'mocha-test-container-support'; +import coreModule from 'bpmn-js/lib/core'; +import modelingModule from 'bpmn-js/lib/features/modeling'; +import { BpmnPropertiesPanelModule } from 'bpmn-js-properties-panel'; +import zeebeModdlePackage from 'zeebe-bpmn-moddle/resources/zeebe'; +import ZeebeBehaviorsModule from 'camunda-bpmn-js-behaviors/lib/camunda-cloud'; + +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + +import elementTemplatesModule from 'src/cloud-element-templates'; +import { findExtension } from 'src/cloud-element-templates/Helper'; + +import diagramXML from './CalledElementBehavior.bpmn'; +import templates from './CalledElementBehavior.json'; + + +describe('CalledElementBehavior', function() { + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + + beforeEach(bootstrapModeler(diagramXML, { + container, + modules: [ + coreModule, + elementTemplatesModule, + modelingModule, + BpmnPropertiesPanelModule, + { + propertiesPanel: [ 'value', { registerProvider() {} } ] + }, + ZeebeBehaviorsModule + ], + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + elementTemplates: templates + })); + + + describe('variable propagation', function() { + + it('should ensure no variable propagation when template is applied', inject( + function(elementRegistry, elementTemplates) { + + // given + let callActivity = elementRegistry.get('Task'); + + // when + callActivity = elementTemplates.applyTemplate(callActivity, templates[0]); + + // then + expect(getPropagation(callActivity)).to.eql({ + propagateAllChildVariables: false, + propagateAllParentVariables: false + }); + }) + ); + + + it('should ensure no variable propagation when property is activated via condition', inject( + function(elementRegistry, modeling) { + + // given + const callActivity = elementRegistry.get('TemplatedNoProperty'); + + // when + modeling.updateProperties(callActivity, { + name: 'foo' + }); + + expect(getPropagation(callActivity)).to.eql({ + propagateAllChildVariables: false, + propagateAllParentVariables: false + }); + }) + ); + + + it('should NOT create called element if property is deactivated', inject( + function(elementRegistry, modeling) { + + // given + const callActivity = elementRegistry.get('Templated'); + + // when + modeling.updateProperties(callActivity, { + name: 'baz' + }); + + expect(getPropagation(callActivity)).to.be.null; + }) + ); + + + it('should NOT affect non-templated call activities', inject( + function(elementRegistry, modeling) { + + // given + const callActivity = elementRegistry.get('NonTemplated'); + const initialPropagation = getPropagation(callActivity); + + // when + modeling.updateProperties(callActivity, { + name: 'foo' + }); + + // then + expect(getPropagation(callActivity)).to.eql(initialPropagation); + }) + ); + + + it('should NOT affect non-call-activity', inject( + function(elementRegistry, modeling) { + + // given + const task = elementRegistry.get('Task'); + + // when + modeling.updateProperties(task, { + name: 'newName' + }); + + // then + expect(getPropagation(task)).to.be.null; + }) + ); + }); +}); + + +// helpers ////////// + +function getCalledElement(element) { + return findExtension(element, 'zeebe:CalledElement'); +} + +function getPropagation(callActivity) { + const calledElement = getCalledElement(callActivity); + return calledElement ? { + propagateAllChildVariables: calledElement.get('propagateAllChildVariables'), + propagateAllParentVariables: calledElement.get('propagateAllParentVariables') + } : null; +} diff --git a/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js b/test/spec/cloud-element-templates/ElementTemplateConditionChecker.spec.js index 1a3b575..24a322f 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,119 @@ describe('provider/cloud-element-templates - ElementTemplatesConditionChecker', }); + describe('update zeebe:calledElement', function() { + + it('should add conditional entries', inject( + async function(elementRegistry, modeling) { + + // given + const element = changeTemplate(elementRegistry.get('Task_1'), calledElementTemplate); + const businessObject = getBusinessObject(element); + + // when + modeling.updateProperties(element, { + name: 'foo' + }); + + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + // then + expect(calledElement).to.have.property('processId', 'one'); + }) + ); + + + it('should remove conditional entries', inject( + async function(elementRegistry, modeling) { + + // given + const element = changeTemplate(elementRegistry.get('Task_1'), 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 = changeTemplate(elementRegistry.get('Task_1'), 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.have.property('processId', 'two'); + }) + ); + + + it('undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + const element = changeTemplate(elementRegistry.get('Task_1'), 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 = changeTemplate(elementRegistry.get('Task_1'), 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.have.property('processId', 'one'); + })); + + }); + + 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 29adc24..da8d918 100644 --- a/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js +++ b/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js @@ -1700,6 +1700,79 @@ describe('cloud-element-templates/cmd - ChangeElementTemplateHandler', function( }); + describe('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.have.property('processId', 'paymentProcess'); + expect(calledElement).to.have.property('propagateAllChildVariables', false); + expect(calledElement).to.have.property('propagateAllParentVariables', 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.have.property('processId', 'paymentProcess'); + expect(calledElement).to.have.property('propagateAllChildVariables', false); + expect(calledElement).to.have.property('propagateAllParentVariables', 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 0000000..2496d43 --- /dev/null +++ b/test/spec/cloud-element-templates/cmd/called-element.json @@ -0,0 +1,23 @@ +{ + "$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", + "bpmn:CallActivity" + ], + "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 e08af3d..c55b45a 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 b84a5dd..3ad34db 100644 --- a/test/spec/cloud-element-templates/create/TemplatesElementFactory.json +++ b/test/spec/cloud-element-templates/create/TemplatesElementFactory.json @@ -512,5 +512,28 @@ } } ] + }, + { + "$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", + "bpmn:CallActivity" + ], + "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 0000000..acb27d2 --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/called-element.json @@ -0,0 +1,49 @@ +{ + "$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", + "bpmn:CallActivity" + ], + "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 0000000..0078112 --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/condition-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", + "bpmn:CallActivity" + ], + "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 ad333f5..e1679eb 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,12 @@ + + + + + + @@ -132,6 +138,14 @@ + + + + + + + + diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.json b/test/spec/cloud-element-templates/properties/CustomProperties.json index a55a66b..0e591dc 100644 --- a/test/spec/cloud-element-templates/properties/CustomProperties.json +++ b/test/spec/cloud-element-templates/properties/CustomProperties.json @@ -706,5 +706,41 @@ } } ] + }, + { + "$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", + "bpmn:CallActivity" + ], + "elementType": { + "value": "bpmn:CallActivity" + }, + "properties":[ + { + "type": "String", + "value": "paymentProcess", + "binding": { + "type": "zeebe:calledElement", + "property": "processId" + }, + "condition": { + "property": "name", + "equals": "Called Element" + } + }, + { + "id": "name", + "type": "String", + "value": "Called Element", + "binding": { + "type": "property", + "name": "name" + } + } + ] } ] diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.spec.js b/test/spec/cloud-element-templates/properties/CustomProperties.spec.js index 054a48e..1efd063 100644 --- a/test/spec/cloud-element-templates/properties/CustomProperties.spec.js +++ b/test/spec/cloud-element-templates/properties/CustomProperties.spec.js @@ -928,6 +928,85 @@ describe('provider/cloud-element-templates - CustomProperties', function() { }); + describe('zeebe:calledElement', function() { + + + it('should display', async function() { + + // when + await expectSelected('CalledElement'); + + // then + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('paymentProcess'); + }); + + + it('should change, setting zeebe:calledElement', async function() { + + // given + const element = await expectSelected('CalledElement'), + businessObject = getBusinessObject(element); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, 'anotherProcessId'); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', 'anotherProcessId'); + }); + + + it('should change, creating zeebe:calledElement if non-existing', async function() { + + // given + const element = await expectSelected('CalledElement_empty'), + businessObject = getBusinessObject(element); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, 'Called Element'); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', 'paymentProcess'); + }); + + + it('should NOT remove zeebe:calledElement when changed to empty value', inject(async function() { + + // given + const event = await expectSelected('CalledElement'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', ''); + })); + }); + + describe('types', function() { describe('Dropdown', function() {