feat(modeling): define behavior for non-interrupting start events

Related to #302
This commit is contained in:
pedesen 2015-08-11 11:58:30 +02:00 committed by Nico Rehwaldt
parent 4af603e5be
commit 5b0029a8fd
8 changed files with 372 additions and 31 deletions

View File

@ -0,0 +1,62 @@
'use strict';
var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var forEach = require('lodash/collection').forEach;
var isInterrupting = require('../../../util/DiUtil').isInterrupting,
isEventSubProcess = require('../../../util/DiUtil').isEventSubProcess,
is = require('../../../util/ModelUtil').is;
/**
* Defines the behavior when a start event is moved
*/
function MoveStartEventBehavior(eventBus, bpmnReplace) {
CommandInterceptor.call(this, eventBus);
/**
* Replaces non-interrupting StartEvents by blank interrupting StartEvents,
* if the target is not an event sub process.
*/
function replaceElement(element, target) {
if (!isEventSubProcess(target)) {
if (is(element, 'bpmn:StartEvent') && !isInterrupting(element) && element.type !== 'label') {
bpmnReplace.replaceElement(element, { type: 'bpmn:StartEvent' });
}
}
}
this.postExecuted([ 'shapes.move' ], function(event) {
var target = event.context.newParent;
forEach(event.context.closure.topLevel, function(topLevelElements) {
if(isEventSubProcess(topLevelElements)) {
forEach(topLevelElements.children, function(element) {
replaceElement(element, target);
});
} else {
forEach(topLevelElements, function(element) {
replaceElement(element, target);
});
}
});
});
}
MoveStartEventBehavior.$inject = [ 'eventBus', 'bpmnReplace' ];
inherits(MoveStartEventBehavior, CommandInterceptor);
module.exports = MoveStartEventBehavior;

View File

@ -6,7 +6,9 @@ module.exports = {
'createOnFlowBehavior', 'createOnFlowBehavior',
'replaceConnectionBehavior', 'replaceConnectionBehavior',
'removeBehavior', 'removeBehavior',
'modelingFeedback' 'modelingFeedback',
'moveStartEventBehavior'
], ],
appendBehavior: [ 'type', require('./AppendBehavior') ], appendBehavior: [ 'type', require('./AppendBehavior') ],
createParticipantBehavior: [ 'type', require('./CreateParticipantBehavior') ], createParticipantBehavior: [ 'type', require('./CreateParticipantBehavior') ],
@ -14,5 +16,6 @@ module.exports = {
createOnFlowBehavior: [ 'type', require('./CreateOnFlowBehavior') ], createOnFlowBehavior: [ 'type', require('./CreateOnFlowBehavior') ],
replaceConnectionBehavior: [ 'type', require('./ReplaceConnectionBehavior') ], replaceConnectionBehavior: [ 'type', require('./ReplaceConnectionBehavior') ],
removeBehavior: [ 'type', require('./RemoveBehavior') ], removeBehavior: [ 'type', require('./RemoveBehavior') ],
modelingFeedback: [ 'type', require('./ModelingFeedback') ] modelingFeedback: [ 'type', require('./ModelingFeedback') ],
moveStartEventBehavior: [ 'type', require('./MoveStartEventBehavior') ]
}; };

View File

@ -4,6 +4,7 @@ module.exports = {
require('../label-editing'), require('../label-editing'),
require('./rules'), require('./rules'),
require('./behavior'), require('./behavior'),
require('../replace'),
require('diagram-js/lib/command'), require('diagram-js/lib/command'),
require('diagram-js/lib/features/tooltips'), require('diagram-js/lib/features/tooltips'),
require('diagram-js/lib/features/label-support'), require('diagram-js/lib/features/label-support'),

View File

@ -20,6 +20,10 @@ module.exports.isExpanded = function(element) {
return true; return true;
}; };
module.exports.isInterrupting = function(element) {
return element && getBusinessObject(element).isInterrupting !== false;
};
module.exports.isEventSubProcess = function(element) { module.exports.isEventSubProcess = function(element) {
return element && !!getBusinessObject(element).triggeredByEvent; return element && !!getBusinessObject(element).triggeredByEvent;
}; };

View File

@ -53,7 +53,7 @@
<bpmn:sequenceFlow id="SequenceFlow_7" sourceRef="BoundaryEvent_2" targetRef="SubProcess_1" /> <bpmn:sequenceFlow id="SequenceFlow_7" sourceRef="BoundaryEvent_2" targetRef="SubProcess_1" />
<bpmn:sequenceFlow id="SequenceFlow_8" sourceRef="Transaction_1" targetRef="EndEvent_1" /> <bpmn:sequenceFlow id="SequenceFlow_8" sourceRef="Transaction_1" targetRef="EndEvent_1" />
<bpmn:subProcess id="EventSubProcess_1" triggeredByEvent="true"> <bpmn:subProcess id="EventSubProcess_1" triggeredByEvent="true">
<bpmn:startEvent id="StartEvent_3"> <bpmn:startEvent id="StartEvent_3" isInterrupting="false">
<bpmn:messageEventDefinition /> <bpmn:messageEventDefinition />
</bpmn:startEvent> </bpmn:startEvent>
</bpmn:subProcess> </bpmn:subProcess>

View File

@ -632,8 +632,8 @@ describe('features/popup-menu', function() {
var entriesContainer = queryPopup(popupMenu, '.djs-popup-body'); var entriesContainer = queryPopup(popupMenu, '.djs-popup-body');
// then // then
expect(queryEntry(popupMenu, 'replace-with-non-interrupting-message-start')).to.be.defined; expect(queryEntry(popupMenu, 'replace-with-non-interrupting-message-start')).to.be.null;
expect(queryEntry(popupMenu, 'replace-with-message-start')).to.be.null; expect(queryEntry(popupMenu, 'replace-with-message-start')).to.be.defined;
expect(entriesContainer.childNodes.length).to.equal(11); expect(entriesContainer.childNodes.length).to.equal(11);
})); }));

View File

@ -0,0 +1,97 @@
<?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:subProcess id="SubProcess_1" name="SubProcess_1">
<bpmn:incoming>SequenceFlow_1</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_2</bpmn:outgoing>
<bpmn:startEvent id="StartEvent_1" name="StartEvent_1">
<bpmn:outgoing>SequenceFlow_3</bpmn:outgoing>
<bpmn:messageEventDefinition />
</bpmn:startEvent>
<bpmn:task id="Task_1">
<bpmn:incoming>SequenceFlow_3</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_3" sourceRef="StartEvent_1" targetRef="Task_1" />
</bpmn:subProcess>
<bpmn:subProcess id="SubProcess_2" name="SubProcess_2" triggeredByEvent="true">
<bpmn:startEvent id="StartEvent_2" name="StartEvent_2" isInterrupting="false">
<bpmn:outgoing>SequenceFlow_4</bpmn:outgoing>
<bpmn:messageEventDefinition />
</bpmn:startEvent>
<bpmn:task id="Task_2">
<bpmn:incoming>SequenceFlow_4</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_4" sourceRef="StartEvent_2" targetRef="Task_2" />
</bpmn:subProcess>
<bpmn:task id="Task_3">
<bpmn:outgoing>SequenceFlow_1</bpmn:outgoing>
</bpmn:task>
<bpmn:task id="Task_4">
<bpmn:incoming>SequenceFlow_2</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1" sourceRef="Task_3" targetRef="SubProcess_1" />
<bpmn:sequenceFlow id="SequenceFlow_2" sourceRef="SubProcess_1" targetRef="Task_4" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="SubProcess_1_di" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="374" y="65" width="214" height="170" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_2_di" bpmnElement="SubProcess_2" isExpanded="true">
<dc:Bounds x="122" y="66" width="204" height="168" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_2_di" bpmnElement="StartEvent_2">
<dc:Bounds x="137" y="132" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="110" y="168" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="394" y="134" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="367" y="170" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_3_di" bpmnElement="Task_3">
<dc:Bounds x="685" y="175" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_4_di" bpmnElement="Task_4">
<dc:Bounds x="685" y="67" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1_di" bpmnElement="SequenceFlow_1">
<di:waypoint xsi:type="dc:Point" x="685" y="215" />
<di:waypoint xsi:type="dc:Point" x="588" y="215" />
<bpmndi:BPMNLabel>
<dc:Bounds x="406" y="251" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_2_di" bpmnElement="SequenceFlow_2">
<di:waypoint xsi:type="dc:Point" x="589" y="107" />
<di:waypoint xsi:type="dc:Point" x="685" y="107" />
<bpmndi:BPMNLabel>
<dc:Bounds x="469.5" y="251" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="460" y="112" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="209" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_4_di" bpmnElement="SequenceFlow_4">
<di:waypoint xsi:type="dc:Point" x="173" y="150" />
<di:waypoint xsi:type="dc:Point" x="209" y="150" />
<bpmndi:BPMNLabel>
<dc:Bounds x="151" y="120.5" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_3_di" bpmnElement="SequenceFlow_3">
<di:waypoint xsi:type="dc:Point" x="430" y="152" />
<di:waypoint xsi:type="dc:Point" x="460" y="152" />
<bpmndi:BPMNLabel>
<dc:Bounds x="400" y="142" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -9,20 +9,21 @@ var modelingModule = require('../../../../lib/features/modeling'),
coreModule = require('../../../../lib/core'), coreModule = require('../../../../lib/core'),
is = require('../../../../lib/util/ModelUtil').is, is = require('../../../../lib/util/ModelUtil').is,
isExpanded = require('../../../../lib/util/DiUtil').isExpanded, isExpanded = require('../../../../lib/util/DiUtil').isExpanded,
isInterrupting = require('../../../../lib/util/DiUtil').isInterrupting,
isEventSubProcess = require('../../../../lib/util/DiUtil').isEventSubProcess; isEventSubProcess = require('../../../../lib/util/DiUtil').isEventSubProcess;
describe('features/replace', function() { describe('features/replace', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
var testModules = [ coreModule, modelingModule, replaceModule ]; var testModules = [ coreModule, modelingModule, replaceModule ];
describe('should replace', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
describe('should replace', function() {
it('task', inject(function(elementRegistry, bpmnReplace) { it('task', inject(function(elementRegistry, bpmnReplace) {
// given // given
@ -186,6 +187,11 @@ describe('features/replace', function() {
describe('position and size', function() { describe('position and size', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should keep position', inject(function(elementRegistry, bpmnReplace) { it('should keep position', inject(function(elementRegistry, bpmnReplace) {
// given // given
@ -207,6 +213,11 @@ describe('features/replace', function() {
describe('selection', function() { describe('selection', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should select after replace', it('should select after replace',
inject(function(elementRegistry, selection, bpmnReplace) { inject(function(elementRegistry, selection, bpmnReplace) {
@ -228,6 +239,10 @@ describe('features/replace', function() {
describe('label', function() { describe('label', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should keep copy label', it('should keep copy label',
inject(function(elementRegistry, bpmnReplace) { inject(function(elementRegistry, bpmnReplace) {
@ -250,6 +265,11 @@ describe('features/replace', function() {
describe('undo support', function() { describe('undo support', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should undo replace', it('should undo replace',
inject(function(elementRegistry, bpmnReplace, commandStack) { inject(function(elementRegistry, bpmnReplace, commandStack) {
@ -307,6 +327,11 @@ describe('features/replace', function() {
describe('connection handling', function() { describe('connection handling', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should reconnect valid connections', it('should reconnect valid connections',
inject(function(elementRegistry, bpmnReplace) { inject(function(elementRegistry, bpmnReplace) {
@ -377,27 +402,6 @@ describe('features/replace', function() {
})); }));
it('should remove connections for event sub processes',
inject(function(elementRegistry, bpmnReplace) {
// given
var transaction = elementRegistry.get('Transaction_1');
var newElementData = {
type: 'bpmn:SubProcess',
triggeredByEvent: true
};
// when
var newElement = bpmnReplace.replaceElement(transaction, newElementData);
// then
var incoming = newElement.incoming[0],
outgoing = newElement.outgoing[0];
expect(incoming).to.be.undefined;
expect(outgoing).to.be.undefined;
}));
describe('undo support', function() { describe('undo support', function() {
it('should reconnect valid connections', it('should reconnect valid connections',
@ -566,8 +570,14 @@ describe('features/replace', function() {
}); });
describe('children handling', function() { describe('children handling', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should update bpmn containment properly', inject(function(elementRegistry, modeling, bpmnReplace) { it('should update bpmn containment properly', inject(function(elementRegistry, modeling, bpmnReplace) {
// given // given
@ -601,8 +611,14 @@ describe('features/replace', function() {
}); });
describe('sub processes', function() { describe('sub processes', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should allow morphing expanded into expanded ad hoc', it('should allow morphing expanded into expanded ad hoc',
inject(function(bpmnReplace, elementRegistry) { inject(function(bpmnReplace, elementRegistry) {
@ -678,4 +694,162 @@ describe('features/replace', function() {
}); });
describe('event sub processes', function() {
var diagramXML = require('./BpmnReplace.eventSubProcesses.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should remove connections',
inject(function(elementRegistry, bpmnReplace) {
// given
var transaction = elementRegistry.get('SubProcess_1');
var newElementData = {
type: 'bpmn:SubProcess',
triggeredByEvent: true
};
// when
var newElement = bpmnReplace.replaceElement(transaction, newElementData);
// then
var incoming = newElement.incoming[0],
outgoing = newElement.outgoing[0];
expect(incoming).to.be.undefined;
expect(outgoing).to.be.undefined;
}));
it('should replace non-interrupting start event after moving it outside event sub process',
inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var startEvent = elementRegistry.get('StartEvent_2'),
root = elementRegistry.get('Process_1');
// when
modeling.moveShapes([startEvent], { x: 0, y: 200 }, root);
var startEventAfter = elementRegistry.filter(function(element) {
return is(element, 'bpmn:StartEvent') && element.parent === root;
})[0];
// then
expect(isInterrupting(startEventAfter)).to.be.true;
}));
it('should replace non-interrupting start event after moving it to a regular sub process',
inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var startEvent = elementRegistry.get('StartEvent_2'),
subProcess = elementRegistry.get('SubProcess_1');
// when
modeling.moveShapes([startEvent], { x: 260, y: 60 }, subProcess);
var startEventAfter = elementRegistry.filter(function(element) {
return is(element, 'bpmn:StartEvent') && element.parent === subProcess;
})[0];
// then
expect(isInterrupting(startEventAfter)).to.be.true;
}));
it('should not replace non-interrupting start event after moving it to another event sub process',
inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var startEvent = elementRegistry.get('StartEvent_2'),
subProcess = elementRegistry.get('SubProcess_1');
var eventSubProcess = bpmnReplace.replaceElement(subProcess, {
type: 'bpmn:SubProcess',
triggeredByEvent: true,
isExpanded: true
});
// when
modeling.moveShapes([startEvent], { x: 260, y: 60 }, eventSubProcess);
var startEventAfter = elementRegistry.filter(function(element) {
return is(element, 'bpmn:StartEvent') && element.parent === eventSubProcess && element.type !== 'label';
})[1];
// then
expect(startEvent.id).to.equal(startEventAfter.id);
}));
it('should not replace interrupting start event after moving it outside event sub process',
inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var startEvent = elementRegistry.get('StartEvent_2'),
root = elementRegistry.get('Process_1');
var interruptingStartEvent = bpmnReplace.replaceElement(startEvent, { type: 'bpmn:StartEvent' });
// when
modeling.moveShapes([interruptingStartEvent], { x: 0, y: 200 }, root);
var startEventAfter = elementRegistry.filter(function(element) {
return is(element, 'bpmn:StartEvent') && element.parent === root;
})[0];
// then
expect(startEventAfter).to.equal(interruptingStartEvent);
}));
it('should replace non-interrupting start event when replacing parent event sub process',
inject(function(elementRegistry, bpmnReplace){
// given
var eventSubProcess = elementRegistry.get('SubProcess_2');
// when
var subProcess = bpmnReplace.replaceElement(eventSubProcess, { type: 'bpmn:SubProcess' });
// then
var replacedStartEvent = elementRegistry.filter(function (element) {
return (element.parent === subProcess && element.type !== 'label');
})[0];
expect(isInterrupting(replacedStartEvent)).to.be.true;
}));
it('should not replace non-interrupting start event when moving parent event sub process',
inject(function(elementRegistry, bpmnReplace, modeling){
// given
var eventSubProcess = elementRegistry.get('SubProcess_2'),
startEvent = elementRegistry.get('StartEvent_2');
// when
modeling.moveShapes([eventSubProcess], { x: 20, y: 30 });
// start event after moving parent
var startEventAfter = elementRegistry.filter(function (element) {
return (element.parent === eventSubProcess && element.type !== 'label');
})[0];
// then
expect(startEventAfter).to.equal(startEvent);
expect(startEventAfter.parent).to.eql(eventSubProcess);
}));
});
}); });