From 30b8ed6c480a23a51fd401b7eeb58bcb6167d25c Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Thu, 21 May 2015 14:55:10 +0200 Subject: [PATCH] feat(modeling): add generic create-on-flow Closes #232 --- .../modeling/behavior/CreateBehavior.js | 19 --- .../modeling/behavior/CreateOnFlowBehavior.js | 95 ++++++++++++ lib/features/modeling/behavior/index.js | 2 + lib/features/modeling/rules/BpmnRules.js | 35 ++--- lib/features/snapping/BpmnSnapping.js | 5 +- .../DropOnConnectionSpec.js | 36 ----- .../behavior/CreateOnFlowBehavior.bpmn} | 26 ++-- .../behavior/CreateOnFlowBehaviorSpec.js | 143 ++++++++++++++++++ 8 files changed, 272 insertions(+), 89 deletions(-) create mode 100644 lib/features/modeling/behavior/CreateOnFlowBehavior.js delete mode 100644 test/spec/features/drop-on-connection/DropOnConnectionSpec.js rename test/spec/features/{drop-on-connection/diagram.bpmn => modeling/behavior/CreateOnFlowBehavior.bpmn} (55%) create mode 100644 test/spec/features/modeling/behavior/CreateOnFlowBehaviorSpec.js diff --git a/lib/features/modeling/behavior/CreateBehavior.js b/lib/features/modeling/behavior/CreateBehavior.js index d0cb040e9a..0db1e246a4 100644 --- a/lib/features/modeling/behavior/CreateBehavior.js +++ b/lib/features/modeling/behavior/CreateBehavior.js @@ -25,11 +25,6 @@ function CreateBehavior(eventBus, modeling) { shape = context.shape, position = context.position; - if (is(parent, 'bpmn:SequenceFlow')){ - context.insertTarget = parent; - context.parent = context.parent.parent; - } - if (is(parent, 'bpmn:Process') && is(shape, 'bpmn:Participant')) { // this is going to detach the process root @@ -80,20 +75,6 @@ function CreateBehavior(eventBus, modeling) { var processChildren = processRoot.children.slice(); modeling.moveShapes(processChildren, { x: 0, y: 0 }, shape); } - - if (context.insertTarget) { - - var initialTarget = context.insertTarget.target; - var insertShape = context.shape; - - // reconnecting end to inserted shape - modeling.reconnectEnd(context.insertTarget, insertShape, context.position); - - // create new connection between inserted shape and initial target - modeling.createConnection(insertShape, initialTarget, { - type: context.insertTarget.type, - }, context.parent); - } }, true); } diff --git a/lib/features/modeling/behavior/CreateOnFlowBehavior.js b/lib/features/modeling/behavior/CreateOnFlowBehavior.js new file mode 100644 index 0000000000..7ffb21381d --- /dev/null +++ b/lib/features/modeling/behavior/CreateOnFlowBehavior.js @@ -0,0 +1,95 @@ +'use strict'; + +var inherits = require('inherits'); + +var assign = require('lodash/object/assign'); + +var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor'); + +var getApproxIntersection = require('diagram-js/lib/util/LineIntersection').getApproxIntersection; + + +function copy(obj) { + return assign({}, obj); +} + +function CreateOnFlowBehavior(eventBus, bpmnRules, modeling) { + + CommandInterceptor.call(this, eventBus); + + /** + * Reconnect start / end of a connection after + * dropping an element on a flow. + */ + + this.preExecute('shape.create', function(context) { + + var parent = context.parent, + shape = context.shape; + + if (bpmnRules.canInsert(shape, parent)) { + context.insertTarget = parent; + context.parent = parent.parent; + } + }, true); + + + this.postExecute('shape.create', function(context) { + + var shape = context.shape, + insertTarget = context.insertTarget, + position = context.position, + source, + target, + reconnected, + intersection, + waypoints, + waypointsBefore, + waypointsAfter, + dockingPoint; + + if (insertTarget) { + + waypoints = insertTarget.waypoints; + + + intersection = getApproxIntersection(waypoints, position); + + if (intersection) { + waypointsBefore = waypoints.slice(0, intersection.index); + waypointsAfter = waypoints.slice(intersection.index + (intersection.bendpoint ? 1 : 0)); + + dockingPoint = intersection.bendpoint ? waypoints[intersection.index] : position; + + waypointsBefore.push(copy(dockingPoint)); + waypointsAfter.unshift(copy(dockingPoint)); + } + + source = insertTarget.source; + target = insertTarget.target; + + if (bpmnRules.canConnect(source, shape, insertTarget)) { + // reconnect source -> inserted shape + modeling.reconnectEnd(insertTarget, shape, waypointsBefore || copy(position)); + + reconnected = true; + } + + if (bpmnRules.canConnect(shape, target, insertTarget)) { + + if (!reconnected) { + // reconnect inserted shape -> end + modeling.reconnectStart(insertTarget, shape, waypointsAfter || copy(position)); + } else { + modeling.connect(shape, target, { type: insertTarget.type, waypoints: waypointsAfter }); + } + } + } + }, true); +} + +inherits(CreateOnFlowBehavior, CommandInterceptor); + +CreateOnFlowBehavior.$inject = [ 'eventBus', 'bpmnRules', 'modeling' ]; + +module.exports = CreateOnFlowBehavior; \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 82bb3cd4ba..ca4714afdf 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -2,6 +2,7 @@ module.exports = { __init__: [ 'appendBehavior', 'createBehavior', + 'createOnFlowBehavior', 'dropBehavior', 'removeBehavior', 'modelingFeedback' @@ -9,6 +10,7 @@ module.exports = { appendBehavior: [ 'type', require('./AppendBehavior') ], dropBehavior: [ 'type', require('./DropBehavior') ], createBehavior: [ 'type', require('./CreateBehavior') ], + createOnFlowBehavior: [ 'type', require('./CreateOnFlowBehavior') ], removeBehavior: [ 'type', require('./RemoveBehavior') ], modelingFeedback: [ 'type', require('./ModelingFeedback') ] }; \ No newline at end of file diff --git a/lib/features/modeling/rules/BpmnRules.js b/lib/features/modeling/rules/BpmnRules.js index a12bd182ac..00b24c9812 100644 --- a/lib/features/modeling/rules/BpmnRules.js +++ b/lib/features/modeling/rules/BpmnRules.js @@ -94,6 +94,8 @@ BpmnRules.prototype.canMove = canMove; BpmnRules.prototype.canDrop = canDrop; +BpmnRules.prototype.canInsert = canInsert; + BpmnRules.prototype.canCreate = canCreate; BpmnRules.prototype.canConnect = canConnect; @@ -339,11 +341,7 @@ function canCreate(shape, target, source) { return false; } - if (canInsert(shape, target)){ - return true; - } - - return canDrop(shape, target); + return canDrop(shape, target) || canInsert(shape, target); } function canResize(shape, newBounds) { @@ -391,20 +389,15 @@ function canConnectSequenceFlow(source, target) { !(is(source, 'bpmn:EventBasedGateway') && !isEventBasedTarget(target)); } -function canInsert(shape, target) { - - var startEvent = target.source; - var endEvent = target.target; - - if (!is(target, 'bpmn:SequenceFlow')) { - return false; - } - - if(is(shape, 'bpmn:IntermediateThrowEvent') && - is(startEvent, 'bpmn:FlowElement') && - is(endEvent, 'bpmn:FlowElement')) { - return true; - } - - return false; +function canInsert(shape, flow) { + + // return true if we can drop on the + // underlying flow parent + // + // at this point we are not really able to talk + // about connection rules (yet) + return ( + is(flow, 'bpmn:SequenceFlow') || + is(flow, 'bpmn:MessageFlow') + ) && canDrop(shape, flow.parent); } \ No newline at end of file diff --git a/lib/features/snapping/BpmnSnapping.js b/lib/features/snapping/BpmnSnapping.js index d13fb50ac8..c6e59d8b5e 100644 --- a/lib/features/snapping/BpmnSnapping.js +++ b/lib/features/snapping/BpmnSnapping.js @@ -137,7 +137,7 @@ function BpmnSnapping(eventBus, canvas) { context.minDimensions = { width: 50, height: 50 }; } }); - + } inherits(BpmnSnapping, Snapping); @@ -227,6 +227,9 @@ BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) { var siblings = this.getSiblings(shape, target); + if (is(target, 'bpmn:SequenceFlow')) { + this.addTargetSnaps(snapPoints, shape, target.parent); + } forEach(siblings, function(s) { snapPoints.add('mid', mid(s)); diff --git a/test/spec/features/drop-on-connection/DropOnConnectionSpec.js b/test/spec/features/drop-on-connection/DropOnConnectionSpec.js deleted file mode 100644 index 101915f0c8..0000000000 --- a/test/spec/features/drop-on-connection/DropOnConnectionSpec.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var TestHelper = require('../../../TestHelper'); - -describe('drop on conection', function(){ - - var bpmnRules = require('../../../../lib/features/modeling/rules'); - var diagramXML = require('./diagram.bpmn'); - - beforeEach(bootstrapModeler(diagramXML, {modules: bpmnRules})); - - it('should be allowed for an IntermediateThrowEvent', inject(function(elementRegistry, bpmnRules, elementFactory) { - var sequenceFlow = elementRegistry.get('SequenceFlow_0lk9mnl'); - var intermediateThrowEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' }); - - expect(bpmnRules.canCreate(intermediateThrowEvent, sequenceFlow)).toBe(true); - })); - - it('should rearrange connections', inject(function(modeling, elementRegistry, elementFactory){ - var intermediateThrowEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' }); - var startEvent = elementRegistry.get('StartEvent_1'); - var sequenceFlow = elementRegistry.get('SequenceFlow_0lk9mnl'); - var task = elementRegistry.get('Task_195jx60'); - var position = {x: startEvent.x + startEvent.height/2 + 100, - y: startEvent.y + startEvent.width/2}; - - // create new intermediateThrowEvent onto sequenceFlow - modeling.createShape(intermediateThrowEvent, position, sequenceFlow); - - // check rearragned connection - expect(startEvent.outgoing[0].id).toBe(intermediateThrowEvent.incoming[0].id); - - // check newly created connection - expect(intermediateThrowEvent.outgoing[0].id).toBe(task.incoming[0].id); - })); -}); \ No newline at end of file diff --git a/test/spec/features/drop-on-connection/diagram.bpmn b/test/spec/features/modeling/behavior/CreateOnFlowBehavior.bpmn similarity index 55% rename from test/spec/features/drop-on-connection/diagram.bpmn rename to test/spec/features/modeling/behavior/CreateOnFlowBehavior.bpmn index d2c4893c57..3bc7b4d771 100644 --- a/test/spec/features/drop-on-connection/diagram.bpmn +++ b/test/spec/features/modeling/behavior/CreateOnFlowBehavior.bpmn @@ -1,25 +1,27 @@ - - - SequenceFlow_0lk9mnl + + + SequenceFlow - - SequenceFlow_0lk9mnl + + SequenceFlow - + - - + + - - + + - + - + + + diff --git a/test/spec/features/modeling/behavior/CreateOnFlowBehaviorSpec.js b/test/spec/features/modeling/behavior/CreateOnFlowBehaviorSpec.js new file mode 100644 index 0000000000..9d5f80bc98 --- /dev/null +++ b/test/spec/features/modeling/behavior/CreateOnFlowBehaviorSpec.js @@ -0,0 +1,143 @@ +'use strict'; + +var TestHelper = require('../../../../TestHelper'), + Matchers = require('../../../../Matchers'); + +/* global inject, bootstrapModeler */ + + +var modelingModule = require('../../../../../lib/features/modeling'); + + +describe('modeling/behavior - drop on connection', function(){ + + beforeEach(Matchers.addDeepEquals); + + + var diagramXML = require('./CreateOnFlowBehavior.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { modules: modelingModule })); + + + describe('rules', function() { + + it('should be allowed for an IntermediateThrowEvent', inject(function(elementRegistry, bpmnRules, elementFactory) { + + // when + var sequenceFlow = elementRegistry.get('SequenceFlow'); + var intermediateThrowEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' }); + + // then + expect(bpmnRules.canCreate(intermediateThrowEvent, sequenceFlow)).toBe(true); + })); + + }); + + + describe('execution', function() { + + it('should connect start -> target -> end', inject(function(modeling, elementRegistry, elementFactory) { + + // given + var intermediateThrowEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' }); + + var startEvent = elementRegistry.get('StartEvent'), + sequenceFlow = elementRegistry.get('SequenceFlow'), + task = elementRegistry.get('Task'); + + var position = { x: 340, y: 120 }; // first bendpoint + + // when + var newShape = modeling.createShape(intermediateThrowEvent, position, sequenceFlow); + + // then + + var targetConnection = newShape.outgoing[0]; + + // new incoming connection + expect(newShape.incoming.length).toBe(1); + expect(newShape.incoming[0]).toBe(sequenceFlow); + + // new outgoing connection + expect(newShape.outgoing.length).toBe(1); + expect(targetConnection).toBeTruthy(); + expect(targetConnection.type).toBe('bpmn:SequenceFlow'); + + expect(startEvent.outgoing[0]).toBe(newShape.incoming[0]); + expect(task.incoming[0]).toBe(newShape.outgoing[0]); + + // split target at insertion point + expect(sequenceFlow.waypoints).toDeepEqual([ + { original: { x: 209, y: 120 }, x: 209, y: 120 }, + { original: { x: 340, y: 120 }, x: 322, y: 120 } + ]); + + expect(targetConnection.waypoints).toDeepEqual([ + { original: { x: 340, y: 120 }, x: 340, y: 138 }, + { x: 340, y: 299 }, + { original: { x: 502, y: 299 }, x: 502, y: 299 } + ]); + })); + + + it('should connect start -> target', inject(function(modeling, elementRegistry, elementFactory) { + + // given + var endEventShape = elementFactory.createShape({ type: 'bpmn:EndEvent' }); + + var sequenceFlow = elementRegistry.get('SequenceFlow'); + + var position = { x: 340, y: 120 }; // first bendpoint + + // when + var newShape = modeling.createShape(endEventShape, position, sequenceFlow); + + // then + + // new incoming connection + expect(newShape.incoming.length).toBe(1); + expect(newShape.incoming[0]).toBe(sequenceFlow); + + // no outgoing edges + expect(newShape.outgoing.length).toBe(0); + + // split target at insertion point + expect(sequenceFlow.waypoints).toDeepEqual([ + { original: { x: 209, y: 120 }, x: 209, y: 120 }, + { original: { x: 340, y: 120 }, x: 322, y: 120 } + ]); + })); + + + it('should connect target -> end', inject(function(modeling, elementRegistry, elementFactory) { + + // given + var startEventShape = elementFactory.createShape({ type: 'bpmn:StartEvent' }); + + var sequenceFlow = elementRegistry.get('SequenceFlow'); + + var position = { x: 340, y: 120 }; // first bendpoint + + // when + var newShape = modeling.createShape(startEventShape, position, sequenceFlow); + + // then + + // no incoming connection + expect(newShape.incoming.length).toBe(0); + + // no outgoing edges + expect(newShape.outgoing.length).toBe(1); + expect(newShape.outgoing[0]).toBe(sequenceFlow); + + // split target at insertion point + expect(sequenceFlow.waypoints).toDeepEqual([ + { original: { x: 340, y: 120 }, x: 340, y: 138 }, + { x: 340, y: 299 }, + { original: { x: 502, y: 299 }, x: 502, y: 299 } + ]); + })); + + }); + +}); \ No newline at end of file