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}');