feat(modeling): add generic create-on-flow

Closes #232
This commit is contained in:
Nico Rehwaldt 2015-05-21 14:55:10 +02:00 committed by Ricardo Matias
parent 58f5965335
commit 084d831d9e
8 changed files with 271 additions and 88 deletions

View File

@ -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);
}

View File

@ -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.targetFlow = parent;
context.parent = parent.parent;
}
}, true);
this.postExecute('shape.create', function(context) {
var shape = context.shape,
targetFlow = context.targetFlow,
position = context.position,
source,
target,
reconnected,
intersection,
waypoints,
waypointsBefore,
waypointsAfter,
dockingPoint;
if (targetFlow) {
waypoints = targetFlow.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 = targetFlow.source;
target = targetFlow.target;
if (bpmnRules.canConnect(source, shape, targetFlow)) {
// reconnect source -> inserted shape
modeling.reconnectEnd(targetFlow, shape, waypointsBefore || copy(position));
reconnected = true;
}
if (bpmnRules.canConnect(shape, target, targetFlow)) {
if (!reconnected) {
// reconnect inserted shape -> end
modeling.reconnectStart(targetFlow, shape, waypointsAfter || copy(position));
} else {
modeling.connect(shape, target, { type: targetFlow.type, waypoints: waypointsAfter });
}
}
}
}, true);
}
inherits(CreateOnFlowBehavior, CommandInterceptor);
CreateOnFlowBehavior.$inject = [ 'eventBus', 'bpmnRules', 'modeling' ];
module.exports = CreateOnFlowBehavior;

View File

@ -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') ]
};

View File

@ -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) {
function canInsert(shape, flow) {
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;
// 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);
}

View File

@ -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));

View File

@ -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);
}));
});

View File

@ -1,25 +1,27 @@
<?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_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0lk9mnl</bpmn:outgoing>
<bpmn:process id="Process" isExecutable="false">
<bpmn:startEvent id="StartEvent">
<bpmn:outgoing>SequenceFlow</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_195jx60">
<bpmn:incoming>SequenceFlow_0lk9mnl</bpmn:incoming>
<bpmn:task id="Task">
<bpmn:incoming>SequenceFlow</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0lk9mnl" sourceRef="StartEvent_1" targetRef="Task_195jx60" />
<bpmn:sequenceFlow id="SequenceFlow" sourceRef="StartEvent" targetRef="Task" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_195jx60_di" bpmnElement="Task_195jx60">
<dc:Bounds x="476" y="80" width="100" height="80" />
<bpmndi:BPMNShape id="Task_di" bpmnElement="Task">
<dc:Bounds x="502" y="259" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0lk9mnl_di" bpmnElement="SequenceFlow_0lk9mnl">
<bpmndi:BPMNEdge id="SequenceFlow_di" bpmnElement="SequenceFlow">
<di:waypoint xsi:type="dc:Point" x="209" y="120" />
<di:waypoint xsi:type="dc:Point" x="476" y="120" />
<di:waypoint xsi:type="dc:Point" x="340" y="120" />
<di:waypoint xsi:type="dc:Point" x="340" y="299" />
<di:waypoint xsi:type="dc:Point" x="502" y="299" />
<bpmndi:BPMNLabel>
<dc:Bounds x="297.5" y="110" width="90" height="20" />
</bpmndi:BPMNLabel>

View File

@ -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 }
]);
}));
});
});