From 9ac0a9a957e6f7ec5c87875198d3f37a39cf3bcf Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Wed, 6 Jan 2016 00:28:01 +0100 Subject: [PATCH] fix(modeling): correctly populate DataInputAssociation#targetRef Closes #431 --- lib/features/modeling/BpmnFactory.js | 3 +- .../behavior/DataInputAssociationBehavior.js | 152 ++++++++++++++++++ lib/features/modeling/behavior/index.js | 6 +- .../DataInputAssociationBehavior.bpmn | 33 ++++ .../DataInputAssociationBehaviorSpec.js | 147 +++++++++++++++++ 5 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 lib/features/modeling/behavior/DataInputAssociationBehavior.js create mode 100644 test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn create mode 100644 test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js diff --git a/lib/features/modeling/BpmnFactory.js b/lib/features/modeling/BpmnFactory.js index b0d1538f..92252cb8 100644 --- a/lib/features/modeling/BpmnFactory.js +++ b/lib/features/modeling/BpmnFactory.js @@ -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) { diff --git a/lib/features/modeling/behavior/DataInputAssociationBehavior.js b/lib/features/modeling/behavior/DataInputAssociationBehavior.js new file mode 100644 index 00000000..b5a09c0d --- /dev/null +++ b/lib/features/modeling/behavior/DataInputAssociationBehavior.js @@ -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); + } + }; +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 7b2fcf5c..d32deddd 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -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') ], diff --git a/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn b/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn new file mode 100644 index 00000000..7e21cd1e --- /dev/null +++ b/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn @@ -0,0 +1,33 @@ + + + + + + DataObjectReference + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js b/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js new file mode 100644 index 00000000..6f9208d1 --- /dev/null +++ b/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js @@ -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'; + }); +} \ No newline at end of file