feat(modeling): allow to detach Boundary Events

Closes #1045
This commit is contained in:
Maciej Barelkowski 2019-05-24 14:46:01 +02:00 committed by Nico Rehwaldt
parent ecf9118a09
commit 4d6c8586e4
7 changed files with 412 additions and 94 deletions

View File

@ -0,0 +1,75 @@
import inherits from 'inherits';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import {
getBusinessObject,
is
} from '../../../util/ModelUtil';
import { isLabel } from '../../../util/LabelUtil';
/**
* BPMN specific detach event behavior
*/
export default function DetachEventBehavior(eventBus, bpmnReplace) {
CommandInterceptor.call(this, eventBus);
/**
* replace boundary event with intermediate event when
* detaching from a shape
*/
this.preExecute('elements.move', function(context) {
var shapes = context.shapes,
host = context.newHost,
shape,
eventDefinition,
intermediateEvent,
newShape;
if (shapes.length !== 1) {
return;
}
shape = shapes[0];
if (!isLabel(shape) && !host && is(shape, 'bpmn:BoundaryEvent')) {
eventDefinition = getEventDefinition(shape);
if (eventDefinition) {
intermediateEvent = {
type: 'bpmn:IntermediateCatchEvent',
eventDefinitionType: eventDefinition.$type
};
} else {
intermediateEvent = {
type: 'bpmn:IntermediateThrowEvent'
};
}
newShape = bpmnReplace.replaceElement(shape, intermediateEvent);
context.shapes = [ newShape ];
}
}, true);
}
DetachEventBehavior.$inject = [
'eventBus',
'bpmnReplace'
];
inherits(DetachEventBehavior, CommandInterceptor);
// helper /////
function getEventDefinition(element) {
var bo = getBusinessObject(element);
return bo && bo.eventDefinitions && bo.eventDefinitions[0];
}

View File

@ -8,6 +8,7 @@ import CreateParticipantBehavior from './CreateParticipantBehavior';
import DataInputAssociationBehavior from './DataInputAssociationBehavior';
import DataStoreBehavior from './DataStoreBehavior';
import DeleteLaneBehavior from './DeleteLaneBehavior';
import DetachEventBehavior from './DetachEventBehavior';
import DropOnFlowBehavior from './DropOnFlowBehavior';
import EventBasedGatewayBehavior from './EventBasedGatewayBehavior';
import GroupBehavior from './GroupBehavior';
@ -34,10 +35,11 @@ export default {
'copyPasteBehavior',
'createBoundaryEventBehavior',
'createDataObjectBehavior',
'dataStoreBehavior',
'createParticipantBehavior',
'dataStoreBehavior',
'dataInputAssociationBehavior',
'deleteLaneBehavior',
'detachEventBehavior',
'dropOnFlowBehavior',
'eventBasedGatewayBehavior',
'groupBehavior',
@ -66,6 +68,7 @@ export default {
dataInputAssociationBehavior: [ 'type', DataInputAssociationBehavior ],
dataStoreBehavior: [ 'type', DataStoreBehavior ],
deleteLaneBehavior: [ 'type', DeleteLaneBehavior ],
detachEventBehavior: [ 'type', DetachEventBehavior ],
dropOnFlowBehavior: [ 'type', DropOnFlowBehavior ],
eventBasedGatewayBehavior: [ 'type', EventBasedGatewayBehavior ],
groupBehavior: [ 'type', GroupBehavior ],

View File

@ -479,7 +479,9 @@ function canDrop(element, target, position) {
}
if (is(element, 'bpmn:BoundaryEvent')) {
return false;
return getBusinessObject(element).cancelActivity && (
hasNoEventDefinition(element) || hasCommonBoundaryIntermediateEventDefinition(element)
);
}
// drop flow elements onto flow element containers
@ -566,6 +568,27 @@ function isBoundaryCandidate(element) {
(is(element, 'bpmn:IntermediateThrowEvent') && !element.parent);
}
function hasNoEventDefinition(element) {
var bo = getBusinessObject(element);
return bo && !(bo.eventDefinitions && bo.eventDefinitions.length);
}
function hasCommonBoundaryIntermediateEventDefinition(element) {
return hasOneOfEventDefinitions(element, [
'bpmn:MessageEventDefinition',
'bpmn:TimerEventDefinition',
'bpmn:SignalEventDefinition',
'bpmn:ConditionalEventDefinition'
]);
}
function hasOneOfEventDefinitions(element, eventDefinitions) {
return eventDefinitions.some(function(definition) {
return hasEventDefinition(element, definition);
});
}
function isReceiveTaskAfterEventBasedGateway(element) {
return (
is(element, 'bpmn:ReceiveTask') &&
@ -718,11 +741,6 @@ function canReplace(elements, target, position) {
function canMove(elements, target) {
// do not move selection containing boundary events
if (some(elements, isBoundaryEvent)) {
return false;
}
// do not move selection containing lanes
if (some(elements, isLane)) {
return false;

View File

@ -30,43 +30,14 @@ describe('features/modeling - move', function() {
}));
it('should not attach label when moving BoundaryEvent', inject(function(elementRegistry, move, dragging) {
// given
var boundaryEvent = elementRegistry.get('BoundaryEvent_1'),
subProcess = elementRegistry.get('SubProcess_1'),
label = boundaryEvent.label;
// when
move.start(canvasEvent({ x: 190, y: 355 }), boundaryEvent);
dragging.hover({
element: subProcess,
gfx: elementRegistry.getGraphics(subProcess)
});
dragging.move(canvasEvent({ x: 220, y: 240 }));
dragging.end();
// then
expect(subProcess.attachers).not.to.include(label);
expect(subProcess.attachers).to.include(boundaryEvent);
expect(boundaryEvent.host).to.eql(subProcess);
expect(label.host).not.to.exist;
}));
it('should only move label when moving BoundaryEvent and Label',
inject(function(elementRegistry, move, dragging, selection) {
it('should not attach label when moving BoundaryEvent',
inject(function(elementRegistry, move, dragging) {
// given
var boundaryEvent = elementRegistry.get('BoundaryEvent_1'),
subProcess = elementRegistry.get('SubProcess_1'),
label = boundaryEvent.label;
// when
selection.select([ boundaryEvent, label ]);
move.start(canvasEvent({ x: 190, y: 355 }), boundaryEvent);
dragging.hover({
@ -79,9 +50,6 @@ describe('features/modeling - move', function() {
// then
expect(subProcess.attachers).not.to.include(label);
expect(subProcess.attachers).to.include(boundaryEvent);
expect(boundaryEvent.host).to.eql(subProcess);
expect(label.host).not.to.exist;
})
);

View File

@ -0,0 +1,181 @@
import {
bootstrapModeler,
inject
} from 'test/TestHelper';
import modelingModule from 'lib/features/modeling';
import coreModule from 'lib/core';
describe('features/modeling/behavior - detach events', function() {
var testModules = [ coreModule, modelingModule ];
var processDiagramXML = require('test/spec/features/rules/BpmnRules.detaching.bpmn');
beforeEach(bootstrapModeler(processDiagramXML, { modules: testModules }));
describe('basics', function() {
it('should execute on detach', inject(function(canvas, elementRegistry, modeling) {
// given
var eventId = 'BoundaryEvent',
boundaryEvent = elementRegistry.get(eventId),
root = canvas.getRootElement(),
intermediateThrowEvent;
var elements = [ boundaryEvent ];
// when
modeling.moveElements(elements, { x: 0, y: 100 }, root);
// then
intermediateThrowEvent = elementRegistry.get(eventId);
expect(boundaryEvent.parent).to.not.exist;
expect(intermediateThrowEvent).to.exist;
expect(intermediateThrowEvent.type).to.equal('bpmn:IntermediateThrowEvent');
expect(intermediateThrowEvent.businessObject.attachedToRef).to.not.exist;
expect(intermediateThrowEvent.parent).to.equal(root);
}));
it('should NOT execute on move to another host', inject(function(elementRegistry, modeling) {
// given
var eventId = 'BoundaryEvent',
boundaryEvent = elementRegistry.get(eventId),
subProcess = elementRegistry.get('SubProcess_1');
var elements = [ boundaryEvent ];
// when
modeling.moveElements(elements, { x: -20, y: 0 }, subProcess, { attach: true });
// then
expect(boundaryEvent.host).to.eql(subProcess);
expect(boundaryEvent.type).to.equal('bpmn:BoundaryEvent');
expect(boundaryEvent.businessObject.attachedToRef).to.equal(subProcess.businessObject);
}));
});
describe('event definition', function() {
it('should leave event definitions empty if not present',
inject(function(canvas, elementRegistry, modeling) {
// given
var boundaryEvent = elementRegistry.get('BoundaryEvent'),
root = canvas.getRootElement(),
eventDefinitions = boundaryEvent.businessObject.eventDefinitions,
intermediateEvent, bo;
var elements = [ boundaryEvent ];
// when
modeling.moveElements(elements, { x: 0, y: 90 }, root);
// then
intermediateEvent = elementRegistry.get('BoundaryEvent');
bo = intermediateEvent.businessObject;
expect(intermediateEvent.type).to.equal('bpmn:IntermediateThrowEvent');
expect(bo.eventDefinitions).to.jsonEqual(eventDefinitions, skipId);
})
);
it('should copy event definitions', inject(function(canvas, elementRegistry, modeling) {
// given
var detachableEvents = [
'BoundaryMessageEvent',
'BoundaryTimerEvent',
'BoundarySignalEvent',
'BoundaryConditionalEvent'
];
detachableEvents.forEach(function(eventId) {
var boundaryEvent = elementRegistry.get(eventId),
root = canvas.getRootElement(),
eventDefinitions = boundaryEvent.businessObject.eventDefinitions,
intermediateEvent, bo;
var elements = [ boundaryEvent ];
// when
modeling.moveElements(elements, { x: 0, y: 90 }, root);
// then
intermediateEvent = elementRegistry.get(eventId);
bo = intermediateEvent.businessObject;
expect(intermediateEvent.type).to.equal('bpmn:IntermediateCatchEvent');
expect(bo.eventDefinitions).to.jsonEqual(eventDefinitions, skipId);
});
}));
});
describe('connections', function() {
var eventId = 'BoundaryEventWithConnections';
it('should keep outgoing connection', inject(function(canvas, elementRegistry, modeling) {
var event = elementRegistry.get(eventId),
root = canvas.getRootElement(),
task = elementRegistry.get('Task_1'),
intermediateEvent;
var elements = [ event ];
// when
modeling.moveElements(elements, { x: 0, y: 100 }, root);
// then
intermediateEvent = elementRegistry.get(eventId);
expect(intermediateEvent.outgoing).to.have.lengthOf(1);
expect(task.incoming).to.have.lengthOf(1);
}));
});
describe('labels', function() {
var eventId = 'BoundaryEventWithLabel';
it('should ignore label movement', inject(function(canvas, elementRegistry, modeling) {
var event = elementRegistry.get(eventId),
root = canvas.getRootElement(),
initialElements = elementRegistry.getAll().slice();
var elements = [ event.label ];
// when
modeling.moveElements(elements, { x: 0, y: 300 }, root);
// then
expect(elementRegistry.getAll()).to.eql(initialElements);
}));
});
});
// helper //////
function skipId(key, value) {
if (key === 'id') {
return;
}
return value;
}

View File

@ -0,0 +1,124 @@
<?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:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.1.1">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:subProcess id="SubProcess_1" name="SubProcess" />
<bpmn:boundaryEvent id="BoundaryEvent" attachedToRef="SubProcess_1" />
<bpmn:boundaryEvent id="BoundaryMessageEvent" attachedToRef="SubProcess_1">
<bpmn:messageEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryTimerEvent" attachedToRef="SubProcess_1">
<bpmn:timerEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryEscalationEvent" attachedToRef="SubProcess_1">
<bpmn:escalationEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryConditionalEvent" attachedToRef="SubProcess_1">
<bpmn:conditionalEventDefinition>
<bpmn:condition xsi:type="bpmn:tFormalExpression" />
</bpmn:conditionalEventDefinition>
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryErrorEvent" attachedToRef="SubProcess_1">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundarySignalEvent" attachedToRef="SubProcess_1">
<bpmn:signalEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryCompensationEvent" attachedToRef="SubProcess_1">
<bpmn:compensateEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryNonInterruptingMessageEvent" cancelActivity="false" attachedToRef="SubProcess_1">
<bpmn:messageEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryTimerNonInterruptingEvent" cancelActivity="false" attachedToRef="SubProcess_1">
<bpmn:timerEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryEscalationNonInterruptingEvent" cancelActivity="false" attachedToRef="SubProcess_1">
<bpmn:escalationEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryConditionalNonInterruptingEvent" cancelActivity="false" attachedToRef="SubProcess_1">
<bpmn:conditionalEventDefinition>
<bpmn:condition xsi:type="bpmn:tFormalExpression" />
</bpmn:conditionalEventDefinition>
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryCancelEvent" attachedToRef="SubProcess_1">
<bpmn:cancelEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundarySignalNonInterruptingEvent" cancelActivity="false" attachedToRef="SubProcess_1">
<bpmn:signalEventDefinition />
</bpmn:boundaryEvent>
<bpmn:boundaryEvent id="BoundaryEventWithConnections" attachedToRef="SubProcess_1">
<bpmn:outgoing>SequenceFlow_06au3yc</bpmn:outgoing>
</bpmn:boundaryEvent>
<bpmn:task id="Task_1">
<bpmn:incoming>SequenceFlow_06au3yc</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_06au3yc" sourceRef="BoundaryEventWithConnections" targetRef="Task_1" />
<bpmn:boundaryEvent id="BoundaryEventWithLabel" name="label" attachedToRef="SubProcess_1" />
</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="156" y="81" width="849" height="201" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_07t981c_di" bpmnElement="BoundaryEvent">
<dc:Bounds x="170" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_14eo0nz_di" bpmnElement="BoundaryMessageEvent">
<dc:Bounds x="218" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1caoqb9_di" bpmnElement="BoundaryTimerEvent">
<dc:Bounds x="261" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_0us9p55_di" bpmnElement="BoundaryEscalationEvent">
<dc:Bounds x="309" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_10jnkwx_di" bpmnElement="BoundaryConditionalEvent">
<dc:Bounds x="353" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_0heyf2g_di" bpmnElement="BoundaryErrorEvent">
<dc:Bounds x="399" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1a81efa_di" bpmnElement="BoundarySignalEvent">
<dc:Bounds x="452" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_08kjzds_di" bpmnElement="BoundaryCompensationEvent">
<dc:Bounds x="535" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1jl6ugf_di" bpmnElement="BoundaryNonInterruptingMessageEvent">
<dc:Bounds x="590" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_0v82qap_di" bpmnElement="BoundaryTimerNonInterruptingEvent">
<dc:Bounds x="645" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_0nnhzzw_di" bpmnElement="BoundaryEscalationNonInterruptingEvent">
<dc:Bounds x="700" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1b3ud2q_di" bpmnElement="BoundaryConditionalNonInterruptingEvent">
<dc:Bounds x="758" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1btj0hc_di" bpmnElement="BoundaryCancelEvent">
<dc:Bounds x="492" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_04ls5w4_di" bpmnElement="BoundarySignalNonInterruptingEvent">
<dc:Bounds x="813" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_06gp814_di" bpmnElement="BoundaryEventWithConnections">
<dc:Bounds x="862" y="264" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0k50qut_di" bpmnElement="Task_1">
<dc:Bounds x="1010" y="370" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_06au3yc_di" bpmnElement="SequenceFlow_06au3yc">
<di:waypoint x="880" y="300" />
<di:waypoint x="880" y="410" />
<di:waypoint x="1010" y="410" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="BoundaryEvent_111pjze_di" bpmnElement="BoundaryEventWithLabel">
<dc:Bounds x="138" y="162" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="144" y="205" width="24" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1222,57 +1222,6 @@ describe('features/modeling/rules - BpmnRules', function() {
beforeEach(bootstrapModeler(testXML, { modules: testModules }));
it('attach/move BoundaryEvent -> Process', inject(function(elementRegistry) {
// when
var boundaryEvent = elementRegistry.get('BoundaryEvent_1');
var elements = [ boundaryEvent ];
// then
expectCanMove(elements, 'Process_1', {
attach: false,
move: false
});
}));
it('attach/move BoundaryEvent -> Task', inject(function(elementRegistry) {
// when
var boundaryEvent = elementRegistry.get('BoundaryEvent_1');
var elements = [ boundaryEvent ];
// then
expectCanMove(elements, 'Task_2', {
attach: 'attach',
move: false
});
}));
it('attach/move BoundaryEvent label -> SubProcess', inject(
function(elementRegistry) {
// when
var boundaryEvent = elementRegistry.get('BoundaryEvent_1'),
label = boundaryEvent.label;
var elements = [ label ];
// then
expectCanMove(elements, 'SubProcess_1', {
attach: false,
move: true
});
}
));
it('attach/move multiple BoundaryEvents -> SubProcess_1', inject(
function(elementRegistry) {
// when
@ -1286,7 +1235,7 @@ describe('features/modeling/rules - BpmnRules', function() {
// then
expectCanMove(elements, 'SubProcess_1', {
attach: false,
move: false
move: true
});
}
));
@ -1306,7 +1255,7 @@ describe('features/modeling/rules - BpmnRules', function() {
// then
expectCanMove(elements, 'Process_1', {
attach: false,
move: false
move: true
});
}
));