From 5b4f20e3ab7e50e2721c6b87931bce3efe3c1936 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Thu, 22 Nov 2018 15:54:08 +0100 Subject: [PATCH 1/4] feat(data-input-association-behavior): ensure BPMN 2.0 compatibility * reference bpmn:DataInput as targetRef Related to camunda/camunda-modeler#984 --- lib/features/modeling/BpmnFactory.js | 5 +- .../behavior/DataInputAssociationBehavior.js | 196 +++++++++++---- .../behavior/DataAssociationBehavior.bpmn | 29 +++ .../DataInputAssociationBehavior.bpmn | 33 --- .../DataInputAssociationBehaviorSpec.js | 234 +++++++++++++----- 5 files changed, 356 insertions(+), 141 deletions(-) create mode 100644 test/spec/features/modeling/behavior/DataAssociationBehavior.bpmn delete mode 100644 test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn diff --git a/lib/features/modeling/BpmnFactory.js b/lib/features/modeling/BpmnFactory.js index 5a508126d5..a680594204 100644 --- a/lib/features/modeling/BpmnFactory.js +++ b/lib/features/modeling/BpmnFactory.js @@ -32,7 +32,10 @@ BpmnFactory.prototype._needsId = function(element) { 'bpmndi:BPMNEdge', 'bpmndi:BPMNDiagram', 'bpmndi:BPMNPlane', - 'bpmn:Property' + 'bpmn:Property', + 'bpmn:InputOutputSpecification', + 'bpmn:DataInput', + 'bpmn:InputSet' ]); }; diff --git a/lib/features/modeling/behavior/DataInputAssociationBehavior.js b/lib/features/modeling/behavior/DataInputAssociationBehavior.js index 59a251fe28..bc4bc5fc6c 100644 --- a/lib/features/modeling/behavior/DataInputAssociationBehavior.js +++ b/lib/features/modeling/behavior/DataInputAssociationBehavior.js @@ -8,24 +8,19 @@ import { } from 'diagram-js/lib/util/Collections'; import { - find + find, + forEach } from 'min-dash'; import { is } from '../../../util/ModelUtil'; -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. + * This behavior makes sure a bpmn:DataInput is created and referenced when a + * bpmn:DataInputAssociation is created. It also makes sure the bpmn:DataInput and + * the reference are removed when a bpmn:InputAssociation is removed. * * @param {EventBus} eventBus * @param {BpmnFactory} bpmnFactory @@ -40,66 +35,121 @@ export default function DataInputAssociationBehavior(eventBus, bpmnFactory) { 'connection.delete', 'connection.move', 'connection.reconnectEnd' - ], ifDataInputAssociation(fixTargetRef)); + ], ifDataInputAssociation(updateTargetRef)); this.reverted([ 'connection.create', 'connection.delete', 'connection.move', 'connection.reconnectEnd' - ], ifDataInputAssociation(fixTargetRef)); + ], ifDataInputAssociation(updateTargetRef)); + /** + * Create and return bpmn:DataInput. + * + * Create bpmn:InputOutputSpecification, dataInputs and inputSets if not + * found. + * + * @param {ModdleElement} element - Element. + * + * @returns {ModdleElement} + */ + function createDataInput(element) { + var ioSpecification = element.get('ioSpecification'); - function usesTargetRef(element, targetRef, removedConnection) { + var inputSet, outputSet; - var inputAssociations = element.get('dataInputAssociations'); + if (!ioSpecification) { + ioSpecification = bpmnFactory.create('bpmn:InputOutputSpecification', { + dataInputs: [], + inputSets: [] + }); - return find(inputAssociations, function(association) { - return association !== removedConnection && - association.targetRef === targetRef; - }); - } + element.ioSpecification = ioSpecification; - function getTargetRef(element, create) { + inputSet = bpmnFactory.create('bpmn:InputSet', { + dataInputRefs: [], + name: 'Inputs' + }); - var properties = element.get('properties'); + inputSet.$parent = ioSpecification; - var targetRefProp = find(properties, function(p) { - return p.name === TARGET_REF_PLACEHOLDER_NAME; - }); + collectionAdd(ioSpecification.get('inputSets'), inputSet); - if (!targetRefProp && create) { - targetRefProp = bpmnFactory.create('bpmn:Property', { - name: TARGET_REF_PLACEHOLDER_NAME + outputSet = bpmnFactory.create('bpmn:OutputSet', { + dataOutputRefs: [], + name: 'Outputs' }); - collectionAdd(properties, targetRefProp); + outputSet.$parent = ioSpecification; + + collectionAdd(ioSpecification.get('outputSets'), outputSet); } - return targetRefProp; - } + var dataInput = bpmnFactory.create('bpmn:DataInput'); - function cleanupTargetRef(element, connection) { + dataInput.$parent = ioSpecification; - var targetRefProp = getTargetRef(element); + if (!ioSpecification.dataInputs) { + ioSpecification.dataInputs = []; + } + + collectionAdd(ioSpecification.get('dataInputs'), dataInput); + + if (!ioSpecification.inputSets) { + inputSet = bpmnFactory.create('bpmn:InputSet', { + dataInputRefs: [], + name: 'Inputs' + }); + + inputSet.$parent = ioSpecification; + + collectionAdd(ioSpecification.get('inputSets'), inputSet); + } + + inputSet = ioSpecification.get('inputSets')[0]; + + collectionAdd(inputSet.dataInputRefs, dataInput); + + return dataInput; + } + + /** + * Remove bpmn:DataInput that is referenced by connection as targetRef from + * bpmn:InputOutputSpecification. + * + * @param {ModdleElement} element - Element. + * @param {ModdleElement} connection - Connection that references + * bpmn:DataInput. + */ + function removeDataInput(element, connection) { + var dataInput = getDataInput(element, connection.targetRef); - if (!targetRefProp) { + if (!dataInput) { return; } - if (!usesTargetRef(element, targetRefProp, connection)) { - collectionRemove(element.get('properties'), targetRefProp); + var ioSpecification = element.get('ioSpecification'); + + if (ioSpecification && + ioSpecification.dataInputs && + ioSpecification.inputSets) { + + collectionRemove(ioSpecification.dataInputs, dataInput); + + collectionRemove(ioSpecification.inputSets[0].dataInputRefs, dataInput); + + cleanUpIoSpecification(element); } } /** - * Make sure targetRef is set to a valid property or + * Make sure targetRef is set to a valid bpmn:DataInput or * `null` if the connection is detached. * - * @param {Event} event + * @param {Event} event - Event. */ - function fixTargetRef(event) { - + function updateTargetRef(event) { var context = event.context, connection = context.connection, connectionBo = connection.businessObject, @@ -111,19 +161,20 @@ export default function DataInputAssociationBehavior(eventBus, bpmnFactory) { oldTargetBo = oldTarget && oldTarget.businessObject; var dataAssociation = connection.businessObject, - targetRefProp; + dataInput; if (oldTargetBo && oldTargetBo !== targetBo) { - cleanupTargetRef(oldTargetBo, connectionBo); + removeDataInput(oldTargetBo, connectionBo); } if (newTargetBo && newTargetBo !== targetBo) { - cleanupTargetRef(newTargetBo, connectionBo); + removeDataInput(newTargetBo, connectionBo); } if (targetBo) { - targetRefProp = getTargetRef(targetBo, true); - dataAssociation.targetRef = targetRefProp; + dataInput = createDataInput(targetBo, true); + + dataAssociation.targetRef = dataInput; } else { dataAssociation.targetRef = null; } @@ -137,6 +188,7 @@ DataInputAssociationBehavior.$inject = [ inherits(DataInputAssociationBehavior, CommandInterceptor); +// helpers ////////// /** * Only call the given function when the event @@ -146,7 +198,6 @@ inherits(DataInputAssociationBehavior, CommandInterceptor); * @return {Function} */ function ifDataInputAssociation(fn) { - return function(event) { var context = event.context, connection = context.connection; @@ -155,4 +206,59 @@ function ifDataInputAssociation(fn) { return fn(event); } }; +} + +/** + * Get bpmn:DataInput that is is referenced by element as targetRef. + * + * @param {ModdleElement} element - Element. + * @param {ModdleElement} targetRef - Element that is targetRef. + * + * @returns {ModdleElement} + */ +export function getDataInput(element, targetRef) { + var ioSpecification = element.get('ioSpecification'); + + if (ioSpecification && ioSpecification.dataInputs) { + return find(ioSpecification.dataInputs, function(dataInput) { + return dataInput === targetRef; + }); + } +} + +/** + * Clean up and remove bpmn:InputOutputSpecification from an element if it's empty. + * + * @param {ModdleElement} element - Element. + */ +function cleanUpIoSpecification(element) { + var ioSpecification = element.get('ioSpecification'); + + var dataInputs, + dataOutputs, + inputSets, + outputSets; + + if (ioSpecification) { + dataInputs = ioSpecification.dataInputs; + dataOutputs = ioSpecification.dataOutputs; + inputSets = ioSpecification.inputSets; + outputSets = ioSpecification.outputSets; + + if (dataInputs && !dataInputs.length) { + delete ioSpecification.dataInputs; + } + + if (dataOutputs && !dataOutputs.length) { + delete ioSpecification.dataOutputs; + } + + if ((!dataInputs || !dataInputs.length) && + (!dataOutputs || !dataOutputs.length) && + !inputSets[0].dataInputRefs.length && + !outputSets[0].dataOutputRefs.length) { + + delete element.ioSpecification; + } + } } \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/DataAssociationBehavior.bpmn b/test/spec/features/modeling/behavior/DataAssociationBehavior.bpmn new file mode 100644 index 0000000000..18b262fee5 --- /dev/null +++ b/test/spec/features/modeling/behavior/DataAssociationBehavior.bpmn @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn b/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn deleted file mode 100644 index 7e21cd1eb3..0000000000 --- a/test/spec/features/modeling/behavior/DataInputAssociationBehavior.bpmn +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - DataObjectReference - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js b/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js index 859e1dedc0..05d98929c2 100644 --- a/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js +++ b/test/spec/features/modeling/behavior/DataInputAssociationBehaviorSpec.js @@ -3,146 +3,256 @@ import { inject } from 'test/TestHelper'; -import { - find -} from 'min-dash'; - import modelingModule from 'lib/features/modeling'; +import { + getDataInput +} from 'lib/features/modeling/behavior/DataInputAssociationBehavior'; + -describe('modeling/behavior - fix DataInputAssociation#targetRef', function() { +describe('modeling/behavior - DataInputAssociationBehavior', function() { - var diagramXML = require('./DataInputAssociationBehavior.bpmn'); + var diagramXML = require('./DataAssociationBehavior.bpmn'); beforeEach(bootstrapModeler(diagramXML, { modules: modelingModule })); - it('should add on connect', inject(function(modeling, elementRegistry) { + it('should add bpmn:DataInput on connect', inject(function(elementRegistry, modeling) { // given - var dataObjectShape = elementRegistry.get('DataObjectReference'), - taskShape = elementRegistry.get('Task_B'); + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; // when - var newConnection = modeling.connect(dataObjectShape, taskShape, { + var dataInputAssociation = modeling.connect(dataObject, task, { type: 'bpmn:DataInputAssociation' }); - var dataInputAssociation = newConnection.businessObject; - // then - expect(dataInputAssociation.targetRef).to.exist; - expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(taskShape)); + var dataInputAssociationBo = dataInputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.inputSets).to.exist; + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets).to.exist; + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + + expect(dataInputAssociationBo.targetRef).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociationBo.targetRef)); })); - it('should remove on connect / undo', inject(function(modeling, elementRegistry, commandStack) { + it('should remove bpmn:DataInput on connect -> undo', inject(function(commandStack, elementRegistry, modeling) { // given - var dataObjectShape = elementRegistry.get('DataObjectReference'), - taskShape = elementRegistry.get('Task_B'); + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; - var newConnection = modeling.connect(dataObjectShape, taskShape, { + modeling.connect(dataObject, task, { type: 'bpmn:DataInputAssociation' }); - var dataInputAssociation = newConnection.businessObject; - // when commandStack.undo(); // then - expect(dataInputAssociation.targetRef).not.to.exist; - expect(getTargetRefProp(taskShape)).not.to.exist; + expect(taskBo.ioSpecification).not.to.exist; })); - it('should update on reconnectEnd', inject(function(modeling, elementRegistry) { + it('should update bpmn:DataInput on reconnectEnd', inject(function(elementRegistry, modeling) { // given - var oldTarget = elementRegistry.get('Task_A'), - connection = elementRegistry.get('DataInputAssociation'), - dataInputAssociation = connection.businessObject, - newTarget = elementRegistry.get('Task_B'); + var dataObject = elementRegistry.get('DataObjectReference_1'), + task1 = elementRegistry.get('Task_1'), + task1Bo = task1.businessObject, + task2 = elementRegistry.get('Task_2'), + task2Bo = task2.businessObject; + + var dataInputAssociation = modeling.connect(dataObject, task1, { + type: 'bpmn:DataInputAssociation' + }); // when - modeling.reconnectEnd(connection, newTarget, { x: newTarget.x, y: newTarget.y }); + modeling.reconnectEnd(dataInputAssociation, task2, { x: task2.x, y: task2.y }); // then - expect(getTargetRefProp(oldTarget)).not.to.exist; + var dataInputAssociationBo = dataInputAssociation.businessObject; + + expect(getDataInput(task1Bo, dataInputAssociationBo.targetRef)).not.to.exist; - expect(dataInputAssociation.targetRef).to.exist; - expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(newTarget)); + expect(getDataInput(task2Bo, dataInputAssociationBo.targetRef)).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(task2Bo, dataInputAssociationBo.targetRef)); })); - it('should update on reconnectEnd / undo', inject(function(modeling, elementRegistry, commandStack) { + it('should update bpmn:DataInput on reconnectEnd -> undo', inject(function(commandStack, elementRegistry, modeling) { // given - var oldTarget = elementRegistry.get('Task_A'), - connection = elementRegistry.get('DataInputAssociation'), - dataInputAssociation = connection.businessObject, - newTarget = elementRegistry.get('Task_B'); + var dataObject = elementRegistry.get('DataObjectReference_1'), + task1 = elementRegistry.get('Task_1'), + task1Bo = task1.businessObject, + task2 = elementRegistry.get('Task_2'), + task2Bo = task2.businessObject; - modeling.reconnectEnd(connection, newTarget, { x: newTarget.x, y: newTarget.y }); + var dataInputAssociation = modeling.connect(dataObject, task1, { + type: 'bpmn:DataInputAssociation' + }); + + modeling.reconnectEnd(dataInputAssociation, task2, { x: task2.x, y: task2.y }); // when commandStack.undo(); // then - expect(getTargetRefProp(newTarget)).not.to.exist; + var dataInputAssociationBo = dataInputAssociation.businessObject; - expect(dataInputAssociation.targetRef).to.exist; - expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(oldTarget)); + expect(getDataInput(task1Bo, dataInputAssociationBo.targetRef)).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(task1Bo, dataInputAssociationBo.targetRef)); + + expect(getDataInput(task2Bo, dataInputAssociationBo.targetRef)).not.to.exist; })); - it('should unset on remove', inject(function(modeling, elementRegistry) { + it('should remove bpmn:DataInput on remove', inject(function(elementRegistry, modeling) { // given - var oldTarget = elementRegistry.get('Task_A'), - connection = elementRegistry.get('DataInputAssociation'), - dataInputAssociation = connection.businessObject; + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation = modeling.connect(dataObject, task, { + type: 'bpmn:DataInputAssociation' + }); // when - modeling.removeElements([ connection ]); + modeling.removeElements([ dataInputAssociation ]); // then - expect(getTargetRefProp(oldTarget)).not.to.exist; - - expect(dataInputAssociation.targetRef).not.to.exist; + expect(taskBo.ioSpecification).not.to.exist; })); - it('should unset on remove / undo', inject(function(modeling, elementRegistry, commandStack) { + it('should add bpmn:DataInput on remove -> undo', inject(function(commandStack, elementRegistry, modeling) { // given - var oldTarget = elementRegistry.get('Task_A'), - connection = elementRegistry.get('DataInputAssociation'), - dataInputAssociation = connection.businessObject; + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation = modeling.connect(dataObject, task, { + type: 'bpmn:DataInputAssociation' + }); - modeling.removeElements([ connection ]); + modeling.removeElements([ dataInputAssociation ]); // when commandStack.undo(); // then - expect(dataInputAssociation.targetRef).to.exist; - expect(dataInputAssociation.targetRef).to.eql(getTargetRefProp(oldTarget)); + var dataInputAssociationBo = dataInputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(dataInputAssociationBo.targetRef).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociationBo.targetRef)); })); -}); + describe('multiple bpmn:DataInput elements', function() { + + it('should add second bpmn:DataInput on connect', inject(function(elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation1 = modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); + + // when + var dataInputAssociation2 = modeling.connect(dataObject2, task, { + type: 'bpmn:DataInputAssociation' + }); + + // then + var dataInputAssociation1Bo = dataInputAssociation1.businessObject, + dataInputAssociation2Bo = dataInputAssociation2.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.dataInputs).to.have.length(2); + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.inputSets[0].dataInputRefs).to.have.length(2); + + expect(dataInputAssociation1Bo.targetRef).to.exist; + expect(dataInputAssociation1Bo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociation1Bo.targetRef)); + + expect(dataInputAssociation2Bo.targetRef).to.exist; + expect(dataInputAssociation2Bo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociation2Bo.targetRef)); + })); + + + it('should remove second bpmn:DataInput on connect -> undo', inject(function(commandStack, elementRegistry, modeling) { + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; -function getTargetRefProp(element) { + var dataInputAssociation1 = modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); - expect(element).to.exist; + modeling.connect(dataObject2, task, { + type: 'bpmn:DataInputAssociation' + }); - var properties = element.businessObject.get('properties'); + // when + commandStack.undo(); + + // then + var dataInputAssociation1Bo = dataInputAssociation1.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.dataInputs).to.have.length(1); + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.inputSets[0].dataInputRefs).to.have.length(1); + + expect(dataInputAssociation1Bo.targetRef).to.exist; + expect(dataInputAssociation1Bo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociation1Bo.targetRef)); + })); + + + it('should remove all bpmn:DataInput elements on connect -> connect -> undo -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); + + modeling.connect(dataObject2, task, { + type: 'bpmn:DataInputAssociation' + }); + + // when + commandStack.undo(); + commandStack.undo(); + + // then + expect(taskBo.ioSpecification).not.to.exist; + })); - return find(properties, function(p) { - return p.name === '__targetRef_placeholder'; }); -} \ No newline at end of file + +}); \ No newline at end of file From bc7b9a3a6073f96ade59772949b55807a09a91f6 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Fri, 23 Nov 2018 13:19:03 +0100 Subject: [PATCH 2/4] feat(data-output-association-behavior): add and ensure BPMN 2.0 compatibility * reference bpmn:DataOutput as sourceRef Related to camunda/camunda-modeler#984 --- lib/features/modeling/BpmnFactory.js | 4 +- .../behavior/DataOutputAssociationBehavior.js | 265 ++++++++++++++++++ lib/features/modeling/behavior/index.js | 3 + .../DataOutputAssociationBehaviorSpec.js | 258 +++++++++++++++++ 4 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 lib/features/modeling/behavior/DataOutputAssociationBehavior.js create mode 100644 test/spec/features/modeling/behavior/DataOutputAssociationBehaviorSpec.js diff --git a/lib/features/modeling/BpmnFactory.js b/lib/features/modeling/BpmnFactory.js index a680594204..af80a20820 100644 --- a/lib/features/modeling/BpmnFactory.js +++ b/lib/features/modeling/BpmnFactory.js @@ -35,7 +35,9 @@ BpmnFactory.prototype._needsId = function(element) { 'bpmn:Property', 'bpmn:InputOutputSpecification', 'bpmn:DataInput', - 'bpmn:InputSet' + 'bpmn:InputSet', + 'bpmn:DataOutput', + 'bpmn:OutputSet' ]); }; diff --git a/lib/features/modeling/behavior/DataOutputAssociationBehavior.js b/lib/features/modeling/behavior/DataOutputAssociationBehavior.js new file mode 100644 index 0000000000..8b477dd4d7 --- /dev/null +++ b/lib/features/modeling/behavior/DataOutputAssociationBehavior.js @@ -0,0 +1,265 @@ +import inherits from 'inherits'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { + add as collectionAdd, + remove as collectionRemove +} from 'diagram-js/lib/util/Collections'; + +import { + find, + forEach +} from 'min-dash'; + +import { + is +} from '../../../util/ModelUtil'; + + +/** + * This behavior makes sure a bpmn:DataOutput is created and referenced when a + * bpmn:DataOutputAssociation is created. It also makes sure the bpmn:DataOutput and + * the reference are removed when a bpmn:OutputAssociation is removed. + * + * @param {EventBus} eventBus + * @param {BpmnFactory} bpmnFactory + */ +export default function DataOutputAssociationBehavior(eventBus, bpmnFactory) { + + CommandInterceptor.call(this, eventBus); + + + this.executed([ + 'connection.create', + 'connection.delete', + 'connection.move', + 'connection.reconnectStart' + ], ifDataOutputAssociation(updateSoureRef)); + + this.reverted([ + 'connection.create', + 'connection.delete', + 'connection.move', + 'connection.reconnectStart' + ], ifDataOutputAssociation(updateSoureRef)); + + /** + * Create and return bpmn:DataOutput. + * + * Create bpmn:InputOutputSpecification, dataOutputs and outputSets if not + * found. + * + * @param {ModdleElement} element - Element. + * + * @returns {ModdleElement} + */ + function createDataOutput(element) { + var ioSpecification = element.get('ioSpecification'); + + var inputSet, outputSet; + + if (!ioSpecification) { + ioSpecification = bpmnFactory.create('bpmn:InputOutputSpecification', { + dataOutputs: [], + outputSets: [] + }); + + element.ioSpecification = ioSpecification; + + inputSet = bpmnFactory.create('bpmn:InputSet', { + dataInputRefs: [], + name: 'Inputs' + }); + + inputSet.$parent = ioSpecification; + + collectionAdd(ioSpecification.get('inputSets'), inputSet); + + outputSet = bpmnFactory.create('bpmn:OutputSet', { + dataOutputRefs: [], + name: 'Outputs' + }); + + outputSet.$parent = ioSpecification; + + collectionAdd(ioSpecification.get('outputSets'), outputSet); + } + + var dataOutput = bpmnFactory.create('bpmn:DataOutput'); + + dataOutput.$parent = ioSpecification; + + if (!ioSpecification.dataOutputs) { + ioSpecification.dataOutputs = []; + } + + collectionAdd(ioSpecification.get('dataOutputs'), dataOutput); + + if (!ioSpecification.outputSets) { + outputSet = bpmnFactory.create('bpmn:OutputSet', { + dataOutputRefs: [], + name: 'Outputs' + }); + + outputSet.$parent = ioSpecification; + + collectionAdd(ioSpecification.get('outputSets'), outputSet); + } + + outputSet = ioSpecification.get('outputSets')[0]; + + collectionAdd(outputSet.dataOutputRefs, dataOutput); + + return dataOutput; + } + + /** + * Remove bpmn:DataOutput that is referenced by connection as sourceRef from + * bpmn:InputOutputSpecification. + * + * @param {ModdleElement} element - Element. + * @param {ModdleElement} connection - Connection that references + * bpmn:DataOutput. + */ + function removeDataOutput(element, connection) { + var dataOutput = getDataOutput(element, connection.get('sourceRef')[0]); + + if (!dataOutput) { + return; + } + + var ioSpecification = element.get('ioSpecification'); + + if (ioSpecification && + ioSpecification.dataOutputs && + ioSpecification.outputSets) { + + collectionRemove(ioSpecification.dataOutputs, dataOutput); + + collectionRemove(ioSpecification.outputSets[0].dataOutputRefs, dataOutput); + + cleanUpIoSpecification(element); + } + } + + /** + * Make sure sourceRef is set to a valid bpmn:DataOutput or + * `null` if the connection is detached. + * + * @param {Event} event - Event. + */ + function updateSoureRef(event) { + var context = event.context, + connection = context.connection, + connectionBo = connection.businessObject, + source = connection.source, + sourceBo = source && source.businessObject, + newsource = context.newsource, + newsourceBo = newsource && newsource.businessObject, + oldsource = context.oldsource || context.source, + oldsourceBo = oldsource && oldsource.businessObject; + + var dataAssociation = connection.businessObject, + dataOutput; + + if (oldsourceBo && oldsourceBo !== sourceBo) { + removeDataOutput(oldsourceBo, connectionBo); + } + + if (newsourceBo && newsourceBo !== sourceBo) { + removeDataOutput(newsourceBo, connectionBo); + } + + if (sourceBo) { + dataOutput = createDataOutput(sourceBo, true); + + // sourceRef is isMany + dataAssociation.get('sourceRef')[0] = dataOutput; + } else { + dataAssociation.sourceRef = null; + } + } +} + +DataOutputAssociationBehavior.$inject = [ + 'eventBus', + 'bpmnFactory' +]; + +inherits(DataOutputAssociationBehavior, CommandInterceptor); + +// helpers ////////// + +/** + * Only call the given function when the event + * touches a bpmn:DataOutputAssociation. + * + * @param {Function} fn + * @return {Function} + */ +function ifDataOutputAssociation(fn) { + return function(event) { + var context = event.context, + connection = context.connection; + + if (is(connection, 'bpmn:DataOutputAssociation')) { + return fn(event); + } + }; +} + +/** + * Get bpmn:DataOutput that is is referenced by element as sourceRef. + * + * @param {ModdleElement} element - Element. + * @param {ModdleElement} sourceRef - Element that is sourceRef. + * + * @returns {ModdleElement} + */ +export function getDataOutput(element, sourceRef) { + var ioSpecification = element.get('ioSpecification'); + + if (ioSpecification && ioSpecification.dataOutputs) { + return find(ioSpecification.dataOutputs, function(dataOutput) { + return dataOutput === sourceRef; + }); + } +} + +/** + * Clean up and remove bpmn:InputOutputSpecification from an element if it's empty. + * + * @param {ModdleElement} element - Element. + */ +function cleanUpIoSpecification(element) { + var ioSpecification = element.get('ioSpecification'); + + var dataInputs, + dataOutputs, + inputSets, + outputSets; + + if (ioSpecification) { + dataInputs = ioSpecification.dataInputs; + dataOutputs = ioSpecification.dataOutputs; + inputSets = ioSpecification.inputSets; + outputSets = ioSpecification.outputSets; + + if (dataInputs && !dataInputs.length) { + delete ioSpecification.dataInputs; + } + + if (dataOutputs && !dataOutputs.length) { + delete ioSpecification.dataOutputs; + } + + if ((!dataInputs || !dataInputs.length) && + (!dataOutputs || !dataOutputs.length) && + !inputSets[0].dataInputRefs.length && + !outputSets[0].dataOutputRefs.length) { + + delete element.ioSpecification; + } + } +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 98f0510290..4e9621f542 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -6,6 +6,7 @@ import CreateBoundaryEventBehavior from './CreateBoundaryEventBehavior'; import CreateDataObjectBehavior from './CreateDataObjectBehavior'; import CreateParticipantBehavior from './CreateParticipantBehavior'; import DataInputAssociationBehavior from './DataInputAssociationBehavior'; +import DataOutputAssociationBehavior from './DataOutputAssociationBehavior'; import DataStoreBehavior from './DataStoreBehavior'; import DeleteLaneBehavior from './DeleteLaneBehavior'; import DropOnFlowBehavior from './DropOnFlowBehavior'; @@ -33,6 +34,7 @@ export default { 'dataStoreBehavior', 'createParticipantBehavior', 'dataInputAssociationBehavior', + 'dataOutputAssociationBehavior', 'deleteLaneBehavior', 'dropOnFlowBehavior', 'importDockingFix', @@ -56,6 +58,7 @@ export default { createDataObjectBehavior: [ 'type', CreateDataObjectBehavior ], createParticipantBehavior: [ 'type', CreateParticipantBehavior ], dataInputAssociationBehavior: [ 'type', DataInputAssociationBehavior ], + dataOutputAssociationBehavior: [ 'type', DataOutputAssociationBehavior ], dataStoreBehavior: [ 'type', DataStoreBehavior ], deleteLaneBehavior: [ 'type', DeleteLaneBehavior ], dropOnFlowBehavior: [ 'type', DropOnFlowBehavior ], diff --git a/test/spec/features/modeling/behavior/DataOutputAssociationBehaviorSpec.js b/test/spec/features/modeling/behavior/DataOutputAssociationBehaviorSpec.js new file mode 100644 index 0000000000..8fc1f0a87f --- /dev/null +++ b/test/spec/features/modeling/behavior/DataOutputAssociationBehaviorSpec.js @@ -0,0 +1,258 @@ +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + +import modelingModule from 'lib/features/modeling'; + +import { + getDataOutput +} from 'lib/features/modeling/behavior/DataOutputAssociationBehavior'; + + +describe('modeling/behavior - DataOutputAssociationBehavior', function() { + + var diagramXML = require('./DataAssociationBehavior.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { modules: modelingModule })); + + + it('should add bpmn:DataOutput on connect', inject(function(elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + + // when + var dataOutputAssociation = modeling.connect(task, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + // then + var dataOutputAssociationBo = dataOutputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.inputSets).to.exist; + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets).to.exist; + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataOutputAssociationBo.get('sourceRef')[0])); + })); + + + it('should remove bpmn:DataOutput on connect -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + modeling.connect(task, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + commandStack.undo(); + + // then + expect(taskBo.ioSpecification).not.to.exist; + })); + + + it('should update bpmn:DataOutput on reconnectStart', inject(function(elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task1 = elementRegistry.get('Task_1'), + task1Bo = task1.businessObject, + task2 = elementRegistry.get('Task_2'), + task2Bo = task2.businessObject; + + var dataOutputAssociation = modeling.connect(task1, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + modeling.reconnectStart(dataOutputAssociation, task2, { x: task2.x, y: task2.y }); + + // then + var dataOutputAssociationBo = dataOutputAssociation.businessObject; + + expect(getDataOutput(task1Bo, dataOutputAssociationBo.get('sourceRef')[0])).not.to.exist; + + expect(getDataOutput(task2Bo, dataOutputAssociationBo.get('sourceRef')[0])).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.eql(getDataOutput(task2Bo, dataOutputAssociationBo.get('sourceRef')[0])); + })); + + + it('should update bpmn:DataOutput on reconnectEnd -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task1 = elementRegistry.get('Task_1'), + task1Bo = task1.businessObject, + task2 = elementRegistry.get('Task_2'), + task2Bo = task2.businessObject; + + var dataOutputAssociation = modeling.connect(task1, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + modeling.reconnectStart(dataOutputAssociation, task2, { x: task2.x, y: task2.y }); + + // when + commandStack.undo(); + + // then + var dataOutputAssociationBo = dataOutputAssociation.businessObject; + + expect(getDataOutput(task1Bo, dataOutputAssociationBo.get('sourceRef')[0])).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.eql(getDataOutput(task1Bo, dataOutputAssociationBo.get('sourceRef')[0])); + + expect(getDataOutput(task2Bo, dataOutputAssociationBo.get('sourceRef')[0])).not.to.exist; + })); + + + it('should remove bpmn:DataOutput on remove', inject(function(elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataOutputAssociation = modeling.connect(task, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + modeling.removeElements([ dataOutputAssociation ]); + + // then + expect(taskBo.ioSpecification).not.to.exist; + })); + + + it('should add bpmn:DataOutput on remove -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject = elementRegistry.get('DataObjectReference_1'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataOutputAssociation = modeling.connect(task, dataObject, { + type: 'bpmn:DataOutputAssociation' + }); + + modeling.removeElements([ dataOutputAssociation ]); + + // when + commandStack.undo(); + + // then + var dataOutputAssociationBo = dataOutputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataOutputAssociationBo.get('sourceRef')[0])); + })); + + + describe('multiple bpmn:DataOutput elements', function() { + + it('should add second bpmn:DataOutput on connect', inject(function(elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation1 = modeling.connect(task, dataObject1, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + var dataInputAssociation2 = modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // then + var dataInputAssociation1Bo = dataInputAssociation1.businessObject, + dataInputAssociation2Bo = dataInputAssociation2.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.dataOutputs).to.have.length(2); + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets[0].dataOutputRefs).to.have.length(2); + + expect(dataInputAssociation1Bo.get('sourceRef')[0]).to.exist; + expect(dataInputAssociation1Bo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataInputAssociation1Bo.get('sourceRef')[0])); + + expect(dataInputAssociation2Bo.get('sourceRef')[0]).to.exist; + expect(dataInputAssociation2Bo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataInputAssociation2Bo.get('sourceRef')[0])); + })); + + + it('should remove second bpmn:DataOutput on connect -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation1 = modeling.connect(task, dataObject1, { + type: 'bpmn:DataOutputAssociation' + }); + + modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + commandStack.undo(); + + // then + var dataInputAssociation1Bo = dataInputAssociation1.businessObject; + + expect(taskBo.ioSpecification).to.exist; + expect(taskBo.ioSpecification.dataOutputs).to.have.length(1); + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets[0].dataOutputRefs).to.have.length(1); + + expect(dataInputAssociation1Bo.get('sourceRef')[0]).to.exist; + expect(dataInputAssociation1Bo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataInputAssociation1Bo.get('sourceRef')[0])); + })); + + + it('should remove all bpmn:DataOutput elements on connect -> connect -> undo -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + modeling.connect(task, dataObject1, { + type: 'bpmn:DataOutputAssociation' + }); + + modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + commandStack.undo(); + commandStack.undo(); + + // then + expect(taskBo.ioSpecification).not.to.exist; + })); + + }); + +}); \ No newline at end of file From cd2904ca20fda7d9393f38d82165901b7a43f075 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Fri, 23 Nov 2018 13:19:40 +0100 Subject: [PATCH 3/4] feat(data-association-behavior): test integration of both behaviors --- .../behavior/DataAssociationBehaviorSpec.js | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/spec/features/modeling/behavior/DataAssociationBehaviorSpec.js diff --git a/test/spec/features/modeling/behavior/DataAssociationBehaviorSpec.js b/test/spec/features/modeling/behavior/DataAssociationBehaviorSpec.js new file mode 100644 index 0000000000..7c68a081af --- /dev/null +++ b/test/spec/features/modeling/behavior/DataAssociationBehaviorSpec.js @@ -0,0 +1,129 @@ +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + +import modelingModule from 'lib/features/modeling'; + +import { + getDataInput +} from 'lib/features/modeling/behavior/DataInputAssociationBehavior'; + +import { + getDataOutput +} from 'lib/features/modeling/behavior/DataOutputAssociationBehavior'; + + +describe('modeling/behavior - DataInputAssociationBehavior and DataOutputAssociationBehavior integration', function() { + + var diagramXML = require('./DataAssociationBehavior.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { modules: modelingModule })); + + + it('should add bpmn:DataInput and bpmn:DataOutput on connect -> connect', inject(function(elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation = modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); + + // when + var dataOutputAssociation = modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // then + var dataInputAssociationBo = dataInputAssociation.businessObject, + dataOutputAssociationBo = dataOutputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + + expect(taskBo.ioSpecification.dataInputs).to.have.length(1); + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.inputSets[0].dataInputRefs).to.have.length(1); + + expect(taskBo.ioSpecification.dataOutputs).to.have.length(1); + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets[0].dataOutputRefs).to.have.length(1); + + expect(dataInputAssociationBo.targetRef).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociationBo.targetRef)); + + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.exist; + expect(dataOutputAssociationBo.get('sourceRef')[0]).to.eql(getDataOutput(taskBo, dataOutputAssociationBo.get('sourceRef')[0])); + })); + + + it('should remove bpmn:DataOutput on connect -> undo', inject(function(commandStack, elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + var dataInputAssociation = modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); + + modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + commandStack.undo(); + + // then + var dataInputAssociationBo = dataInputAssociation.businessObject; + + expect(taskBo.ioSpecification).to.exist; + + expect(taskBo.ioSpecification.dataInputs).to.have.length(1); + expect(taskBo.ioSpecification.dataOutputs).to.not.exist; + + expect(taskBo.ioSpecification.inputSets).to.exist; + expect(taskBo.ioSpecification.inputSets).to.have.length(1); + expect(taskBo.ioSpecification.inputSets[0].dataInputRefs).to.have.length(1); + + expect(taskBo.ioSpecification.outputSets).to.exist; + expect(taskBo.ioSpecification.outputSets).to.have.length(1); + expect(taskBo.ioSpecification.outputSets[0].dataOutputRefs).to.have.length(0); + + expect(dataInputAssociationBo.targetRef).to.exist; + expect(dataInputAssociationBo.targetRef).to.eql(getDataInput(taskBo, dataInputAssociationBo.targetRef)); + })); + + + it('should remove bpmn:DataInput and bpmn:DataOutput on connect -> connect -> undo -> undo', inject( + function(commandStack, elementRegistry, modeling) { + + // given + var dataObject1 = elementRegistry.get('DataObjectReference_1'), + dataObject2 = elementRegistry.get('DataObjectReference_2'), + task = elementRegistry.get('Task_1'), + taskBo = task.businessObject; + + modeling.connect(dataObject1, task, { + type: 'bpmn:DataInputAssociation' + }); + + modeling.connect(task, dataObject2, { + type: 'bpmn:DataOutputAssociation' + }); + + // when + commandStack.undo(); + commandStack.undo(); + + // then + expect(taskBo.ioSpecification).not.to.exist; + } + )); + +}); \ No newline at end of file From fddf8701c22abb4419f9fd27cdace76019ede9cf Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Fri, 23 Nov 2018 14:34:08 +0100 Subject: [PATCH 4/4] fix(test): fix test involving bpmn:DataObjectReference --- test/spec/features/replace/BpmnReplaceSpec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/features/replace/BpmnReplaceSpec.js b/test/spec/features/replace/BpmnReplaceSpec.js index 6bc9bf911a..9cf62867d4 100644 --- a/test/spec/features/replace/BpmnReplaceSpec.js +++ b/test/spec/features/replace/BpmnReplaceSpec.js @@ -344,8 +344,8 @@ describe('features/replace - bpmn replace', function() { var inputAssociation = inputAssociations[0]; - // expect input association references __target_ref_placeholder property - expect(inputAssociation.targetRef).to.equal(bo.properties[0]); + // expect input association targetRef + expect(inputAssociation.targetRef).to.equal(bo.ioSpecification.dataInputs[0]); // ...and // expect one outgoing connection