feat(modeling): create root elements for subprocesses

closes #1536
This commit is contained in:
Martin Stamm 2021-12-15 11:45:45 +01:00 committed by Nico Rehwaldt
parent 6bbedc47d1
commit ddc10154c9
6 changed files with 544 additions and 0 deletions

View File

@ -0,0 +1,247 @@
import inherits from 'inherits';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { find } from 'min-dash';
import { isExpanded } from '../../../util/DiUtil';
import { getBusinessObject, is } from '../../../util/ModelUtil';
import { getMid } from 'diagram-js/lib/layout/LayoutUtil';
import { getBBox } from 'diagram-js/lib/util/Elements';
import { asPlaneId, planeId } from '../../../util/DrilldownUtil';
/**
* Creates diPlanes and canvas planes when collapsed subprocesses are created.
*
*
* @param {Canvas} canvas
* @param {EventBus} eventBus
* @param {Modeling} modeling
* @param {ElementFactory} elementFactory
* @param {BpmnFactory} bpmnFactory
* @param {Bpmnjs} bpmnjs
* @param {ElementRegistry} elementRegistry
*/
export default function SubProcessPlaneBehavior(
canvas, eventBus, modeling,
elementFactory, bpmnFactory, bpmnjs, elementRegistry) {
CommandInterceptor.call(this, eventBus);
this._canvas = canvas;
this._eventBus = eventBus;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._bpmnFactory = bpmnFactory;
this._bpmnjs = bpmnjs;
this._elementRegistry = elementRegistry;
var self = this;
function isCollapsedSubProcess(element) {
return is(element, 'bpmn:SubProcess') && !isExpanded(element);
}
function createRoot(context) {
var shape = context.shape,
rootElement = context.newRootElement;
var businessObject = getBusinessObject(shape);
rootElement = self._addDiagram(rootElement || businessObject);
context.newRootElement = canvas.addRootElement(rootElement);
}
function removeRoot(context) {
var shape = context.shape;
var businessObject = getBusinessObject(shape);
self._removeDiagram(businessObject);
var rootElement = context.newRootElement = elementRegistry.get(planeId(businessObject));
canvas.removeRootElement(rootElement);
}
// add plane elements for newly created sub-processes
// this ensures we can actually drill down into the element
this.executed('shape.create', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
createRoot(context);
}, true);
this.reverted('shape.create', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
removeRoot(context);
}, true);
// rename secondary elements (roots) when the primary element changes
// this ensures rootElement.id = element.id + '_plane'
this.executed('element.updateProperties', function(context) {
var shape = context.element;
if (!isCollapsedSubProcess(shape)) {
return;
}
var properties = context.properties;
var oldProperties = context.oldProperties;
var oldId = oldProperties.id,
newId = properties.id;
if (oldId === newId) {
return;
}
var planeElement = elementRegistry.get(asPlaneId(oldId));
if (!planeElement) {
return;
}
elementRegistry.updateId(planeElement, asPlaneId(newId));
}, true);
this.reverted('element.updateProperties', function(context) {
var shape = context.element;
if (!isCollapsedSubProcess(shape)) {
return;
}
var properties = context.properties;
var oldProperties = context.oldProperties;
var oldId = oldProperties.id,
newId = properties.id;
if (oldId === newId) {
return;
}
var planeElement = elementRegistry.get(asPlaneId(newId));
if (!planeElement) {
return;
}
elementRegistry.updateId(planeElement, asPlaneId(oldId));
}, true);
}
inherits(SubProcessPlaneBehavior, CommandInterceptor);
/**
* Adds a given diagram to the definitions and returns a .
*
* @param {Object} planeElement
*/
SubProcessPlaneBehavior.prototype._addDiagram = function(planeElement) {
var bpmnjs = this._bpmnjs;
var diagrams = bpmnjs.getDefinitions().diagrams;
if (!planeElement.businessObject) {
planeElement = this._createNewDiagram(planeElement);
}
diagrams.push(planeElement.di.$parent);
return planeElement;
};
/**
* Adds a given rootElement to the bpmnDi diagrams.
*
* @param {Object} rootElement
* @returns {Object} planeElement
*/
SubProcessPlaneBehavior.prototype._addDiagram = function(planeElement) {
var bpmnjs = this._bpmnjs;
var diagrams = bpmnjs.getDefinitions().diagrams;
if (!planeElement.businessObject) {
planeElement = this._createNewDiagram(planeElement);
}
diagrams.push(planeElement.di.$parent);
return planeElement;
};
/**
* Creates a new plane element for the given sub process.
*
* @param {Object} bpmnElement
*
* @return {Object} new diagram element
*/
SubProcessPlaneBehavior.prototype._createNewDiagram = function(bpmnElement) {
var bpmnFactory = this._bpmnFactory;
var elementFactory = this._elementFactory;
var diPlane = bpmnFactory.create('bpmndi:BPMNPlane', {
bpmnElement: bpmnElement
});
var diDiagram = bpmnFactory.create('bpmndi:BPMNDiagram', {
plane: diPlane
});
diPlane.$parent = diDiagram;
// add a virtual element (not being drawn),
// a copy cat of our BpmnImporter code
var planeElement = elementFactory.createRoot({
id: planeId(bpmnElement),
type: bpmnElement.$type,
di: diPlane,
businessObject: bpmnElement,
collapsed: true
});
return planeElement;
};
/**
* Removes the diagram for a given root element
*
* @param {Object} rootElement
* @returns {Object} removed bpmndi:BPMNDiagram
*/
SubProcessPlaneBehavior.prototype._removeDiagram = function(rootElement) {
var bpmnjs = this._bpmnjs;
var diagrams = bpmnjs.getDefinitions().diagrams;
var removedDiagram = find(diagrams, function(diagram) {
return diagram.plane.bpmnElement.id === rootElement.id;
});
diagrams.splice(diagrams.indexOf(removedDiagram), 1);
return removedDiagram;
};
SubProcessPlaneBehavior.$inject = [
'canvas',
'eventBus',
'modeling',
'elementFactory',
'bpmnFactory',
'bpmnjs',
'elementRegistry'
];

View File

@ -28,6 +28,7 @@ import ResizeLaneBehavior from './ResizeLaneBehavior';
import RemoveElementBehavior from './RemoveElementBehavior';
import SpaceToolBehavior from './SpaceToolBehavior';
import SubProcessStartEventBehavior from './SubProcessStartEventBehavior';
import SubProcessPlaneBehavior from './SubProcessPlaneBehavior';
import ToggleElementCollapseBehaviour from './ToggleElementCollapseBehaviour';
import UnclaimIdBehavior from './UnclaimIdBehavior';
import UpdateFlowNodeRefsBehavior from './UpdateFlowNodeRefsBehavior';
@ -66,6 +67,7 @@ export default {
'toggleElementCollapseBehaviour',
'spaceToolBehavior',
'subProcessStartEventBehavior',
'subProcessPlaneBehavior',
'unclaimIdBehavior',
'unsetDefaultFlowBehavior',
'updateFlowNodeRefsBehavior'
@ -101,6 +103,7 @@ export default {
toggleElementCollapseBehaviour : [ 'type', ToggleElementCollapseBehaviour ],
spaceToolBehavior: [ 'type', SpaceToolBehavior ],
subProcessStartEventBehavior: [ 'type', SubProcessStartEventBehavior ],
subProcessPlaneBehavior: [ 'type', SubProcessPlaneBehavior ],
unclaimIdBehavior: [ 'type', UnclaimIdBehavior ],
updateFlowNodeRefsBehavior: [ 'type', UpdateFlowNodeRefsBehavior ],
unsetDefaultFlowBehavior: [ 'type', UnsetDefaultFlowBehavior ]

29
lib/util/DrilldownUtil.js Normal file
View File

@ -0,0 +1,29 @@
import { is } from './ModelUtil';
export var planePostfix = '_plane';
/**
* Returns the ID of the plane associated with an element.
*
* @param {djs.model.Base|ModdleElement} element
* @returns {String} id of the associated plane
*/
export function planeId(element) {
if (is(element, 'bpmn:SubProcess')) {
return element.id + planePostfix;
}
return element.id;
}
/**
* Returns returns the plane ID for a given ID, as if it was associated with a
* subprocess.
*
* @param {String} shape ID
* @returns
*/
export function asPlaneId(string) {
return string + planePostfix;
}

View File

@ -0,0 +1,39 @@
<?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" id="Definitions_007va6i" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.2.0-dev">
<bpmn:process id="Process_1giw3j5" isExecutable="true">
<bpmn:task id="Task_1" />
<bpmn:subProcess id="SubProcess_1" />
<bpmn:subProcess id="SubProcess_2" name="Collapsed Nested">
<bpmn:subProcess id="Activity_0xa1lp6">
<bpmn:task id="nested_task" />
</bpmn:subProcess>
</bpmn:subProcess>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1giw3j5">
<bpmndi:BPMNShape id="Task_07xra8r_di" bpmnElement="Task_1">
<dc:Bounds x="156" y="81" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_01nq2r1_di" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="160" y="280" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1t6m7ms_di" bpmnElement="SubProcess_2">
<dc:Bounds x="310" y="81" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_1pythet">
<bpmndi:BPMNPlane id="BPMNPlane_0bbk4a7" bpmnElement="SubProcess_2">
<bpmndi:BPMNShape id="Activity_0dirtvd_di" bpmnElement="Activity_0xa1lp6">
<dc:Bounds x="150" y="120" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_0rferxm">
<bpmndi:BPMNPlane id="BPMNPlane_0nr9ud3" bpmnElement="Activity_0xa1lp6">
<bpmndi:BPMNShape id="nested_task_di" bpmnElement="nested_task">
<dc:Bounds x="140" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,17 @@
<?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" id="Definitions_007va6i" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.2.0-dev">
<bpmn:process id="Process_1giw3j5" isExecutable="true">
<bpmn:task id="Task_1" />
<bpmn:subProcess id="SubProcess_1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1giw3j5">
<bpmndi:BPMNShape id="Task_07xra8r_di" bpmnElement="Task_1">
<dc:Bounds x="156" y="81" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_01nq2r1_di" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="160" y="280" width="350" height="200" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,209 @@
import {
bootstrapModeler,
inject
} from 'test/TestHelper';
import coreModule from 'lib/core';
import modelingModule from 'lib/features/modeling';
import replaceModule from 'lib/features/replace';
import { is } from 'lib/util/ModelUtil';
describe('features/modeling/behavior - subprocess planes', function() {
var diagramXML = require('./SubProcessBehavior.planes.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: [
coreModule,
modelingModule,
replaceModule
]
}));
describe('create', function() {
it('should create new diagram for collapsed subprocess', inject(function(elementFactory, modeling, canvas, bpmnjs) {
// given
var subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
isExpanded: false
});
// when
modeling.createShape(subProcess, { x: 300, y: 300 }, canvas.getRootElement());
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(2);
expect(canvas.findRoot(planeId(subProcess))).to.exist;
}));
it('should not create new plane for expanded subprocess', inject(function(elementFactory, modeling, canvas, bpmnjs) {
// given
var subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
isExpanded: true
});
// when
modeling.createShape(subProcess, { x: 300, y: 300 }, canvas.getRootElement());
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(1);
expect(canvas.findRoot(planeId(subProcess))).to.not.exist;
}));
it('should undo', inject(function(elementFactory, modeling, commandStack, canvas, bpmnjs) {
// given
var subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
isExpanded: false
});
modeling.createShape(subProcess, { x: 300, y: 300 }, canvas.getRootElement());
// when
commandStack.undo();
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(1);
expect(canvas.findRoot(planeId(subProcess))).to.not.exist;
}));
it('should redo', inject(function(elementFactory, modeling, commandStack, canvas, bpmnjs) {
// given
var subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
isExpanded: false
});
modeling.createShape(subProcess, { x: 300, y: 300 }, canvas.getRootElement());
var plane = canvas.findRoot(planeId(subProcess));
// when
commandStack.undo();
commandStack.redo();
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(2);
expect(canvas.findRoot(planeId(subProcess))).to.exist;
expect(canvas.findRoot(planeId(subProcess))).to.equal(plane);
}));
});
describe('replace', function() {
describe('task -> collapsed subprocess', function() {
it('should add new diagram for collapsed subprocess', inject(
function(elementRegistry, bpmnReplace, bpmnjs, canvas) {
// given
var task = elementRegistry.get('Task_1'),
collapsedSubProcess;
// when
collapsedSubProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: false
});
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(2);
expect(canvas.findRoot(planeId(collapsedSubProcess))).to.exist;
}
));
it('should undo', inject(
function(elementRegistry, bpmnReplace, bpmnjs, canvas, commandStack) {
// given
var task = elementRegistry.get('Task_1'),
collapsedSubProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: false
});
// when
commandStack.undo();
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(1);
expect(canvas.findRoot(planeId(collapsedSubProcess))).to.not.exist;
}
));
it('should redo', inject(
function(elementRegistry, bpmnReplace, bpmnjs, canvas, commandStack) {
// given
var task = elementRegistry.get('Task_1'),
collapsedSubProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: false
});
// when
commandStack.undo();
commandStack.redo();
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(2);
expect(canvas.findRoot(planeId(collapsedSubProcess))).to.exist;
}
));
});
describe('task -> expanded subprocess', function() {
it('should not add new diagram for collapsed subprocess', inject(
function(elementRegistry, bpmnReplace, bpmnjs, canvas) {
// given
var task = elementRegistry.get('Task_1'),
collapsedSubProcess;
// when
collapsedSubProcess = bpmnReplace.replaceElement(task, {
type: 'bpmn:SubProcess',
isExpanded: true
});
// then
var diagrams = bpmnjs.getDefinitions().diagrams;
expect(diagrams.length).to.equal(1);
expect(canvas.findRoot(planeId(collapsedSubProcess))).to.not.exist;
}
));
});
});
});
function planeId(element) {
if (is(element, 'bpmn:SubProcess')) {
return element.id + '_plane';
}
return element.id;
}