diff --git a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js index 8d2ce5b0..45aca061 100644 --- a/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js +++ b/src/cloud-element-templates/cmd/ChangeElementTemplateHandler.js @@ -24,7 +24,6 @@ import { import { find, - isUndefined, without } from 'min-dash'; @@ -1045,7 +1044,7 @@ export default class ChangeElementTemplateHandler { const newBinding = newProperty.binding, newBindingType = newBinding.type; - return newBindingType === 'zeebe:linkedResource#property'; + return newBindingType === 'zeebe:linkedResource'; }); const extensionElements = this._getOrCreateExtensionElements(element); @@ -1082,7 +1081,8 @@ export default class ChangeElementTemplateHandler { }); } - const oldLinkedResources = linkedResources.get('values')?.slice() || []; + const unusedLinkedResources = linkedResources.get('values')?.slice() || []; + const unusedResourceProperties = oldTemplate?.properties.slice() || []; newLinkedResources.forEach((newLinkedResource) => { const oldProperty = findOldProperty(oldTemplate, newLinkedResource), @@ -1090,17 +1090,17 @@ export default class ChangeElementTemplateHandler { newPropertyValue = getDefaultValue(newLinkedResource), newBinding = newLinkedResource.binding; + if (oldProperty) { + remove(unusedResourceProperties, oldProperty); + } + // (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); + remove(unusedLinkedResources, oldLinkedResource); } if (!shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource)) { @@ -1134,17 +1134,36 @@ export default class ChangeElementTemplateHandler { }); - // (4) remove old linkedResources - // TODO: remove single properties as well as complete linkedResources elements - if (oldLinkedResources.length) { + // (4) remove unused linkedResources + if (unusedLinkedResources.length) { commandStack.execute('element.updateModdleProperties', { element, moddleElement: linkedResources, properties: { - properties: without(linkedResources.get('values'), linkedResource => oldLinkedResources.includes(linkedResource)) + values: without(linkedResources.get('values'), linkedResource => unusedLinkedResources.includes(linkedResource)) } }); } + + // (5) remove unused resource properties + unusedResourceProperties.forEach((unusedResourceProperty) => { + const oldLinkedResource = findBusinessObject(extensionElements, unusedResourceProperty); + + const oldBinding = unusedResourceProperty.binding; + + // No property was reused and element was removed in previous step + if (!oldLinkedResource) { + return; + } + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: oldLinkedResource, + properties: { + [oldBinding.property]: undefined + } + }); + }); } } @@ -1473,7 +1492,7 @@ function getPropertyValue(element, property) { function remove(array, item) { const index = array.indexOf(item); - if (isUndefined(index)) { + if (index < 0) { return array; } diff --git a/src/cloud-element-templates/util/bindingTypes.js b/src/cloud-element-templates/util/bindingTypes.js index 0a509484..d560c425 100644 --- a/src/cloud-element-templates/util/bindingTypes.js +++ b/src/cloud-element-templates/util/bindingTypes.js @@ -10,7 +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 ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource'; export const EXTENSION_BINDING_TYPES = [ MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, diff --git a/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js b/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js index cde2f7fc..c86aeb88 100644 --- a/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js +++ b/test/spec/cloud-element-templates/cmd/ChangeElementTemplateHandler.spec.js @@ -1952,6 +1952,172 @@ describe('cloud-element-templates/cmd - ChangeElementTemplateHandler', function( })); }); + + describe('update zeebe:LinkedElement', function() { + + beforeEach(bootstrap(require('./linked-resource.bpmn').default)); + + + describe('zeebe:LinekedElement specified', function() { + const newTemplate = require('./linked-resource.json')[1]; + + it('execute', inject(function(elementRegistry) { + + // given + let task = elementRegistry.get('noResources'); + + // when + changeTemplate(task, newTemplate); + + // then + task = elementRegistry.get('noResources'); + expectElementTemplate(task, 'linkedResource'); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).to.exist; + + const linkedResource = linkedResources.get('values')[0]; + expect(linkedResource).to.exist; + expect(linkedResource).to.have.property('linkName', 'persistedLink'); + expect(linkedResource).to.have.property('resourceType', 'RPA'); + expect(linkedResource).to.have.property('resourceId', 'changed'); + })); + + + it('undo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('noResources'); + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + + // then + task = elementRegistry.get('noResources'); + expectNoElementTemplate(task); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + expect(linkedResources).not.to.exist; + })); + + + it('redo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('noResources'); + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + commandStack.redo(); + + // then + task = elementRegistry.get('noResources'); + expectElementTemplate(task, 'linkedResource'); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).to.exist; + + const linkedResource = linkedResources.get('values')[0]; + expect(linkedResource).to.exist; + expect(linkedResource).to.have.property('linkName', 'persistedLink'); + expect(linkedResource).to.have.property('resourceType', 'RPA'); + expect(linkedResource).to.have.property('resourceId', 'changed'); + })); + + + it('should keep values', inject(function(elementRegistry) { + + // given + let task = elementRegistry.get('withResources'); + + // when + changeTemplate(task, newTemplate); + + // then + task = elementRegistry.get('withResources'); + expectElementTemplate(task, 'linkedResource'); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).to.exist; + + const linkedResource = linkedResources.get('values')[0]; + expect(linkedResource).to.exist; + expect(linkedResource).to.have.property('linkName', 'persistedLink'); + expect(linkedResource).to.have.property('resourceType', 'originalType'); + expect(linkedResource).to.have.property('resourceId', 'originalResource'); + })); + + }); + + describe('zeebe:LinkedElement not specified', function() { + const newTemplate = require('./task-template-no-properties.json'); + + it('execute', inject(function(elementRegistry) { + + // given + let task = elementRegistry.get('withResources'); + + // when + changeTemplate(task, newTemplate); + + // then + task = elementRegistry.get('withResources'); + expectElementTemplate(task, 'task-template-no-properties'); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).not.to.exist; + })); + + + it('undo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('withResources'); + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + + // then + task = elementRegistry.get('withResources'); + expectNoElementTemplate(task); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).to.exist; + })); + + + it('redo', inject(function(commandStack, elementRegistry) { + + // given + let task = elementRegistry.get('withResources'); + changeTemplate(task, newTemplate); + + // when + commandStack.undo(); + commandStack.redo(); + + // then + task = elementRegistry.get('withResources'); + expectElementTemplate(task, 'task-template-no-properties'); + + const linkedResources = findExtension(task, 'zeebe:LinkedResources'); + + expect(linkedResources).not.to.exist; + })); + + + }); + + }); + }); @@ -4011,6 +4177,210 @@ describe('cloud-element-templates/cmd - ChangeElementTemplateHandler', function( }); + describe('update zeebe:LinkedResource', function() { + beforeEach(bootstrap(require('./linked-resource.bpmn').default)); + + it('property changed', inject(function(elementRegistry) { + + // given + const serviceTask = elementRegistry.get('noResources'); + + const oldTemplate = createTemplate([ + { + value: 'property-1-old-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceId' + } + }, + { + value: 'property-2-old-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceType' + } + } + ]); + + const newTemplate = createTemplate([ + { + value: 'property-1-new-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceId' + } + }, + { + value: 'property-2-new-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceType' + } + } + ]); + + changeTemplate('noResources', oldTemplate); + + let linkedResource = getLinkedResource(serviceTask, 'resource1'); + + updateBusinessObject('noResources', linkedResource, { + resourceId: 'property-1-changed-value' + }); + + // when + changeTemplate(serviceTask, newTemplate, oldTemplate); + + // then + linkedResource = getLinkedResource(serviceTask, 'resource1'); + + expect(linkedResource).to.exist; + expect(linkedResource).to.jsonEqual( + { + $type: 'zeebe:LinkedResource', + linkName: 'resource1', + resourceId: 'property-1-changed-value', + resourceType: 'property-2-new-value', + } + ); + })); + + + it('property unchanged', inject(function(elementRegistry) { + + // given + const serviceTask = elementRegistry.get('noResources'); + + const oldTemplate = createTemplate([ + { + value: 'property-1-old-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceId' + } + }, + { + value: 'property-2-old-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceType' + } + } + ]); + + const newTemplate = createTemplate([ + { + value: 'property-1-new-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceId' + } + }, + { + value: 'property-2-new-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'resource1', + property: 'resourceType' + } + } + ]); + + changeTemplate(serviceTask, oldTemplate); + + // when + changeTemplate(serviceTask, newTemplate, oldTemplate); + + // then + const linkedResource = getLinkedResource(serviceTask, 'resource1'); + + expect(linkedResource).to.exist; + expect(linkedResource).to.jsonEqual( + { + $type: 'zeebe:LinkedResource', + linkName: 'resource1', + resourceId: 'property-1-new-value', + resourceType: 'property-2-new-value', + } + ); + })); + + + it('complex', inject(function(elementRegistry) { + + // given + const serviceTask = elementRegistry.get('noResources'); + + const oldTemplate = createTemplate([ + { + value: 'old-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'changed-resource', + property: 'resourceId' + } + }, + { + value: 'removed-property', + binding: { + type: 'zeebe:linkedResource', + linkName: 'changed-resource', + property: 'resourceType' + } + }, + { + value: 'removed-resource', + binding: { + type: 'zeebe:linkedResource', + linkName: 'removed-resource', + property: 'resourceType' + } + } + ]); + + const newTemplate = createTemplate([ + { + value: 'new-value', + binding: { + type: 'zeebe:linkedResource', + linkName: 'changed-resource', + property: 'resourceId' + } + } + ]); + + changeTemplate(serviceTask, oldTemplate); + + // when + changeTemplate(serviceTask, newTemplate, oldTemplate); + + // then + const linkedResources = findExtension(serviceTask, 'zeebe:LinkedResources'); + + expect(linkedResources).to.exist; + expect(linkedResources.get('values')).to.have.length(1); + + const linkedResource = getLinkedResource(serviceTask, 'changed-resource'); + + expect(linkedResource).to.exist; + expect(linkedResource).to.jsonEqual( + { + $type: 'zeebe:LinkedResource', + linkName: 'changed-resource', + resourceId: 'new-value', + } + ); + })); + + }); + + describe('update bpmn:Message', function() { beforeEach(bootstrap(require('./event.bpmn').default)); @@ -4431,6 +4801,14 @@ function getZeebeProperty(element, name) { }); } +function getLinkedResource(element, linkName) { + const linkedResources = findExtension(element, 'zeebe:LinkedResources'); + + return linkedResources.get('values').find((resource) => { + return resource.get('linkName') === linkName; + }); +} + function updateBusinessObject(element, businessObject, properties) { getBpmnJS().invoke(function(commandStack, elementRegistry) { if (isString(element)) { diff --git a/test/spec/cloud-element-templates/cmd/linked-resource.bpmn b/test/spec/cloud-element-templates/cmd/linked-resource.bpmn new file mode 100644 index 00000000..0b87f679 --- /dev/null +++ b/test/spec/cloud-element-templates/cmd/linked-resource.bpmn @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/cloud-element-templates/cmd/linked-resource.json b/test/spec/cloud-element-templates/cmd/linked-resource.json new file mode 100644 index 00000000..d0d37607 --- /dev/null +++ b/test/spec/cloud-element-templates/cmd/linked-resource.json @@ -0,0 +1,85 @@ +[ + { + "$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": "String", + "value": "RPA", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "persistedLink", + "property": "resourceType" + } + }, + { + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "persistedLink", + "property": "resourceId" + } + }, + { + "type": "String", + "value": "latest", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "persistedLink", + "property": "bindingType" + } + }, + { + "type": "String", + "value": "RPA", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "removedLink", + "property": "resourceType" + } + } + ] + }, + { + "$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": "String", + "value": "RPA", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "persistedLink", + "property": "resourceType" + } + }, + { + "type": "String", + "feel": "optional", + "value": "changed", + "binding": { + "type": "zeebe:linkedResource", + "linkName": "persistedLink", + "property": "resourceId" + } + } + ] + } +] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/fixtures/linkedResource.json b/test/spec/cloud-element-templates/fixtures/linked-resource.json similarity index 87% rename from test/spec/cloud-element-templates/fixtures/linkedResource.json rename to test/spec/cloud-element-templates/fixtures/linked-resource.json index afbb8ddb..50ff945e 100644 --- a/test/spec/cloud-element-templates/fixtures/linkedResource.json +++ b/test/spec/cloud-element-templates/fixtures/linked-resource.json @@ -15,7 +15,7 @@ "type": "Hidden", "value": "RPA", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "resourceType" } @@ -25,7 +25,7 @@ "type": "String", "feel": "optional", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "resourceId" } @@ -34,7 +34,7 @@ "label": "Binding", "type": "Dropdown", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "bindingType" }, @@ -62,7 +62,7 @@ "type": "Hidden", "value": "RPAv2", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "resourceType" } @@ -72,7 +72,7 @@ "type": "String", "feel": "optional", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "resourceId" } @@ -81,7 +81,7 @@ "label": "Binding", "type": "Dropdown", "binding": { - "type": "zeebe:linkedResource#property", + "type": "zeebe:linkedResource", "linkName": "entryPoint", "property": "bindingType" },