From a9b68b69e0569b218b5a6f89c9272817d1e28f35 Mon Sep 17 00:00:00 2001 From: Ricardo Matias Date: Wed, 11 Jan 2017 15:22:32 +0100 Subject: [PATCH] feat(replace): clone properties when morphing to new element Closes #647 --- lib/Modeler.js | 3 +- lib/features/bpmn-clone/index.js | 6 - lib/features/replace/BpmnReplace.js | 42 +++++- lib/features/replace/index.js | 2 +- .../model/ModelCloneHelper.js} | 51 ++++--- lib/util/model/ModelCloneUtils.js | 20 +++ .../features/replace/clone-properties.bpmn | 141 ++++++++++++++++++ test/fixtures/json/model/camunda.json | 3 + test/spec/features/replace/BpmnReplaceSpec.js | 65 ++++++++ .../ModelCloneHelperSpec.js} | 66 ++++---- 10 files changed, 334 insertions(+), 65 deletions(-) delete mode 100644 lib/features/bpmn-clone/index.js rename lib/{features/bpmn-clone/BpmnClone.js => util/model/ModelCloneHelper.js} (69%) create mode 100644 lib/util/model/ModelCloneUtils.js create mode 100644 test/fixtures/bpmn/features/replace/clone-properties.bpmn rename test/spec/{features/bpmn-clone/BpmnCloneSpec.js => util/ModelCloneHelperSpec.js} (71%) diff --git a/lib/Modeler.js b/lib/Modeler.js index 0ffec2a1..56ecee91 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -192,8 +192,7 @@ Modeler.prototype._modelingModules = [ require('./features/modeling'), require('./features/palette'), require('./features/replace-preview'), - require('./features/snapping'), - require('./features/bpmn-clone') + require('./features/snapping') ]; diff --git a/lib/features/bpmn-clone/index.js b/lib/features/bpmn-clone/index.js deleted file mode 100644 index 49857eb9..00000000 --- a/lib/features/bpmn-clone/index.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = { - __init__: [ 'bpmnClone' ], - bpmnClone: [ 'type', require('./BpmnClone') ] -}; diff --git a/lib/features/replace/BpmnReplace.js b/lib/features/replace/BpmnReplace.js index a98d351f..a6220b11 100644 --- a/lib/features/replace/BpmnReplace.js +++ b/lib/features/replace/BpmnReplace.js @@ -2,11 +2,16 @@ var pick = require('lodash/object/pick'), assign = require('lodash/object/assign'), + intersection = require('lodash/array/intersection'), + filter = require('lodash/collection/filter'), has = require('lodash/object/has'); var is = require('../../util/ModelUtil').is, isExpanded = require('../../util/DiUtil').isExpanded, - isEventSubProcess = require('../../util/DiUtil').isEventSubProcess; + isEventSubProcess = require('../../util/DiUtil').isEventSubProcess, + getProperties = require('../../util/model/ModelCloneUtils').getProperties; + +var ModelCloneHelper = require('../../util/model/ModelCloneHelper'); var CUSTOM_PROPERTIES = [ 'cancelActivity', @@ -16,6 +21,21 @@ var CUSTOM_PROPERTIES = [ 'isInterrupting' ]; +var IGNORED_PROPERTIES = [ + 'id', + 'lanes', + 'incoming', + 'outgoing', + 'eventDefinitions', + 'processRef', + 'flowElements', + 'triggeredByEvent', + 'dataInputAssociations', + 'dataOutputAssociations', + 'incomingConversationLinks', + 'outgoingConversationLinks' +]; + function toggeling(element, target) { var oldCollapsed = has(element, 'collapsed') ? @@ -41,10 +61,13 @@ function toggeling(element, target) { } + /** * This module takes care of replacing BPMN elements */ -function BpmnReplace(bpmnFactory, replace, selection, modeling) { +function BpmnReplace(bpmnFactory, replace, selection, modeling, moddle) { + + var helper = new ModelCloneHelper(moddle); /** * Prepares a new business object for the replacement element @@ -63,8 +86,6 @@ function BpmnReplace(bpmnFactory, replace, selection, modeling) { var type = target.type, oldBusinessObject = element.businessObject; - - if (is(oldBusinessObject, 'bpmn:SubProcess')) { if (type === 'bpmn:SubProcess') { if (toggeling(element, target)) { @@ -76,7 +97,6 @@ function BpmnReplace(bpmnFactory, replace, selection, modeling) { } } - var newBusinessObject = bpmnFactory.create(type); var newElement = { @@ -84,6 +104,16 @@ function BpmnReplace(bpmnFactory, replace, selection, modeling) { businessObject: newBusinessObject }; + var elementProps = getProperties(oldBusinessObject.$descriptor), + newElementProps = getProperties(newBusinessObject.$descriptor, true), + properties = intersection(elementProps, newElementProps); + + properties = filter(properties, function(property) { + return IGNORED_PROPERTIES.indexOf(property.replace(/bpmn:/, '')) === -1; + }); + + newBusinessObject = helper.clone(oldBusinessObject, newBusinessObject, properties); + // initialize custom BPMN extensions if (target.eventDefinitionType) { newElement.eventDefinitionType = target.eventDefinitionType; @@ -162,6 +192,6 @@ function BpmnReplace(bpmnFactory, replace, selection, modeling) { this.replaceElement = replaceElement; } -BpmnReplace.$inject = [ 'bpmnFactory', 'replace', 'selection', 'modeling' ]; +BpmnReplace.$inject = [ 'bpmnFactory', 'replace', 'selection', 'modeling', 'moddle' ]; module.exports = BpmnReplace; diff --git a/lib/features/replace/index.js b/lib/features/replace/index.js index c8b5a7eb..8b488a27 100644 --- a/lib/features/replace/index.js +++ b/lib/features/replace/index.js @@ -4,4 +4,4 @@ module.exports = { require('diagram-js/lib/features/selection') ], bpmnReplace: [ 'type', require('./BpmnReplace') ] -}; \ No newline at end of file +}; diff --git a/lib/features/bpmn-clone/BpmnClone.js b/lib/util/model/ModelCloneHelper.js similarity index 69% rename from lib/features/bpmn-clone/BpmnClone.js rename to lib/util/model/ModelCloneHelper.js index a8029d98..25435543 100644 --- a/lib/features/bpmn-clone/BpmnClone.js +++ b/lib/util/model/ModelCloneHelper.js @@ -3,24 +3,33 @@ var forEach = require('lodash/collection/forEach'), filter = require('lodash/collection/filter'), isArray = require('lodash/lang/isArray'), - contains = require('lodash/collection/contains'), - map = require('lodash/collection/map'); + contains = require('lodash/collection/contains'); + + +function isAllowedIn(extProp, type) { + var allowedIn = extProp.meta.allowedIn; + + // '*' is a wildcard, which means any element is allowed to use this property + if (allowedIn.length === 1 && allowedIn[0] === '*') { + return true; + } + + return allowedIn.indexOf(type) !== -1; +} /** * A bpmn properties cloning interface * * @param {Moddle} moddle */ -function BpmnClone(moddle) { +function ModelCloneHelper(moddle) { this._moddle = moddle; } -module.exports = BpmnClone; - -BpmnClone.$inject = [ 'moddle' ]; +module.exports = ModelCloneHelper; -BpmnClone.prototype.clone = function(oldElement, newElement, properties) { +ModelCloneHelper.prototype.clone = function(oldElement, newElement, properties) { var moddle = this._moddle; forEach(properties, function(propName) { @@ -45,10 +54,12 @@ BpmnClone.prototype.clone = function(oldElement, newElement, properties) { var extProp = moddle.registry.typeMap[extElement.$type]; - if (extProp.meta.allowedIn && extProp.meta.allowedIn.indexOf(newElement.$type) !== -1) { + if (extProp.meta.allowedIn && isAllowedIn(extProp, newElement.$type)) { var newProp = this._deepClone(extElement); + newProp.$parent = newElement.extensionElements; + newElement.extensionElements.values.push(newProp); } }, this); @@ -58,6 +69,8 @@ BpmnClone.prototype.clone = function(oldElement, newElement, properties) { forEach(oldElementProp, function(extElement) { var newProp = this._deepClone(extElement); + newProp.$parent = newElement; + newElement.documentation.push(newProp); }, this); } @@ -66,19 +79,9 @@ BpmnClone.prototype.clone = function(oldElement, newElement, properties) { return newElement; }; -BpmnClone.prototype._deepClone = function _deepClone(extElement) { +ModelCloneHelper.prototype._deepClone = function _deepClone(extElement) { var newProp = extElement.$model.create(extElement.$type), - properties; - - // figure out which properties we want to assign to the newElement - // we're interested in enumerable ones (todo: double check this) - if (isArray(extElement)) { - properties = map(extElement, function(item) { - return item.$type; - }); - } else { - properties = filter(Object.keys(extElement), function(prop) { return prop !== '$type'; }); - } + properties = filter(Object.keys(extElement), function(prop) { return prop !== '$type'; }); forEach(properties, function(propName) { // check if the extElement has this property defined @@ -88,11 +91,17 @@ BpmnClone.prototype._deepClone = function _deepClone(extElement) { newProp[propName] = []; forEach(extElement[propName], function(property) { - newProp[propName].push(this._deepClone(property)); + var newDeepProp = this._deepClone(property); + + newDeepProp.$parent = newProp; + + newProp[propName].push(newDeepProp); }, this); } else if (extElement[propName].$type) { newProp[propName] = this._deepClone(extElement[propName]); + + newProp[propName].$parent = newProp; } } else { // just assign directly if it's a value diff --git a/lib/util/model/ModelCloneUtils.js b/lib/util/model/ModelCloneUtils.js new file mode 100644 index 00000000..3212f4c8 --- /dev/null +++ b/lib/util/model/ModelCloneUtils.js @@ -0,0 +1,20 @@ +'use strict'; + +var forEach = require('lodash/collection/forEach'); + +function getProperties(descriptor, keepDefault) { + var properties = []; + + forEach(descriptor.properties, function(property) { + + if (keepDefault && property.default) { + return; + } + + properties.push(property.ns.name); + }); + + return properties; +} + +module.exports.getProperties = getProperties; diff --git a/test/fixtures/bpmn/features/replace/clone-properties.bpmn b/test/fixtures/bpmn/features/replace/clone-properties.bpmn new file mode 100644 index 00000000..f0c908c2 --- /dev/null +++ b/test/fixtures/bpmn/features/replace/clone-properties.bpmn @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + hello world + + + foo + bar + + + + + + 10 + + + SequenceFlow_1e74z8m + SequenceFlow_1tdxph9 + + + DataStoreReference_1elrt45 + Property_0j0o7pl + + + DataObjectReference_0hkbt95 + Property_0j0o7pl + + + DataStoreReference_1j8ymac + + + DataObjectReference_1js94kb + + + + SequenceFlow_1e74z8m + + + + SequenceFlow_1tdxph9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/json/model/camunda.json b/test/fixtures/json/model/camunda.json index a9783471..5b1b82f5 100644 --- a/test/fixtures/json/model/camunda.json +++ b/test/fixtures/json/model/camunda.json @@ -460,6 +460,9 @@ "superClass": [ "Element" ], + "meta": { + "allowedIn": [ "*" ] + }, "properties": [ { "name": "values", diff --git a/test/spec/features/replace/BpmnReplaceSpec.js b/test/spec/features/replace/BpmnReplaceSpec.js index 899c1917..aa51b3cc 100644 --- a/test/spec/features/replace/BpmnReplaceSpec.js +++ b/test/spec/features/replace/BpmnReplaceSpec.js @@ -1169,4 +1169,69 @@ describe('features/replace - bpmn replace', function() { }); + + describe('properties', function() { + var clonePropertiesXML = require('../../../fixtures/bpmn/features/replace/clone-properties.bpmn'); + + var camundaPackage = require('../../../fixtures/json/model/camunda'); + + beforeEach(bootstrapModeler(clonePropertiesXML, { + modules: testModules, + moddleExtensions: { + camunda: camundaPackage + } + })); + + it('should copy properties', inject(function(elementRegistry, bpmnReplace) { + // given + var task = elementRegistry.get('Task_1'); + var newElementData = { + type: 'bpmn:ServiceTask' + }; + + // when + var newElement = bpmnReplace.replaceElement(task, newElementData); + + // then + var businessObject = newElement.businessObject; + + expect(businessObject.asyncBefore).to.be.true; + expect(businessObject.jobPriority).to.equal('100'); + expect(businessObject.documentation[0].text).to.equal('hello world'); + + var extensionElements = businessObject.extensionElements.values; + + expect(extensionElements).to.have.length(4); + + expect(is(extensionElements[0], 'camunda:InputOutput')).to.be.true; + + expect(is(extensionElements[0].inputParameters[0], 'camunda:InputParameter')).to.be.true; + + expect(extensionElements[0].inputParameters[0].name).to.equal('Input_1'); + expect(extensionElements[0].inputParameters[0].value).to.equal('foo'); + + expect(is(extensionElements[0].outputParameters[0], 'camunda:OutputParameter')).to.be.true; + + expect(extensionElements[0].outputParameters[0].name).to.equal('Output_1'); + expect(extensionElements[0].outputParameters[0].value).to.equal('bar'); + + expect(is(extensionElements[1], 'camunda:Properties')).to.be.true; + + expect(is(extensionElements[1].values[0], 'camunda:Property')).to.be.true; + + expect(extensionElements[1].values[0].name).to.equal('bar'); + expect(extensionElements[1].values[0].value).to.equal('foo'); + + expect(is(extensionElements[2], 'camunda:ExecutionListener')).to.be.true; + + expect(extensionElements[2].class).to.equal('reallyClassy'); + expect(extensionElements[2].event).to.equal('start'); + + expect(is(extensionElements[3], 'camunda:FailedJobRetryTimeCycle')).to.be.true; + + expect(extensionElements[3].body).to.equal('10'); + })); + + }); + }); diff --git a/test/spec/features/bpmn-clone/BpmnCloneSpec.js b/test/spec/util/ModelCloneHelperSpec.js similarity index 71% rename from test/spec/features/bpmn-clone/BpmnCloneSpec.js rename to test/spec/util/ModelCloneHelperSpec.js index 5a777c3f..dc6f11bc 100644 --- a/test/spec/features/bpmn-clone/BpmnCloneSpec.js +++ b/test/spec/util/ModelCloneHelperSpec.js @@ -1,23 +1,24 @@ 'use strict'; -require('../../../TestHelper'); +require('../../TestHelper'); /* global bootstrapModeler, inject */ -var bpmnCloneModule = require('../../../../lib/features/bpmn-clone'), - coreModule = require('../../../../lib/core'); +var coreModule = require('../../../lib/core'); -var camundaPackage = require('../../../fixtures/json/model/camunda'); +var ModelCloneHelper = require('../../../lib/util/model/ModelCloneHelper'); + +var camundaPackage = require('../../fixtures/json/model/camunda'); function getProp(element, property) { return element && element.$model.properties.get(element, property); } -describe('features/bpmn-clone', function() { +describe('util/ModelCloneHelper', function() { - var testModules = [ bpmnCloneModule, coreModule ]; + var testModules = [ coreModule ]; - var basicXML = require('../../../fixtures/bpmn/basic.bpmn'); + var basicXML = require('../../fixtures/bpmn/basic.bpmn'); beforeEach(bootstrapModeler(basicXML, { modules: testModules, @@ -26,33 +27,35 @@ describe('features/bpmn-clone', function() { } })); + var helper; + + beforeEach(inject(function(moddle) { + helper = new ModelCloneHelper(moddle); + })); + describe('simple', function() { - it('should pass property', inject(function(moddle, bpmnClone) { + it('should pass property', inject(function(moddle) { // given var userTask = moddle.create('bpmn:UserTask', { - name: 'Field_1', - stringValue: 'myFieldValue', asyncBefore: true }); - var serviceTask = bpmnClone.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'camunda:asyncBefore' ]); + var serviceTask = helper.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'camunda:asyncBefore' ]); expect(getProp(serviceTask, 'camunda:asyncBefore')).to.be.true; })); - it('should not pass property', inject(function(bpmnClone, moddle) { + it('should not pass property', inject(function(moddle) { // given var userTask = moddle.create('bpmn:UserTask', { - name: 'Field_1', - stringValue: 'myFieldValue', assignee: 'foobar' }); - var serviceTask = bpmnClone.clone(userTask, moddle.create('bpmn:ServiceTask'), []); + var serviceTask = helper.clone(userTask, moddle.create('bpmn:ServiceTask'), []); expect(getProp(serviceTask, 'camunda:assignee')).to.not.exist; })); @@ -61,26 +64,25 @@ describe('features/bpmn-clone', function() { describe('nested', function() { - it('should pass nested property - documentation', inject(function(moddle, bpmnClone) { + it('should pass nested property - documentation', inject(function(moddle) { // given - var userTask = moddle.create('bpmn:UserTask', { - name: 'Field_1', - stringValue: 'myFieldValue' - }); + var userTask = moddle.create('bpmn:UserTask'); var docs = userTask.get('documentation'); docs.push(moddle.create('bpmn:Documentation', { textFormat: 'xyz', text: 'FOO\nBAR' })); docs.push(moddle.create('bpmn:Documentation', { text: '' })); - var serviceTask = bpmnClone.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'bpmn:documentation' ]); + var serviceTask = helper.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'bpmn:documentation' ]); var serviceTaskDocs = getProp(serviceTask, 'bpmn:documentation'), userTaskDocs = getProp(userTask, 'bpmn:documentation'); expect(userTaskDocs[0]).to.not.equal(serviceTaskDocs[0]); + expect(serviceTaskDocs[0].$parent).to.equal(serviceTask); + expect(serviceTaskDocs[0].text).to.equal('FOO\nBAR'); expect(serviceTaskDocs[0].textFormat).to.equal('xyz'); @@ -88,7 +90,7 @@ describe('features/bpmn-clone', function() { })); - it('should pass deeply nested property - executionListener', inject(function(moddle, bpmnClone) { + it('should pass deeply nested property - executionListener', inject(function(moddle) { // given var script = moddle.create('camunda:Script', { @@ -104,12 +106,10 @@ describe('features/bpmn-clone', function() { var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ execListener ] }); var userTask = moddle.create('bpmn:UserTask', { - name: 'Field_1', - stringValue: 'myFieldValue', extensionElements: extensionElements }); - var serviceTask = bpmnClone.clone(userTask, moddle.create('bpmn:ServiceTask'), [ + var serviceTask = helper.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'bpmn:extensionElements', 'camunda:executionListener' ]); @@ -118,15 +118,19 @@ describe('features/bpmn-clone', function() { // then expect(executionListener.$type).to.equal('camunda:ExecutionListener'); + expect(executionListener.$parent).to.equal(serviceTask.extensionElements); + expect(executionListener.event).to.equal('start'); expect(executionListener.script.$type).to.equal('camunda:Script'); + expect(executionListener.script.$parent).to.equal(executionListener); + expect(executionListener.script.scriptFormat).to.equal('groovy'); expect(executionListener.script.value).to.equal('foo = bar;'); })); - it('should pass deeply nested property - inputOutput', inject(function(moddle, bpmnClone) { + it('should pass deeply nested property - inputOutput', inject(function(moddle) { // given var outputParameter = moddle.create('camunda:OutputParameter', { @@ -147,12 +151,10 @@ describe('features/bpmn-clone', function() { var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ inputOutput ] }); var userTask = moddle.create('bpmn:UserTask', { - name: 'Field_1', - stringValue: 'myFieldValue', extensionElements: extensionElements }); - var serviceTask = bpmnClone.clone(userTask, moddle.create('bpmn:ServiceTask'), [ + var serviceTask = helper.clone(userTask, moddle.create('bpmn:ServiceTask'), [ 'bpmn:extensionElements', 'camunda:inputOutput' ]); @@ -166,9 +168,15 @@ describe('features/bpmn-clone', function() { var oldOutParam = userTask.extensionElements.values[0].outputParameters[0]; expect(newOutParam).to.not.equal(oldOutParam); + + expect(newOutParam.$parent).to.equal(executionListener); expect(newOutParam.definition).to.not.equal(oldOutParam.definition); + expect(newOutParam.definition.$parent).to.equal(newOutParam); + expect(newOutParam.definition.items[0]).to.not.equal(oldOutParam.definition.items[0]); + expect(newOutParam.definition.items[0].$parent).to.not.equal(newOutParam.definition.$parent); + expect(newOutParam.$type).to.equal('camunda:OutputParameter'); expect(newOutParam.definition.$type).to.equal('camunda:List'); expect(newOutParam.definition.items[0].value).to.equal('${1+1}');