feat(util/model): make property cloning pluggable

This introduces a 'property.clone' event that allows
plugging into the cloning mechanism when cloning
nested extension elements.

Related to camunda/camunda-bpmn-moddle#35
This commit is contained in:
Ricardo Matias 2017-01-23 14:12:43 +01:00 committed by Nico Rehwaldt
parent 2ecb9aeae4
commit b37182b53b
6 changed files with 263 additions and 15 deletions

View File

@ -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),

View File

@ -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;

View File

@ -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);

View File

@ -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": [

View File

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

View File

@ -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 ]
};