diff --git a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js index c3ba27c5..8d2ce5b0 100644 --- a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js +++ b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js @@ -33,7 +33,8 @@ import { MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, TASK_DEFINITION_TYPES, - ZEEBE_CALLED_ELEMENT + ZEEBE_CALLED_ELEMENT, + ZEEBE_LINKED_RESOURCE_PROPERTY } from '../util/bindingTypes'; import { @@ -107,6 +108,8 @@ export default class ChangeElementTemplateHandler { this._updateMessage(element, oldTemplate, newTemplate); this._updateCalledElement(element, oldTemplate, newTemplate); + + this._updateLinkedResources(element, oldTemplate, newTemplate); } } @@ -1032,6 +1035,118 @@ export default class ChangeElementTemplateHandler { return replacedElement; } + + + _updateLinkedResources(element, oldTemplate, newTemplate) { + const bpmnFactory = this._bpmnFactory, + commandStack = this._commandStack; + + const newLinkedResources = newTemplate.properties.filter((newProperty) => { + const newBinding = newProperty.binding, + newBindingType = newBinding.type; + + return newBindingType === 'zeebe:linkedResource#property'; + }); + + const extensionElements = this._getOrCreateExtensionElements(element); + + let linkedResources = findExtension(extensionElements, 'zeebe:LinkedResources'); + + // (1) remove linkedResourcess if no new specified + if (!newLinkedResources.length) { + if (!linkedResources) { + return; + } + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: extensionElements, + properties: { + values: without(extensionElements.get('values'), linkedResources) + } + }); + return; + } + + if (!linkedResources) { + linkedResources = bpmnFactory.create('zeebe:LinkedResources'); + + linkedResources.$parent = extensionElements; + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: extensionElements, + properties: { + values: [ ...extensionElements.get('values'), linkedResources ] + } + }); + } + + const oldLinkedResources = linkedResources.get('values')?.slice() || []; + + newLinkedResources.forEach((newLinkedResource) => { + const oldProperty = findOldProperty(oldTemplate, newLinkedResource), + oldLinkedResource = findBusinessObject(extensionElements, newLinkedResource), + newPropertyValue = getDefaultValue(newLinkedResource), + newBinding = newLinkedResource.binding; + + // (2) update old LinkesResources + if (oldLinkedResource) { + + // debugger; + console.log(oldLinkedResource, oldLinkedResource, oldProperty, newLinkedResource, shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource)); + + if ( + shouldUpdate(newPropertyValue, newLinkedResource) + || shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource) + ) { + remove(oldLinkedResources, oldLinkedResource); + } + + if (!shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource)) { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: oldLinkedResource, + properties: { + [newBinding.property]: newPropertyValue + } + }); + } + } + + // (3) add new linkedResources + else if (shouldUpdate(newPropertyValue, newLinkedResource)) { + const newProperties = { + linkName: newBinding.linkName, + [newBinding.property]: newPropertyValue + }; + + const newLinkedResource = createElement('zeebe:LinkedResource', newProperties, extensionElements, bpmnFactory); + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: linkedResources, + properties: { + values: [ ...linkedResources.get('values'), newLinkedResource ] + } + }); + } + }); + + + // (4) remove old linkedResources + // TODO: remove single properties as well as complete linkedResources elements + if (oldLinkedResources.length) { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: linkedResources, + properties: { + properties: without(linkedResources.get('values'), linkedResource => oldLinkedResources.includes(linkedResource)) + } + }); + } + } + } ChangeElementTemplateHandler.$inject = [ @@ -1106,6 +1221,18 @@ function findBusinessObject(element, property) { return value.get('name') === binding.name; }); } + + if (bindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) { + const linkedResources = findExtension(businessObject, 'zeebe:LinkedResources'); + + if (!linkedResources) { + return; + } + + return linkedResources.get('values').find((value) => { + return value.get('linkName') === binding.linkName; + }); + } } /** @@ -1224,6 +1351,19 @@ export function findOldProperty(oldTemplate, newProperty) { return oldBinding.name === newBinding.name; }); } + + if (newBindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) { + return oldProperties.find(oldProperty => { + const oldBinding = oldProperty.binding, + oldBindingType = oldBinding.type; + + if (oldBindingType !== ZEEBE_LINKED_RESOURCE_PROPERTY) { + return; + } + + return oldBinding.linkName === newBinding.linkName && oldBinding.property === newBinding.property; + }); + } } /** @@ -1289,7 +1429,8 @@ function getPropertyValue(element, property) { const binding = property.binding, bindingName = binding.name, - bindingType = binding.type; + bindingType = binding.type, + bindingProperty = binding.property; if (bindingType === 'property') { @@ -1323,6 +1464,10 @@ function getPropertyValue(element, property) { if (bindingType === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) { return businessObject.get(bindingName); } + + if (bindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) { + return businessObject.get(bindingProperty); + } } function remove(array, item) { diff --git a/src/cloud-element-templates/create/LinkedResourceProvider.js b/src/cloud-element-templates/create/LinkedResourceProvider.js new file mode 100644 index 00000000..4f569f6d --- /dev/null +++ b/src/cloud-element-templates/create/LinkedResourceProvider.js @@ -0,0 +1,41 @@ +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; +import { createElement } from '../../utils/ElementUtil'; +import { + ensureExtension +} from '../CreateHelper'; +import { getDefaultValue } from '../Helper'; + +import { + getTaskDefinitionPropertyName +} from '../util/taskDefinition'; + + +export default class LinkedResourcePropertyBindingProvider { + static create(element, options) { + const { + property, + bpmnFactory + } = options; + + const { + binding: { + property: bindingProperty, + linkName + } + } = property; + + const value = getDefaultValue(property); + + const bo = getBusinessObject(element); + const linkedResources = ensureExtension(element, 'zeebe:LinkedResources', bpmnFactory); + + let linkedResource = linkedResources.get('values').find(linkedResource => linkedResource.get('linkName') === linkName); + + if (!linkedResource) { + linkedResource = createElement('zeebe:LinkedResource', { linkName }, bo, bpmnFactory); + linkedResources.get('values').push(linkedResource); + } + + linkedResource.set(bindingProperty, value); + } +} diff --git a/src/cloud-element-templates/create/TemplateElementFactory.js b/src/cloud-element-templates/create/TemplateElementFactory.js index add94066..deb441e6 100644 --- a/src/cloud-element-templates/create/TemplateElementFactory.js +++ b/src/cloud-element-templates/create/TemplateElementFactory.js @@ -13,6 +13,7 @@ import ZeebePropertiesProvider from './ZeebePropertiesProvider'; import { MessagePropertyBindingProvider } from './MessagePropertyBindingProvider'; import { MessageZeebeSubscriptionBindingProvider } from './MessageZeebeSubscriptionBindingProvider'; import { CalledElementBindingProvider } from './CalledElementBindingProvider'; +import LinkedResourcePropertyBindingProvider from './LinkedResourceProvider'; import { MESSAGE_PROPERTY_TYPE, @@ -24,7 +25,8 @@ import { ZEEBE_OUTPUT_TYPE, ZEEBE_TASK_HEADER_TYPE, ZEBBE_PROPERTY_TYPE, - ZEEBE_CALLED_ELEMENT + ZEEBE_CALLED_ELEMENT, + ZEEBE_LINKED_RESOURCE_PROPERTY } from '../util/bindingTypes'; import { @@ -47,7 +49,8 @@ export default class TemplateElementFactory { [ZEEBE_TASK_HEADER_TYPE]: TaskHeaderBindingProvider, [MESSAGE_PROPERTY_TYPE]: MessagePropertyBindingProvider, [MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]: MessageZeebeSubscriptionBindingProvider, - [ZEEBE_CALLED_ELEMENT]: CalledElementBindingProvider + [ZEEBE_CALLED_ELEMENT]: CalledElementBindingProvider, + [ZEEBE_LINKED_RESOURCE_PROPERTY]: LinkedResourcePropertyBindingProvider }; } diff --git a/src/cloud-element-templates/util/bindingTypes.js b/src/cloud-element-templates/util/bindingTypes.js index 77bd8edc..0a509484 100644 --- a/src/cloud-element-templates/util/bindingTypes.js +++ b/src/cloud-element-templates/util/bindingTypes.js @@ -10,6 +10,7 @@ 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 ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource#property'; export const EXTENSION_BINDING_TYPES = [ MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, @@ -19,7 +20,8 @@ export const EXTENSION_BINDING_TYPES = [ ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION, ZEEBE_TASK_HEADER_TYPE, - ZEEBE_CALLED_ELEMENT + ZEEBE_CALLED_ELEMENT, + ZEEBE_LINKED_RESOURCE_PROPERTY ]; export const TASK_DEFINITION_TYPES = [ diff --git a/src/cloud-element-templates/util/propertyUtil.js b/src/cloud-element-templates/util/propertyUtil.js index 221fe185..09203694 100644 --- a/src/cloud-element-templates/util/propertyUtil.js +++ b/src/cloud-element-templates/util/propertyUtil.js @@ -22,7 +22,8 @@ import { ZEEBE_OUTPUT_TYPE, ZEEBE_PROPERTY_TYPE, ZEEBE_TASK_HEADER_TYPE, - ZEEBE_CALLED_ELEMENT + ZEEBE_CALLED_ELEMENT, + ZEEBE_LINKED_RESOURCE_PROPERTY } from './bindingTypes'; import { @@ -81,7 +82,8 @@ function getRawPropertyValue(element, property, scope) { const { name, property: bindingProperty, - type + type, + linkName } = binding; // property @@ -210,6 +212,18 @@ function getRawPropertyValue(element, property, scope) { return calledElement ? calledElement.get(bindingProperty) : defaultValue; } + if (type === ZEEBE_LINKED_RESOURCE_PROPERTY) { + const linkedResources = findExtension(businessObject, 'zeebe:LinkedResources'); + + if (!linkedResources) { + return defaultValue; + } + + const linkedResource = linkedResources.get('values').find((value) => value.get('linkName') === linkName); + + return linkedResource ? linkedResource.get(bindingProperty) : defaultValue; + } + // should never throw as templates are validated beforehand throw unknownBindingError(element, property); } @@ -242,7 +256,9 @@ export function setPropertyValue(bpmnFactory, commandStack, element, property, v const { name, - type + type, + property: bindingProperty, + linkName } = binding; let extensionElements; @@ -593,6 +609,47 @@ export function setPropertyValue(bpmnFactory, commandStack, element, property, v } } + if (type === ZEEBE_LINKED_RESOURCE_PROPERTY) { + let linkedResources = findExtension(businessObject, 'zeebe:LinkedResources'); + + if (!linkedResources) { + linkedResources = createElement('zeebe:LinkedResources', null, businessObject, bpmnFactory); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + ...context, + moddleElement: extensionElements, + properties: { values: [ ...extensionElements.get('values'), linkedResources ] } + } + }); + } + + let linkedResource = linkedResources.get('values').find((value) => value.get('linkName') === linkName); + + if (!linkedResource) { + linkedResource = createElement('zeebe:LinkedResource', { linkName }, businessObject, bpmnFactory); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + ...context, + moddleElement: linkedResources, + properties: { values: [ ...linkedResources.get('values'), linkedResource ] } + } + }); + } + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + ...context, + moddleElement: linkedResource, + properties: { [ bindingProperty ]: value } + } + }); + } + if (commands.length) { const commandsToExecute = commands.filter((command) => command !== NO_OP); diff --git a/test/spec/cloud-element-templates/fixtures/linkedResource.json b/test/spec/cloud-element-templates/fixtures/linkedResource.json new file mode 100644 index 00000000..afbb8ddb --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/linkedResource.json @@ -0,0 +1,96 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "linkedResource", + "id": "linkedResource", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "properties": [ + { + "type": "Hidden", + "value": "RPA", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "resourceType" + } + }, + { + "label": "Script ID", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "resourceId" + } + }, + { + "label": "Binding", + "type": "Dropdown", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "bindingType" + }, + "choices": [ + { "name": "latest", "value": "latest" }, + { "name": "deployment", "value": "deployment" }, + { "name": "version tag", "value": "versionTag" } + ] + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "linkedResource v2", + "id": "linkedResource", + "version": 2, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "properties": [ + { + "type": "Hidden", + "value": "RPAv2", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "resourceType" + } + }, + { + "label": "Script ID", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "resourceId" + } + }, + { + "label": "Binding", + "type": "Dropdown", + "binding": { + "type": "zeebe:linkedResource#property", + "linkName": "entryPoint", + "property": "bindingType" + }, + "choices": [ + { "name": "latest", "value": "latest" }, + { "name": "deployment", "value": "deployment" }, + { "name": "version tag", "value": "versionTag" } + ] + } + ] + } +] \ No newline at end of file