diff --git a/lib/features/copy-paste/BpmnCopyPaste.js b/lib/features/copy-paste/BpmnCopyPaste.js index c4983941..e35abdde 100644 --- a/lib/features/copy-paste/BpmnCopyPaste.js +++ b/lib/features/copy-paste/BpmnCopyPaste.js @@ -32,7 +32,7 @@ function removeProperties(element, properties) { function BpmnCopyPaste(bpmnFactory, eventBus, copyPaste, clipboard, moddle, canvas, bpmnRules) { - var helper = new ModelCloneHelper(); + var helper = new ModelCloneHelper(eventBus); copyPaste.registerDescriptor(function(element, descriptor) { var businessObject = getBusinessObject(element), diff --git a/lib/features/replace/BpmnReplace.js b/lib/features/replace/BpmnReplace.js index 62bbc850..fc5ca558 100644 --- a/lib/features/replace/BpmnReplace.js +++ b/lib/features/replace/BpmnReplace.js @@ -54,9 +54,9 @@ function toggeling(element, target) { /** * This module takes care of replacing BPMN elements */ -function BpmnReplace(bpmnFactory, replace, selection, modeling, moddle) { +function BpmnReplace(bpmnFactory, replace, selection, modeling, eventBus) { - var helper = new ModelCloneHelper(moddle); + var helper = new ModelCloneHelper(eventBus); /** * Prepares a new business object for the replacement element @@ -188,6 +188,6 @@ function BpmnReplace(bpmnFactory, replace, selection, modeling, moddle) { this.replaceElement = replaceElement; } -BpmnReplace.$inject = [ 'bpmnFactory', 'replace', 'selection', 'modeling', 'moddle' ]; +BpmnReplace.$inject = [ 'bpmnFactory', 'replace', 'selection', 'modeling', 'eventBus' ]; module.exports = BpmnReplace; diff --git a/lib/util/model/ModelCloneHelper.js b/lib/util/model/ModelCloneHelper.js index ec828be3..776fc0ee 100644 --- a/lib/util/model/ModelCloneHelper.js +++ b/lib/util/model/ModelCloneHelper.js @@ -3,6 +3,7 @@ var forEach = require('lodash/collection/forEach'), filter = require('lodash/collection/filter'), any = require('lodash/collection/any'), + sort = require('lodash/collection/sortBy'), isArray = require('lodash/lang/isArray'); var IGNORED_PROPERTIES = require('./ModelCloneUtils').IGNORED_PROPERTIES; @@ -28,7 +29,9 @@ function isType(element, types) { * A bpmn properties cloning interface * */ -function ModelCloneHelper() {} +function ModelCloneHelper(eventBus) { + this._eventBus = eventBus; +} module.exports = ModelCloneHelper; @@ -36,6 +39,12 @@ module.exports = ModelCloneHelper; ModelCloneHelper.prototype.clone = function(oldElement, newElement, properties) { this._newElement = newElement; + // we want the extensionElements to be cloned last + // so that they can check certain properties + properties = sort(properties, function(prop) { + return prop === 'bpmn:extensionElements'; + }); + forEach(properties, function(propName) { var oldElementProp = oldElement.get(propName), newElementProp = newElement.get(propName), @@ -78,6 +87,8 @@ ModelCloneHelper.prototype.clone = function(oldElement, newElement, properties) }; ModelCloneHelper.prototype._deepClone = function _deepClone(element) { + var eventBus = this._eventBus; + var newElement = this._newElement; var newProp = element.$model.create(element.$type); @@ -115,11 +126,19 @@ ModelCloneHelper.prototype._deepClone = function _deepClone(element) { return; } - // make sure only allowed extensionElements are copied - if (element.$type === 'bpmn:ExtensionElements' && - extProp.meta && extProp.meta.allowedIn && - !isAllowedIn(extProp, newElement.$type)) { - return; + var canClone = eventBus.fire('property.clone', { + newElement: newElement, + propertyDescriptor: extProp + }); + + if (!canClone) { + // if can clone is 'undefined' or 'false' + // check for the meta information if it is allowed + if (element.$type === 'bpmn:ExtensionElements' && + extProp.meta && extProp.meta.allowedIn && + !isAllowedIn(extProp, newElement.$type)) { + return false; + } } newDeepProp = this._deepClone(property); diff --git a/test/fixtures/json/model/camunda.json b/test/fixtures/json/model/camunda.json index 5b1b82f5..761e4ab1 100644 --- a/test/fixtures/json/model/camunda.json +++ b/test/fixtures/json/model/camunda.json @@ -744,7 +744,10 @@ "bpmn:BusinessRuleTask", "bpmn:ScriptTask", "bpmn:ReceiveTask", - "bpmn:CallActivity" + "bpmn:CallActivity", + "bpmn:TimerEventDefinition", + "bpmn:SignalEventDefinition", + "bpmn:MultiInstanceLoopCharacteristics" ] }, "properties": [ @@ -777,7 +780,8 @@ "bpmn:IntermediateThrowEvent", "bpmn:EndEvent", "bpmn:BoundaryEvent", - "bpmn:CallActivity" + "bpmn:CallActivity", + "bpmn:SubProcess" ] }, "properties": [ diff --git a/test/spec/util/ModelCloneHelperSpec.js b/test/spec/util/ModelCloneHelperSpec.js index b65342e4..afd3f069 100644 --- a/test/spec/util/ModelCloneHelperSpec.js +++ b/test/spec/util/ModelCloneHelperSpec.js @@ -10,13 +10,15 @@ var ModelCloneHelper = require('../../../lib/util/model/ModelCloneHelper'); var camundaPackage = require('../../fixtures/json/model/camunda'); +var camundaModdleModule = require('./camunda-moddle'); + function getProp(element, property) { return element && element.$model.properties.get(element, property); } describe('util/ModelCloneHelper', function() { - var testModules = [ coreModule ]; + var testModules = [ camundaModdleModule, coreModule ]; var basicXML = require('../../fixtures/bpmn/basic.bpmn'); @@ -29,8 +31,8 @@ describe('util/ModelCloneHelper', function() { var helper; - beforeEach(inject(function(moddle) { - helper = new ModelCloneHelper(moddle); + beforeEach(inject(function(eventBus) { + helper = new ModelCloneHelper(eventBus); })); describe('simple', function() { @@ -211,4 +213,160 @@ describe('util/ModelCloneHelper', function() { }); + + describe('special cases', function() { + + it('failed job retry time cycle', inject(function(moddle) { + + function createExtElems() { + var retryTimeCycle = moddle.create('camunda:FailedJobRetryTimeCycle', { body: 'foobar' }); + + return moddle.create('bpmn:ExtensionElements', { values: [ retryTimeCycle ] }); + } + + // given + var timerEvtDef = moddle.create('bpmn:TimerEventDefinition', { + timeDuration: 'foobar' + }); + + var signalEvtDef = moddle.create('bpmn:SignalEventDefinition', { + timeDuration: 'foobar' + }); + + var multiInst = moddle.create('bpmn:MultiInstanceLoopCharacteristics'); + + var timerStartEvent = moddle.create('bpmn:StartEvent', { + extensionElements: createExtElems(), + eventDefinitions: [ timerEvtDef ] + }); + + var signalStartEvt = moddle.create('bpmn:StartEvent', { + extensionElements: createExtElems(), + eventDefinitions: [ signalEvtDef ] + }); + + var subProcess = moddle.create('bpmn:SubProcess', { + extensionElements: createExtElems(), + loopCharacteristics: multiInst + }); + + var intCatchEvt = helper.clone(timerStartEvent, moddle.create('bpmn:IntermediateCatchEvent'), [ + 'bpmn:extensionElements', + 'bpmn:eventDefinitions' + ]); + + var startEvt = helper.clone(signalStartEvt, moddle.create('bpmn:StartEvent'), [ + 'bpmn:extensionElements', + 'bpmn:eventDefinitions' + ]); + + var newSubProcess = helper.clone(subProcess, moddle.create('bpmn:SubProcess'), [ + 'bpmn:extensionElements', + 'bpmn:loopCharacteristics' + ]); + + var intCatchEvtExtElems = intCatchEvt.extensionElements.values, + startEvtExtElems = startEvt.extensionElements.values, + newSubProcessExtElems = newSubProcess.extensionElements.values; + + // then + function expectTimeCycle(extElems) { + expect(extElems[0].$type).to.equal('camunda:FailedJobRetryTimeCycle'); + expect(extElems[0].body).to.equal('foobar'); + } + + expectTimeCycle(intCatchEvtExtElems); + + expectTimeCycle(startEvtExtElems); + + expectTimeCycle(newSubProcessExtElems); + })); + + + it('connector', inject(function(moddle) { + + // given + var connector = moddle.create('camunda:Connector', { + connectorId: 'hello_connector' + }); + + var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ connector ] }); + + var msgEvtDef = moddle.create('bpmn:MessageEventDefinition'); + + var msgIntermThrowEvt = moddle.create('bpmn:IntermediateThrowEvent', { + extensionElements: extensionElements, + eventDefinitions: [ msgEvtDef ] + }); + + var clonedElement = helper.clone(msgIntermThrowEvt, moddle.create('bpmn:EndEvent'), [ + 'bpmn:extensionElements', + 'bpmn:eventDefinitions' + ]); + + var extElems = clonedElement.extensionElements.values; + + // then + expect(extElems[0].$type).to.equal('camunda:Connector'); + expect(extElems[0].connectorId).to.equal('hello_connector'); + })); + + + it('field', inject(function(moddle) { + + // given + var field = moddle.create('camunda:Field', { + name: 'hello_field' + }); + + var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ field ] }); + + var msgEvtDef = moddle.create('bpmn:MessageEventDefinition'); + + var msgIntermThrowEvt = moddle.create('bpmn:IntermediateThrowEvent', { + extensionElements: extensionElements, + eventDefinitions: [ msgEvtDef ] + }); + + var clonedElement = helper.clone(msgIntermThrowEvt, moddle.create('bpmn:EndEvent'), [ + 'bpmn:extensionElements', + 'bpmn:eventDefinitions' + ]); + + var extElems = clonedElement.extensionElements.values; + + // then + expect(extElems[0].$type).to.equal('camunda:Field'); + expect(extElems[0].name).to.equal('hello_field'); + })); + + + it('not clone field', inject(function(moddle) { + + // given + var field = moddle.create('camunda:Field', { + name: 'hello_field' + }); + + var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ field ] }); + + var msgEvtDef = moddle.create('bpmn:MessageEventDefinition'); + + var msgIntermThrowEvt = moddle.create('bpmn:IntermediateThrowEvent', { + extensionElements: extensionElements, + eventDefinitions: [ msgEvtDef ] + }); + + var clonedElement = helper.clone(msgIntermThrowEvt, moddle.create('bpmn:IntermediateThrowEvent'), [ + 'bpmn:extensionElements' + ]); + + var extElems = clonedElement.extensionElements; + + // then + expect(extElems.values).be.empty; + })); + + }); + }); diff --git a/test/spec/util/camunda-moddle.js b/test/spec/util/camunda-moddle.js new file mode 100644 index 00000000..71bcbba0 --- /dev/null +++ b/test/spec/util/camunda-moddle.js @@ -0,0 +1,67 @@ +'use strict'; + +var any = require('lodash/collection/any'); + +var ALLOWED_TYPES = { + FailedJobRetryTimeCycle: [ 'bpmn:StartEvent', 'bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:Activity' ], + Connector: [ 'bpmn:EndEvent', 'bpmn:IntermediateThrowEvent' ], + Field: [ 'bpmn:EndEvent', 'bpmn:IntermediateThrowEvent' ] +}; + + +function is(element, type) { + return element && (typeof element.$instanceOf === 'function') && element.$instanceOf(type); +} + +function exists(element) { + return element && element.length; +} + +function includesType(collection, type) { + return exists(collection) && any(collection, function(element) { + return is(element, type); + }); +} + +function anyType(element, types) { + return any(types, function(type) { + return is(element, type); + }); +} + +function isAllowed(propName, propDescriptor, newElement) { + var name = propDescriptor.name, + types = ALLOWED_TYPES[ name.replace(/camunda:/, '') ]; + + return name === propName && anyType(newElement, types); +} + + +function CamundaModdleExtension(eventBus) { + + eventBus.on('property.clone', function(context) { + var newElement = context.newElement, + propDescriptor = context.propertyDescriptor; + + if (isAllowed('camunda:FailedJobRetryTimeCycle', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') || + includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') || + is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics'); + } + + if (isAllowed('camunda:Connector', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition'); + } + + if (isAllowed('camunda:Field', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition'); + } + }); +} + +CamundaModdleExtension.$inject = [ 'eventBus' ]; + +module.exports = { + __init__: [ 'CamundaModdleExtension' ], + CamundaModdleExtension: [ 'type', CamundaModdleExtension ] +};