From 05fea0583407bc054c685bc2c128683f198c78b5 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Wed, 15 May 2019 16:21:30 +0200 Subject: [PATCH] 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) --- .../behavior/EventBasedGatewayBehavior.js | 9 +- .../modeling/behavior/SubProcessBehavior.js | 91 ++++++++ .../ToggleElementCollapseBehaviour.js | 82 ++++--- lib/features/modeling/behavior/index.js | 3 + .../modeling/behavior/SubProcessBehavior.bpmn | 126 +++++++++++ .../behavior/SubProcessBehaviorSpec.js | 212 ++++++++++++++++++ 6 files changed, 474 insertions(+), 49 deletions(-) create mode 100644 lib/features/modeling/behavior/SubProcessBehavior.js create mode 100644 test/spec/features/modeling/behavior/SubProcessBehavior.bpmn create mode 100644 test/spec/features/modeling/behavior/SubProcessBehaviorSpec.js diff --git a/lib/features/modeling/behavior/EventBasedGatewayBehavior.js b/lib/features/modeling/behavior/EventBasedGatewayBehavior.js index c2cd4489..11262bed 100644 --- a/lib/features/modeling/behavior/EventBasedGatewayBehavior.js +++ b/lib/features/modeling/behavior/EventBasedGatewayBehavior.js @@ -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,9 +70,8 @@ EventBasedGatewayBehavior.$inject = [ inherits(EventBasedGatewayBehavior, CommandInterceptor); - -// helpers ////////////////////// +// helpers ////////// function isSequenceFlow(connection) { return is(connection, 'bpmn:SequenceFlow'); -} +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/SubProcessBehavior.js b/lib/features/modeling/behavior/SubProcessBehavior.js new file mode 100644 index 00000000..0ed27d05 --- /dev/null +++ b/lib/features/modeling/behavior/SubProcessBehavior.js @@ -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'); +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/ToggleElementCollapseBehaviour.js b/lib/features/modeling/behavior/ToggleElementCollapseBehaviour.js index b1e67a3a..49992267 100644 --- a/lib/features/modeling/behavior/ToggleElementCollapseBehaviour.js +++ b/lib/features/modeling/behavior/ToggleElementCollapseBehaviour.js @@ -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) { diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 8c68ef47..6f31a5a4 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -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 ] diff --git a/test/spec/features/modeling/behavior/SubProcessBehavior.bpmn b/test/spec/features/modeling/behavior/SubProcessBehavior.bpmn new file mode 100644 index 00000000..1ed91954 --- /dev/null +++ b/test/spec/features/modeling/behavior/SubProcessBehavior.bpmn @@ -0,0 +1,126 @@ + + + + + SequenceFlow_1 + + + + SequenceFlow_2 + + + + SequenceFlow_2 + + + SequenceFlow_3 + + + SequenceFlow_3 + SequenceFlow_4 + + + + SequenceFlow_4 + + + + SequenceFlow_5 + + + + SequenceFlow_5 + SequenceFlow_6 + + + SequenceFlow_6 + + + + SequenceFlow_1 + + + + + SequenceFlow_7 + + + SequenceFlow_7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/SubProcessBehaviorSpec.js b/test/spec/features/modeling/behavior/SubProcessBehaviorSpec.js new file mode 100644 index 00000000..dda8412e --- /dev/null +++ b/test/spec/features/modeling/behavior/SubProcessBehaviorSpec.js @@ -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); + })); + + }); + + }); + +});