diff --git a/lib/features/modeling/behavior/DropBehavior.js b/lib/features/modeling/behavior/DropBehavior.js index 469299e7..bbbae2e6 100644 --- a/lib/features/modeling/behavior/DropBehavior.js +++ b/lib/features/modeling/behavior/DropBehavior.js @@ -5,7 +5,11 @@ var forEach = require('lodash/collection/forEach'), var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor'); -function DropBehavior(eventBus, modeling) { +var is = require('../ModelingUtil').is, + getSharedParent = require('../ModelingUtil').getSharedParent; + + +function DropBehavior(eventBus, modeling, bpmnRules) { CommandInterceptor.call(this, eventBus); @@ -19,16 +23,67 @@ function DropBehavior(eventBus, modeling) { forEach(allConnections, function(c) { - // remove sequence flows having source / target on different parents - if (c.businessObject.$instanceOf('bpmn:SequenceFlow') && c.source.parent !== c.target.parent) { + var source = c.source, + target = c.target; + + var replacementType, + remove; + + /** + * Check if incoming or outgoing connections + * can stay or could be substituted with an + * appropriate replacement. + * + * This holds true for SequenceFlow <> MessageFlow. + */ + + if (is(c, 'bpmn:SequenceFlow')) { + if (!bpmnRules.canConnectSequenceFlow(source, target)) { + remove = true; + } + + if (bpmnRules.canConnectMessageFlow(source, target)) { + replacementType = 'bpmn:MessageFlow'; + } + } + + // transform message flows into sequence flows, if possible + + if (is(c, 'bpmn:MessageFlow')) { + + if (!bpmnRules.canConnectMessageFlow(source, target)) { + remove = true; + } + + if (bpmnRules.canConnectSequenceFlow(source, target)) { + replacementType = 'bpmn:SequenceFlow'; + } + } + + if (is(c, 'bpmn:Association') && !bpmnRules.canConnectAssociation(source, target)) { + remove = true; + } + + + // remove invalid connection + if (remove) { modeling.removeConnection(c); } + + // replace SequenceFlow <> MessageFlow + + if (replacementType) { + modeling.createConnection(source, target, { + type: replacementType, + waypoints: c.waypoints.slice() + }, getSharedParent(source, target)); + } }); }, true); } inherits(DropBehavior, CommandInterceptor); -DropBehavior.$inject = [ 'eventBus', 'modeling' ]; +DropBehavior.$inject = [ 'eventBus', 'modeling', 'bpmnRules' ]; module.exports = DropBehavior; \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/DropBehavior.association.bpmn b/test/spec/features/modeling/behavior/DropBehavior.association.bpmn new file mode 100644 index 00000000..ce0e5a1a --- /dev/null +++ b/test/spec/features/modeling/behavior/DropBehavior.association.bpmn @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/DropBehavior.message-sequence-flow.bpmn b/test/spec/features/modeling/behavior/DropBehavior.message-sequence-flow.bpmn new file mode 100644 index 00000000..ba239791 --- /dev/null +++ b/test/spec/features/modeling/behavior/DropBehavior.message-sequence-flow.bpmn @@ -0,0 +1,122 @@ + + + + + + + + + + + + + SequenceFlow_1 + + SequenceFlow_2 + + + + SequenceFlow_2 + + + + SequenceFlow_3 + SequenceFlow_1 + + + + SequenceFlow_3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/DropBehaviorSpec.js b/test/spec/features/modeling/behavior/DropBehaviorSpec.js new file mode 100644 index 00000000..5336d902 --- /dev/null +++ b/test/spec/features/modeling/behavior/DropBehaviorSpec.js @@ -0,0 +1,240 @@ +'use strict'; + +var TestHelper = require('../../../../TestHelper'); + +/* global bootstrapModeler, inject */ + +var is = require('../../../../../lib/features/modeling/ModelingUtil').is, + find = require('lodash/collection/find'); + +var modelingModule = require('../../../../../lib/features/modeling'), + coreModule = require('../../../../../lib/core'); + + +function getConnection(source, target, connectionOrType) { + return find(source.outgoing, function(c) { + return c.target === target && + (typeof connectionOrType === 'string' ? is(c, connectionOrType) : c === connectionOrType); + }); +} + +function expectConnected(source, target, connectionOrType) { + expect(getConnection(source, target, connectionOrType)).toBeDefined(); +} + +function expectNotConnected(source, target, connectionOrType) { + expect(getConnection(source, target, connectionOrType)).not.toBeDefined(); +} + + +describe('features/modeling - drop behavior', function() { + + var testModules = [ coreModule, modelingModule ]; + + + describe('should replace SequenceFlow <> MessageFlow', function() { + + var processDiagramXML = require('./DropBehavior.message-sequence-flow.bpmn'); + + beforeEach(bootstrapModeler(processDiagramXML, { modules: testModules })); + + + var element; + + beforeEach(inject(function(elementRegistry) { + element = function(id) { + return elementRegistry.get(id); + }; + })); + + + describe('moving single shape', function() { + + it('execute', inject(function(modeling, elementRegistry) { + + // given + var taskShape = element('Task_2'), + targetShape = element('Participant_2'); + + // when + modeling.moveShapes([ taskShape ], { x: 0, y: 330 }, targetShape); + + // then + expect(taskShape.parent).toBe(targetShape); + + expectNotConnected(element('StartEvent_1'), taskShape, 'bpmn:SequenceFlow'); + + expectConnected(taskShape, element('Task_4'), 'bpmn:SequenceFlow'); + expectConnected(taskShape, element('SubProcess_1'), 'bpmn:MessageFlow'); + })); + + + it('undo', inject(function(modeling, elementRegistry, commandStack) { + + // given + var taskShape = element('Task_2'), + targetShape = element('Participant_2'), + oldParent = taskShape.parent; + + modeling.moveShapes([ taskShape ], { x: 0, y: 300 }, targetShape); + + // when + commandStack.undo(); + + // then + expect(taskShape.parent).toBe(oldParent); + + expectConnected(element('StartEvent_1'), taskShape, element('SequenceFlow_3')); + + expectConnected(taskShape, element('Task_4'), element('MessageFlow_5')); + expectConnected(taskShape, element('SubProcess_1'), element('SequenceFlow_1')); + })); + + }); + + + describe('moving multiple shapes', function() { + + it('execute', inject(function(modeling, elementRegistry) { + + // given + var startEventShape = element('StartEvent_1'), + taskShape = element('Task_2'), + targetShape = element('Participant_2'); + + // when + modeling.moveShapes([ startEventShape, taskShape ], { x: 0, y: 330 }, targetShape); + + // then + expect(taskShape.parent).toBe(targetShape); + expect(startEventShape.parent).toBe(targetShape); + + expectConnected(startEventShape, taskShape, element('SequenceFlow_3')); + + expectNotConnected(element('Participant_2'), startEventShape, 'bpmn:MessageFlow'); + expectConnected(taskShape, element('SubProcess_1'), 'bpmn:MessageFlow'); + })); + + + it('undo', inject(function(modeling, elementRegistry, commandStack) { + + // given + var taskShape = element('Task_2'), + targetShape = element('Participant_2'), + oldParent = taskShape.parent; + + modeling.moveShapes([ taskShape ], { x: 0, y: 300 }, targetShape); + + // when + commandStack.undo(); + + // then + expect(taskShape.parent).toBe(oldParent); + + expectConnected(element('StartEvent_1'), taskShape, element('SequenceFlow_3')); + + expectConnected(taskShape, element('Task_4'), element('MessageFlow_5')); + expectConnected(taskShape, element('SubProcess_1'), element('SequenceFlow_1')); + + expectConnected(element('Participant_2'), element('StartEvent_1'), element('MessageFlow_4')); + })); + + }); + + + describe('moving nested shapes', function() { + + it('execute', inject(function(modeling, elementRegistry) { + + // given + var subProcessShape = element('SubProcess_1'), + targetShape = element('Participant_2'); + + // when + modeling.moveShapes([ subProcessShape ], { x: 0, y: 530 }, targetShape); + + // then + expect(subProcessShape.parent).toBe(targetShape); + + expectConnected(element('Task_2'), subProcessShape, 'bpmn:MessageFlow'); + + expectNotConnected(element('Task_1'), element('Participant_2'), 'bpmn:MessageFlow'); + })); + + + it('undo', inject(function(modeling, elementRegistry, commandStack) { + + // given + var subProcessShape = element('SubProcess_1'), + targetShape = element('Participant_2'); + + modeling.moveShapes([ subProcessShape ], { x: 0, y: 530 }, targetShape); + + // when + commandStack.undo(); + + // then + expectConnected(element('Task_2'), subProcessShape, element('SequenceFlow_1')); + + expectConnected(element('Task_1'), element('Participant_2'), element('MessageFlow_3')); + })); + + }); + + }); + + + describe('should replace SequenceFlow <> MessageFlow', function() { + + var processDiagramXML = require('./DropBehavior.association.bpmn'); + + beforeEach(bootstrapModeler(processDiagramXML, { modules: testModules })); + + + var element; + + beforeEach(inject(function(elementRegistry) { + element = function(id) { + return elementRegistry.get(id); + }; + })); + + + describe('moving text-annotation to participant', function() { + + it('execute', inject(function(modeling, elementRegistry) { + + // given + var textAnnotationShape = element('TextAnnotation_1'), + targetShape = element('Participant_1'); + + // when + modeling.moveShapes([ textAnnotationShape ], { x: -200, y: 40 }, targetShape); + + // then + expect(textAnnotationShape.parent).toBe(targetShape); + + expectNotConnected(textAnnotationShape, targetShape, 'bpmn:TextAnnotation'); + })); + + + it('undo', inject(function(modeling, elementRegistry, commandStack) { + + // given + var textAnnotationShape = element('TextAnnotation_1'), + targetShape = element('Participant_1'); + + modeling.moveShapes([ textAnnotationShape ], { x: -200, y: 40 }, targetShape); + + // when + commandStack.undo(); + + // then + expectConnected(textAnnotationShape, targetShape, element('Association_1')); + })); + + }); + + }); + +}); \ No newline at end of file