feat(modeling): define BPMN specific ordering

Closes #336
This commit is contained in:
Nico Rehwaldt 2015-08-19 16:27:07 +02:00
parent fbf82e83e5
commit 966e3aaa34
6 changed files with 345 additions and 0 deletions

View File

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

View File

@ -0,0 +1,91 @@
'use strict';
var inherits = require('inherits');
var OrderingProvider = require('diagram-js/lib/features/ordering/OrderingProvider');
var findIndex = require('lodash/array/findIndex');
var find = require('lodash/collection/find');
/**
* a simple ordering provider that makes sure:
*
* (1) elements are ordered by a {level} property
* (2) elements with {alwaysOnTop} are always added to the root
*/
function BpmnOrderingProvider(eventBus) {
OrderingProvider.call(this, eventBus);
var orders = [
{ type: 'label', order: { level: 7 } },
{ type: 'bpmn:SequenceFlow', order: { level: 5 } },
{ type: 'bpmn:MessageFlow', order: { level: 6, top: true } },
{ type: 'bpmn:BoundaryEvent', order: { level: 4 } },
{ type: 'bpmn:Participant', order: { level: -2 } },
{ type: 'bpmn:Lane', order: { level: -1 } }
];
function computeOrder(element) {
var entry = find(orders, function(o) {
return element.type === o.type;
});
return entry && entry.order || { level: 1 };
}
function getOrder(element) {
var order = element.order;
if (!order) {
element.order = order = computeOrder(element);
}
return order;
}
this.getOrdering = function(element, newParent) {
var elementOrder = getOrder(element);
if (elementOrder.top) {
while (newParent.parent) {
newParent = newParent.parent;
}
}
var currentIndex = newParent.children.indexOf(element);
var insertIndex = findIndex(newParent.children, function(child) {
return elementOrder.level < getOrder(child).level;
});
// if the element is already in the child list at
// a smaller index, we need to adjust the inser index.
// this takes into account that the element is being removed
// before being re-inserted
if (insertIndex !== -1) {
if (currentIndex !== -1 && currentIndex < insertIndex) {
insertIndex -= 1;
}
}
return {
index: insertIndex,
parent: newParent
};
};
}
BpmnOrderingProvider.$inject = [ 'eventBus' ];
inherits(BpmnOrderingProvider, OrderingProvider);
module.exports = BpmnOrderingProvider;

View File

@ -0,0 +1,4 @@
module.exports = {
__init__: [ 'bpmnOrderingProvider' ],
bpmnOrderingProvider: [ 'type', require('./BpmnOrderingProvider') ]
};

View File

@ -0,0 +1,52 @@
'use strict';
var Helper = require('./Helper');
/* global bootstrapModeler, inject */
var move = Helper.move,
expectZOrder = Helper.expectZOrder;
var modelingModule = require('../../../../lib/features/modeling'),
coreModule = require('../../../../lib/core');
describe('features/modeling - ordering', function() {
var diagramXML = require('./ordering.bpmn');
var testModules = [ coreModule, modelingModule ];
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should keep Task behind BoundaryEvent', inject(function() {
// when
move('Task_With_Boundary');
// then
expectZOrder('Task_With_Boundary', 'BoundaryEvent');
}));
it('should keep Task behind BoundaryEvent, moving both', inject(function() {
// when
move([ 'BoundaryEvent', 'Task_With_Boundary' ], 'Participant_StartEvent');
// then
expectZOrder('Task_With_Boundary', 'BoundaryEvent');
}));
it('should keep Participant behind MessageFlow', inject(function() {
// when
move('Participant', 'Collaboration');
// then
expectZOrder('Participant_StartEvent', 'Participant', 'MessageFlow');
}));
});

View File

@ -0,0 +1,135 @@
'use strict';
var TestHelper = require('../../../TestHelper');
var map = require('lodash/collection/map');
function move(elementIds, delta, targetId, isAttach) {
if (typeof elementIds === 'string') {
elementIds = [ elementIds ];
}
if (typeof delta !== 'object') {
isAttach = targetId;
targetId = delta;
delta = { x: 0, y: 0 };
}
if (typeof targetId !== 'string') {
isAttach = targetId;
targetId = null;
}
return TestHelper.getBpmnJS().invoke(function(elementRegistry, modeling) {
function getElement(id) {
var element = elementRegistry.get(id);
expect(element).to.exist;
return element;
}
var elements = map(elementIds, getElement),
target = targetId && getElement(targetId);
return modeling.moveElements(elements, delta, target, isAttach);
});
}
module.exports.move = move;
function getAncestors(element) {
var ancestors = [];
while (element) {
ancestors.push(element);
element = element.parent;
}
return ancestors;
}
function compareZOrder(aId, bId) {
var elementA,
elementB;
TestHelper.getBpmnJS().invoke(function(elementRegistry) {
function getElement(id) {
var element = elementRegistry.get(id);
expect(element).to.exist;
return element;
}
elementA = getElement(aId);
elementB = getElement(bId);
});
var aAncestors = getAncestors(elementA),
bAncestors = getAncestors(elementB);
var sharedRoot = aAncestors.reduce(function(result, aAncestor, aParentIndex) {
if (result) {
return result;
}
var bParentIndex = bAncestors.indexOf(aAncestor);
if (bParentIndex !== -1) {
return {
a: aAncestors[aParentIndex - 1],
b: bAncestors[bParentIndex - 1],
parent: aAncestor
};
}
});
// b contained in a
if (!sharedRoot.a) {
return -1;
}
// a contained in b
if (!sharedRoot.b) {
return 1;
}
var aIndex = sharedRoot.parent.indexOf(sharedRoot.a),
bIndex = sharedRoot.parent.indexOf(sharedRoot.b);
return Math.sign(aIndex - bIndex);
}
var forEach = require('lodash/collection/forEach');
function expectZOrder() {
var elements = Array.prototype.slice.call(arguments);
var next;
forEach(elements, function(e, idx) {
next = elements[idx];
if (next) {
expect(compareZOrder(e, next)).to.eql(-1);
}
});
return true;
}
module.exports.expectZOrder = expectZOrder;

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:bpmn2="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="_Sp0bEEZWEeW8AbPIK3dKxg" targetNamespace="http://activiti.org/bpmn" exporter="camunda modeler" exporterVersion="2.6.0" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:collaboration id="Collaboration">
<bpmn2:participant id="Participant" name="Participant" processRef="Process_Tasks" />
<bpmn2:participant id="Participant_StartEvent" name="Participant_StartEvent" processRef="Process_StartEvent" />
<bpmn2:messageFlow id="MessageFlow" name="" sourceRef="Task_With_Boundary" targetRef="Participant_StartEvent" />
</bpmn2:collaboration>
<bpmn2:process id="Process_Tasks" isExecutable="false">
<bpmn2:task id="Task_With_Boundary" />
<bpmn2:boundaryEvent id="BoundaryEvent" name="" attachedToRef="Task_With_Boundary">
<bpmn2:outgoing>SequenceFlow</bpmn2:outgoing>
</bpmn2:boundaryEvent>
<bpmn2:task id="Task">
<bpmn2:incoming>SequenceFlow</bpmn2:incoming>
</bpmn2:task>
<bpmn2:sequenceFlow id="SequenceFlow" name="" sourceRef="BoundaryEvent" targetRef="Task" />
</bpmn2:process>
<bpmn2:process id="Process_StartEvent" isExecutable="false">
<bpmn2:startEvent id="StartEvent" name="StartEvent" />
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration">
<bpmndi:BPMNShape id="_BPMNShape_Participant_2" bpmnElement="Participant" isHorizontal="true">
<dc:Bounds x="192" y="108" width="457" height="193" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Participant_3" bpmnElement="Participant_StartEvent" isHorizontal="true">
<dc:Bounds x="192" y="444" width="457" height="158" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="Task_With_Boundary">
<dc:Bounds x="300" y="144" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_BoundaryEvent_2" bpmnElement="BoundaryEvent">
<dc:Bounds x="346" y="206" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_3" bpmnElement="Task">
<dc:Bounds x="480" y="144" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow" sourceElement="_BPMNShape_BoundaryEvent_2" targetElement="_BPMNShape_Task_3">
<di:waypoint xsi:type="dc:Point" x="364" y="242" />
<di:waypoint xsi:type="dc:Point" x="364" y="272" />
<di:waypoint xsi:type="dc:Point" x="530" y="272" />
<di:waypoint xsi:type="dc:Point" x="530" y="224" />
<bpmndi:BPMNLabel>
<dc:Bounds x="471" y="272" width="6" height="6" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="BPMNEdge_MessageFlow_1" bpmnElement="MessageFlow" sourceElement="_BPMNShape_Task_2" targetElement="_BPMNShape_Participant_3">
<di:waypoint xsi:type="dc:Point" x="326" y="224" />
<di:waypoint xsi:type="dc:Point" x="326" y="444" />
<bpmndi:BPMNLabel>
<dc:Bounds x="370" y="334" width="6" height="6" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent">
<dc:Bounds x="271" y="495" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="244" y="531" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>