diff --git a/lib/features/modeling/behavior/SubProcessPlaneBehavior.js b/lib/features/modeling/behavior/SubProcessPlaneBehavior.js new file mode 100644 index 00000000..83756f1f --- /dev/null +++ b/lib/features/modeling/behavior/SubProcessPlaneBehavior.js @@ -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' +]; diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 6c99ee6f..045613d3 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -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 ] diff --git a/lib/util/DrilldownUtil.js b/lib/util/DrilldownUtil.js new file mode 100644 index 00000000..f512a646 --- /dev/null +++ b/lib/util/DrilldownUtil.js @@ -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; +} \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/SubProcessBehavior.multiple-planes.bpmn b/test/spec/features/modeling/behavior/SubProcessBehavior.multiple-planes.bpmn new file mode 100644 index 00000000..70a6dc86 --- /dev/null +++ b/test/spec/features/modeling/behavior/SubProcessBehavior.multiple-planes.bpmn @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/SubProcessBehavior.planes.bpmn b/test/spec/features/modeling/behavior/SubProcessBehavior.planes.bpmn new file mode 100644 index 00000000..25d99d99 --- /dev/null +++ b/test/spec/features/modeling/behavior/SubProcessBehavior.planes.bpmn @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/SubProcessPlaneBehaviorSpec.js b/test/spec/features/modeling/behavior/SubProcessPlaneBehaviorSpec.js new file mode 100644 index 00000000..563626c7 --- /dev/null +++ b/test/spec/features/modeling/behavior/SubProcessPlaneBehaviorSpec.js @@ -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; +}