fix(modeling): correctly populate DataInputAssociation#targetRef

Closes #431
This commit is contained in:
Nico Rehwaldt 2016-01-06 00:28:01 +01:00
parent f89fd529de
commit 9ac0a9a957
5 changed files with 338 additions and 3 deletions

View File

@ -25,7 +25,8 @@ BpmnFactory.prototype._needsId = function(element) {
element.$instanceOf('bpmndi:BPMNShape') ||
element.$instanceOf('bpmndi:BPMNEdge') ||
element.$instanceOf('bpmndi:BPMNDiagram') ||
element.$instanceOf('bpmndi:BPMNPlane');
element.$instanceOf('bpmndi:BPMNPlane') ||
element.$instanceOf('bpmn:Property');
};
BpmnFactory.prototype._ensureId = function(element) {

View File

@ -0,0 +1,152 @@
'use strict';
var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var Collections = require('diagram-js/lib/util/Collections');
var find = require('lodash/collection/find');
var is = require('../../../util/ModelUtil').is;
var TARGET_REF_PLACEHOLDER_NAME = '__targetRef_placeholder';
/**
* This behavior makes sure we always set a fake
* DataInputAssociation#targetRef as demanded by the BPMN 2.0
* XSD schema.
*
* The reference is set to a bpmn:Property{ name: '__targetRef_placeholder' }
* which is created on the fly and cleaned up afterwards if not needed
* anymore.
*
* @param {EventBus} eventBus
* @param {BpmnFactory} bpmnFactory
*/
function DataInputAssociationBehavior(eventBus, bpmnFactory) {
CommandInterceptor.call(this, eventBus);
this.executed([
'connection.create',
'connection.delete',
'connection.move',
'connection.reconnectEnd'
], ifDataInputAssociation(fixTargetRef));
this.reverted([
'connection.create',
'connection.delete',
'connection.move',
'connection.reconnectEnd'
], ifDataInputAssociation(fixTargetRef));
function usesTargetRef(element, targetRef, removedConnection) {
var inputAssociations = element.get('dataInputAssociations');
return find(inputAssociations, function(association) {
return association !== removedConnection &&
association.targetRef === targetRef;
});
}
function getTargetRef(element, create) {
var properties = element.get('properties');
var targetRefProp = find(properties, function(p) {
return p.name === TARGET_REF_PLACEHOLDER_NAME;
});
if (!targetRefProp && create) {
targetRefProp = bpmnFactory.create('bpmn:Property', {
name: TARGET_REF_PLACEHOLDER_NAME
});
Collections.add(properties, targetRefProp);
}
return targetRefProp;
}
function cleanupTargetRef(element, connection) {
var targetRefProp = getTargetRef(element);
if (!targetRefProp) {
return;
}
if (!usesTargetRef(element, targetRefProp, connection)) {
Collections.remove(element.get('properties'), targetRefProp);
}
}
/**
* Make sure targetRef is set to a valid property or
* `null` if the connection is detached.
*
* @param {Event} event
*/
function fixTargetRef(event) {
var context = event.context,
connection = context.connection,
connectionBo = connection.businessObject,
target = connection.target,
targetBo = target && target.businessObject,
newTarget = context.newTarget,
newTargetBo = newTarget && newTarget.businessObject,
oldTarget = context.oldTarget || context.target,
oldTargetBo = oldTarget && oldTarget.businessObject;
var dataAssociation = connection.businessObject,
targetRefProp;
if (oldTargetBo && oldTargetBo !== targetBo) {
cleanupTargetRef(oldTargetBo, connectionBo);
}
if (newTargetBo && newTargetBo !== targetBo) {
cleanupTargetRef(newTargetBo, connectionBo);
}
if (targetBo) {
targetRefProp = getTargetRef(targetBo, true);
dataAssociation.targetRef = targetRefProp;
} else {
dataAssociation.targetRef = null;
}
}
}
DataInputAssociationBehavior.$inject = [ 'eventBus', 'bpmnFactory' ];
inherits(DataInputAssociationBehavior, CommandInterceptor);
module.exports = DataInputAssociationBehavior;
/**
* Only call the given function when the event
* touches a bpmn:DataInputAssociation.
*
* @param {Function} fn
* @return {Function}
*/
function ifDataInputAssociation(fn) {
return function(event) {
var context = event.context,
connection = context.connection;
if (is(connection, 'bpmn:DataInputAssociation')) {
return fn(event);
}
};
}

View File

@ -3,9 +3,10 @@ module.exports = {
'appendBehavior',
'createBoundaryEventBehavior',
'createDataObjectBehavior',
'deleteLaneBehavior',
'createOnFlowBehavior',
'createParticipantBehavior',
'dataInputAssociationBehavior',
'deleteLaneBehavior',
'modelingFeedback',
'removeParticipantBehavior',
'replaceConnectionBehavior',
@ -16,9 +17,10 @@ module.exports = {
appendBehavior: [ 'type', require('./AppendBehavior') ],
createBoundaryEventBehavior: [ 'type', require('./CreateBoundaryEventBehavior') ],
createDataObjectBehavior: [ 'type', require('./CreateDataObjectBehavior') ],
deleteLaneBehavior: [ 'type', require('./DeleteLaneBehavior') ],
createOnFlowBehavior: [ 'type', require('./CreateOnFlowBehavior') ],
createParticipantBehavior: [ 'type', require('./CreateParticipantBehavior') ],
dataInputAssociationBehavior: [ 'type', require('./DataInputAssociationBehavior') ],
deleteLaneBehavior: [ 'type', require('./DeleteLaneBehavior') ],
modelingFeedback: [ 'type', require('./ModelingFeedback') ],
removeParticipantBehavior: [ 'type', require('./RemoveParticipantBehavior') ],
replaceConnectionBehavior: [ 'type', require('./ReplaceConnectionBehavior') ],

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process" isExecutable="false">
<bpmn:Task id="Task_A" name="Task_A">
<bpmn:dataInputAssociation id="DataInputAssociation">
<bpmn:sourceRef>DataObjectReference</bpmn:sourceRef>
</bpmn:dataInputAssociation>
</bpmn:Task>
<bpmn:dataObjectReference id="DataObjectReference" name="DataObjectReference" dataObjectRef="DataObject" />
<bpmn:dataObject id="DataObject" />
<bpmn:Task id="Task_B" name="Task_B" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process">
<bpmndi:BPMNShape id="Task_A_di" bpmnElement="Task_A">
<dc:Bounds x="249" y="22" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DataObjectReference_di" bpmnElement="DataObjectReference">
<dc:Bounds x="100" y="201" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="73" y="251" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_B_di" bpmnElement="Task_B">
<dc:Bounds x="249" y="186" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="DataInputAssociation_di" bpmnElement="DataInputAssociation">
<di:waypoint xsi:type="dc:Point" x="136" y="210" />
<di:waypoint xsi:type="dc:Point" x="257" y="100" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,147 @@
'use strict';
require('../../../../TestHelper');
/* global inject, bootstrapModeler */
var find = require('lodash/collection/find');
var modelingModule = require('../../../../../lib/features/modeling');
describe('modeling/behavior - fix DataInputAssociation#targetRef', function(){
var diagramXML = require('./DataInputAssociationBehavior.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: modelingModule }));
it('should add on connect', inject(function(modeling, elementRegistry) {
// given
var dataObjectShape = elementRegistry.get('DataObjectReference'),
taskShape = elementRegistry.get('Task_B');
// when
var newConnection = modeling.connect(dataObjectShape, taskShape, {
type: 'bpmn:DataInputAssociation'
});
var dataInputAssociation = newConnection.businessObject;
// then
expect(dataInputAssociation.targetRef).to.exist;
expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(taskShape));
}));
it('should remove on connect / undo', inject(function(modeling, elementRegistry, commandStack) {
// given
var dataObjectShape = elementRegistry.get('DataObjectReference'),
taskShape = elementRegistry.get('Task_B');
var newConnection = modeling.connect(dataObjectShape, taskShape, {
type: 'bpmn:DataInputAssociation'
});
var dataInputAssociation = newConnection.businessObject;
// when
commandStack.undo();
// then
expect(dataInputAssociation.targetRef).to.not.exist;
expect(getTargetRefProp(taskShape)).to.not.exist;
}));
it('should update on reconnectEnd', inject(function(modeling, elementRegistry) {
// given
var oldTarget = elementRegistry.get('Task_A'),
connection = elementRegistry.get('DataInputAssociation'),
dataInputAssociation = connection.businessObject,
newTarget = elementRegistry.get('Task_B');
// when
modeling.reconnectEnd(connection, newTarget, { x: newTarget.x, y: newTarget.y });
// then
expect(getTargetRefProp(oldTarget)).not.to.exist;
expect(dataInputAssociation.targetRef).to.exist;
expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(newTarget));
}));
it('should update on reconnectEnd / undo', inject(function(modeling, elementRegistry, commandStack) {
// given
var oldTarget = elementRegistry.get('Task_A'),
connection = elementRegistry.get('DataInputAssociation'),
dataInputAssociation = connection.businessObject,
newTarget = elementRegistry.get('Task_B');
modeling.reconnectEnd(connection, newTarget, { x: newTarget.x, y: newTarget.y });
// when
commandStack.undo();
// then
expect(getTargetRefProp(newTarget)).not.to.exist;
expect(dataInputAssociation.targetRef).to.exist;
expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(oldTarget));
}));
it('should unset on remove', inject(function(modeling, elementRegistry) {
// given
var oldTarget = elementRegistry.get('Task_A'),
connection = elementRegistry.get('DataInputAssociation'),
dataInputAssociation = connection.businessObject;
// when
modeling.removeElements([ connection ]);
// then
expect(getTargetRefProp(oldTarget)).not.to.exist;
expect(dataInputAssociation.targetRef).to.not.exist;
}));
it('should unset on remove / undo', inject(function(modeling, elementRegistry, commandStack) {
// given
var oldTarget = elementRegistry.get('Task_A'),
connection = elementRegistry.get('DataInputAssociation'),
dataInputAssociation = connection.businessObject;
modeling.removeElements([ connection ]);
// when
commandStack.undo();
// then
expect(dataInputAssociation.targetRef).to.exist;
expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(oldTarget));
}));
});
function getTargetRefProp(element) {
expect(element).to.exist;
var properties = element.businessObject.get('properties');
return find(properties, function(p) {
return p.name === '__targetRef_placeholder';
});
}