feat(modeling): prevent expanded subprocess overlap of previous content

This covers two scenarios:

1. When a shape is replaced with an expanded subprocess
2. When a subprocess is toggled from collapsed to expanded

Only when:

1. There are incoming sequence flows (previous content)
2. There are no outgoing sequence flows (following content)
This commit is contained in:
Philipp Fromme 2019-05-15 16:21:30 +02:00
parent 62d7746e81
commit 05fea05834
6 changed files with 474 additions and 49 deletions

View File

@ -4,8 +4,8 @@ import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { is } from '../../../util/ModelUtil';
export default function EventBasedGatewayBehavior(eventBus, modeling) {
export default function EventBasedGatewayBehavior(eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
/**
@ -13,7 +13,6 @@ export default function EventBasedGatewayBehavior(eventBus, modeling) {
* from event-based gateway.
*/
this.preExecuted('connection.create', function(event) {
var source = event.context.source,
target = event.context.target,
existingIncomingConnections = target.incoming.slice();
@ -36,7 +35,6 @@ export default function EventBasedGatewayBehavior(eventBus, modeling) {
* source.
*/
this.preExecuted('shape.replace', function(event) {
var newShape = event.context.newShape,
newShapeTargets,
newShapeTargetsIncomingSequenceFlows;
@ -72,8 +70,7 @@ EventBasedGatewayBehavior.$inject = [
inherits(EventBasedGatewayBehavior, CommandInterceptor);
// helpers //////////////////////
// helpers //////////
function isSequenceFlow(connection) {
return is(connection, 'bpmn:SequenceFlow');

View File

@ -0,0 +1,91 @@
import inherits from 'inherits';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { is } from '../../../util/ModelUtil';
import { expandedBounds } from './ToggleElementCollapseBehaviour';
export default function SubProcessBehavior(elementFactory, eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
/**
* Adjust position of sub process after it replaces a shape with incoming
* sequence flows and no outgoing sequence flows to prevent overlap.
*/
this.postExecuted('shape.replace', function(event) {
var oldShape = event.context.oldShape,
newShape = event.context.newShape;
if (!is(newShape, 'bpmn:SubProcess') ||
!hasIncomingSequenceFlows(newShape) ||
hasOutgoingSequenceFlows(newShape)) {
return;
}
modeling.moveShape(newShape, {
x: oldShape.x - newShape.x,
y: 0
});
});
/**
* Adjust position of sub process with incoming sequence flows and no outgoing
* sequence flows after toggling to prevent overlap.
*/
this.postExecuted('shape.toggleCollapse', function(event) {
var context = event.context,
shape = context.shape,
defaultSize = elementFactory._getDefaultSize(shape),
newBounds;
if (!is(shape, 'bpmn:SubProcess') ||
shape.collapsed ||
!hasIncomingSequenceFlows(shape) ||
hasOutgoingSequenceFlows(shape)) {
return;
}
newBounds = expandedBounds(shape, defaultSize);
modeling.moveShape(shape, {
x: shape.x - newBounds.x,
y: 0
});
});
}
SubProcessBehavior.$inject = [
'elementFactory',
'eventBus',
'modeling'
];
inherits(SubProcessBehavior, CommandInterceptor);
// helpers //////////
function hasIncomingSequenceFlows(shape) {
shape = shape || {};
if (shape.incoming && shape.incoming.length) {
return shape.incoming.some(isSequenceFlow);
}
return false;
}
function hasOutgoingSequenceFlows(shape) {
shape = shape || {};
if (shape.outgoing && shape.outgoing.length) {
return shape.outgoing.some(isSequenceFlow);
}
return false;
}
function isSequenceFlow(connection) {
return is(connection, 'bpmn:SequenceFlow');
}

View File

@ -15,13 +15,9 @@ import {
var LOW_PRIORITY = 500;
export default function ToggleElementCollapseBehaviour(
eventBus, elementFactory, modeling,
resize) {
export default function ToggleElementCollapseBehaviour(elementFactory, eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
function hideEmptyLables(children) {
if (children.length) {
children.forEach(function(child) {
@ -32,42 +28,6 @@ export default function ToggleElementCollapseBehaviour(
}
}
function expandedBounds(shape, defaultSize) {
var children = shape.children,
newBounds = defaultSize,
visibleElements,
visibleBBox;
visibleElements = filterVisible(children).concat([ shape ]);
visibleBBox = computeChildrenBBox(visibleElements);
if (visibleBBox) {
// center to visibleBBox with max(defaultSize, childrenBounds)
newBounds.width = Math.max(visibleBBox.width, newBounds.width);
newBounds.height = Math.max(visibleBBox.height, newBounds.height);
newBounds.x = visibleBBox.x + (visibleBBox.width - newBounds.width) / 2;
newBounds.y = visibleBBox.y + (visibleBBox.height - newBounds.height) / 2;
} else {
// center to collapsed shape with defaultSize
newBounds.x = shape.x + (shape.width - newBounds.width) / 2;
newBounds.y = shape.y + (shape.height - newBounds.height) / 2;
}
return newBounds;
}
function collapsedBounds(shape, defaultSize) {
return {
x: shape.x + (shape.width - defaultSize.width) / 2,
y: shape.y + (shape.height - defaultSize.height) / 2,
width: defaultSize.width,
height: defaultSize.height
};
}
this.executed([ 'shape.toggleCollapse' ], LOW_PRIORITY, function(e) {
var context = e.context,
@ -130,13 +90,49 @@ export default function ToggleElementCollapseBehaviour(
inherits(ToggleElementCollapseBehaviour, CommandInterceptor);
ToggleElementCollapseBehaviour.$inject = [
'eventBus',
'elementFactory',
'eventBus',
'modeling'
];
// helpers //////////
// helpers //////////////////////
export function expandedBounds(shape, defaultSize) {
var children = shape.children,
newBounds = defaultSize,
visibleElements,
visibleBBox;
visibleElements = filterVisible(children).concat([ shape ]);
visibleBBox = computeChildrenBBox(visibleElements);
if (visibleBBox) {
// center to visibleBBox with max(defaultSize, childrenBounds)
newBounds.width = Math.max(visibleBBox.width, newBounds.width);
newBounds.height = Math.max(visibleBBox.height, newBounds.height);
newBounds.x = visibleBBox.x + (visibleBBox.width - newBounds.width) / 2;
newBounds.y = visibleBBox.y + (visibleBBox.height - newBounds.height) / 2;
} else {
// center to collapsed shape with defaultSize
newBounds.x = shape.x + (shape.width - newBounds.width) / 2;
newBounds.y = shape.y + (shape.height - newBounds.height) / 2;
}
return newBounds;
}
export function collapsedBounds(shape, defaultSize) {
return {
x: shape.x + (shape.width - defaultSize.width) / 2,
y: shape.y + (shape.height - defaultSize.height) / 2,
width: defaultSize.width,
height: defaultSize.height
};
}
function filterVisible(elements) {
return elements.filter(function(e) {

View File

@ -19,6 +19,7 @@ import RemoveParticipantBehavior from './RemoveParticipantBehavior';
import ReplaceElementBehaviour from './ReplaceElementBehaviour';
import ResizeLaneBehavior from './ResizeLaneBehavior';
import RemoveElementBehavior from './RemoveElementBehavior';
import SubProcessBehavior from './SubProcessBehavior';
import ToggleElementCollapseBehaviour from './ToggleElementCollapseBehaviour';
import UnclaimIdBehavior from './UnclaimIdBehavior';
import UpdateFlowNodeRefsBehavior from './UpdateFlowNodeRefsBehavior';
@ -48,6 +49,7 @@ export default {
'replaceElementBehaviour',
'resizeLaneBehavior',
'toggleElementCollapseBehaviour',
'subProcessBehavior',
'unclaimIdBehavior',
'unsetDefaultFlowBehavior',
'updateFlowNodeRefsBehavior'
@ -74,6 +76,7 @@ export default {
resizeLaneBehavior: [ 'type', ResizeLaneBehavior ],
removeElementBehavior: [ 'type', RemoveElementBehavior ],
toggleElementCollapseBehaviour : [ 'type', ToggleElementCollapseBehaviour ],
subProcessBehavior: [ 'type', SubProcessBehavior ],
unclaimIdBehavior: [ 'type', UnclaimIdBehavior ],
updateFlowNodeRefsBehavior: [ 'type', UpdateFlowNodeRefsBehavior ],
unsetDefaultFlowBehavior: [ 'type', UnsetDefaultFlowBehavior ]

View File

@ -0,0 +1,126 @@
<?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" exporter="Camunda Modeler" exporterVersion="3.1.0">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1" sourceRef="StartEvent_1" targetRef="Task_1" />
<bpmn:startEvent id="StartEvent_2">
<bpmn:outgoing>SequenceFlow_2</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_2" sourceRef="StartEvent_2" targetRef="SubProcess_1" />
<bpmn:subProcess id="SubProcess_1">
<bpmn:incoming>SequenceFlow_2</bpmn:incoming>
</bpmn:subProcess>
<bpmn:startEvent id="StartEvent_3">
<bpmn:outgoing>SequenceFlow_3</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_3">
<bpmn:incoming>SequenceFlow_3</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_4</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_3" sourceRef="StartEvent_3" targetRef="Task_3" />
<bpmn:endEvent id="EndEvent_1">
<bpmn:incoming>SequenceFlow_4</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_4" sourceRef="Task_3" targetRef="EndEvent_1" />
<bpmn:startEvent id="StartEvent_4">
<bpmn:outgoing>SequenceFlow_5</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_5" sourceRef="StartEvent_4" targetRef="SubProcess_3" />
<bpmn:subProcess id="SubProcess_3">
<bpmn:incoming>SequenceFlow_5</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_6</bpmn:outgoing>
</bpmn:subProcess>
<bpmn:endEvent id="EndEvent_2">
<bpmn:incoming>SequenceFlow_6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_6" sourceRef="SubProcess_3" targetRef="EndEvent_2" />
<bpmn:task id="Task_1">
<bpmn:incoming>SequenceFlow_1</bpmn:incoming>
</bpmn:task>
<bpmn:task id="Task_2" />
<bpmn:subProcess id="SubProcess_2" />
<bpmn:startEvent id="StartEvent_5">
<bpmn:outgoing>SequenceFlow_7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:subProcess id="SubProcess_4">
<bpmn:incoming>SequenceFlow_7</bpmn:incoming>
</bpmn:subProcess>
<bpmn:sequenceFlow id="SequenceFlow_7" sourceRef="StartEvent_5" targetRef="SubProcess_4" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="StartEvent_0clu1xt_di" bpmnElement="StartEvent_1">
<dc:Bounds x="-18" y="-18" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1sitg9h_di" bpmnElement="SequenceFlow_1">
<di:waypoint x="18" y="0" />
<di:waypoint x="50" y="0" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="StartEvent_1jvrle8_di" bpmnElement="StartEvent_2">
<dc:Bounds x="-18" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_059hdit_di" bpmnElement="SequenceFlow_2">
<di:waypoint x="18" y="200" />
<di:waypoint x="50" y="200" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="SubProcess_07paci4_di" bpmnElement="SubProcess_1" isExpanded="false">
<dc:Bounds x="50" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_1tbkscg_di" bpmnElement="StartEvent_3">
<dc:Bounds x="-18" y="382" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0y95x1n_di" bpmnElement="Task_3">
<dc:Bounds x="50" y="360" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_073fv22_di" bpmnElement="SequenceFlow_3">
<di:waypoint x="18" y="400" />
<di:waypoint x="50" y="400" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1meaw0v_di" bpmnElement="EndEvent_1">
<dc:Bounds x="182" y="382" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_16gom91_di" bpmnElement="SequenceFlow_4">
<di:waypoint x="150" y="400" />
<di:waypoint x="182" y="400" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="StartEvent_1pdfdjj_di" bpmnElement="StartEvent_4">
<dc:Bounds x="-18" y="582" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0d47csb_di" bpmnElement="SequenceFlow_5">
<di:waypoint x="18" y="600" />
<di:waypoint x="50" y="600" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="SubProcess_0457ogb_di" bpmnElement="SubProcess_3">
<dc:Bounds x="50" y="560" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1mik03m_di" bpmnElement="EndEvent_2">
<dc:Bounds x="182" y="582" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1mdz5dl_di" bpmnElement="SequenceFlow_6">
<di:waypoint x="150" y="600" />
<di:waypoint x="182" y="600" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="50" y="-40" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="350" y="-40" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_2_di" bpmnElement="SubProcess_2" isExpanded="false">
<dc:Bounds x="350" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_4_di" bpmnElement="StartEvent_5">
<dc:Bounds x="382" y="382" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_3_di" bpmnElement="SubProcess_4" isExpanded="true">
<dc:Bounds x="525" y="300" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_5_di" bpmnElement="SequenceFlow_7">
<di:waypoint x="418" y="400" />
<di:waypoint x="525" y="400" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,212 @@
/* global sinon */
import {
bootstrapModeler,
inject
} from 'test/TestHelper';
import coreModule from 'lib/core';
import modelingModule from 'lib/features/modeling';
import replaceModule from 'lib/features/replace';
import { getMid } from 'diagram-js/lib/layout/LayoutUtil';
describe('features/modeling/behavior - sub process', function() {
var diagramXML = require('./SubProcessBehavior.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: [
coreModule,
modelingModule,
replaceModule
]
}));
afterEach(sinon.restore);
describe('replace', function() {
describe('task -> expanded subprocess', function() {
describe('incoming sequence flows', function() {
it('should move', inject(function(bpmnReplace, elementRegistry) {
// given
var shape = elementRegistry.get('Task_1');
// when
var subProcess = bpmnReplace.replaceElement(shape, {
type: 'bpmn:SubProcess',
isExpanded: true
});
// then
var expectedBounds = {
x: 50,
y: -100,
width: 350,
height: 200
};
expect(subProcess).to.have.bounds(expectedBounds);
}));
});
describe('no incoming sequence flows', function() {
it('should NOT move', inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var task = elementRegistry.get('Task_2'),
taskMid = getMid(task);
// when
var subProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: true
});
// then
expect(getMid(subProcess)).to.eql(taskMid);
}));
});
describe('outgoing sequence flows', function() {
it('should NOT move', inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var task = elementRegistry.get('Task_3'),
taskMid = getMid(task);
// when
var subProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: true
});
// then
expect(getMid(subProcess)).to.eql(taskMid);
}));
});
});
describe('task -> non-subprocess', function() {
it('should NOT move', inject(function(bpmnReplace, elementRegistry, modeling) {
// given
var task = elementRegistry.get('Task_1'),
taskMid = getMid(task);
// when
var callActivity = bpmnReplace.replaceElement(task, {
type: 'bpmn:CallActivity'
});
// then
expect(getMid(callActivity)).to.eql(taskMid);
}));
});
});
describe('toggle', function() {
describe('collapsed subprocess -> expanded subprocess', function() {
describe('incoming sequence flows', function() {
it('should move', inject(function(elementRegistry, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_1');
// when
modeling.toggleCollapse(subProcess);
// then
var expectedBounds = {
x: 50,
y: 100,
width: 350,
height: 200
};
expect(subProcess).to.have.bounds(expectedBounds);
}));
});
describe('no incoming sequence flows', function() {
it('should NOT move', inject(function(elementRegistry, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_2'),
subProcessMid = getMid(subProcess);
// when
modeling.toggleCollapse(subProcess);
// then
expect(getMid(subProcess)).to.eql(subProcessMid);
}));
});
describe('outgoing sequence flows', function() {
it('should NOT move', inject(function(elementRegistry, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_3'),
subProcessMid = getMid(subProcess);
// when
modeling.toggleCollapse(subProcess);
// then
expect(getMid(subProcess)).to.eql(subProcessMid);
}));
});
});
describe('expanded sub process -> collapsed sub process', function() {
it('should NOT move', inject(function(elementRegistry, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_4'),
subProcessMid = getMid(subProcess);
// when
modeling.toggleCollapse(subProcess);
// then
expect(getMid(subProcess)).to.eql(subProcessMid);
}));
});
});
});