From a7e3980059069ebdae703d5de73784174958197d Mon Sep 17 00:00:00 2001 From: Niklas Kiefer Date: Tue, 14 May 2019 14:51:29 +0200 Subject: [PATCH] feat(modeling/behaviors): add GroupBehavior * Create new Category + Value for every new Group * Cleanup on Group deletion --- .../modeling/behavior/GroupBehavior.js | 192 ++++++++++++ lib/features/modeling/behavior/index.js | 3 + .../modeling/behavior/GroupBehaviorSpec.bpmn | 35 +++ .../modeling/behavior/GroupBehaviorSpec.js | 274 ++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 lib/features/modeling/behavior/GroupBehavior.js create mode 100644 test/spec/features/modeling/behavior/GroupBehaviorSpec.bpmn create mode 100644 test/spec/features/modeling/behavior/GroupBehaviorSpec.js diff --git a/lib/features/modeling/behavior/GroupBehavior.js b/lib/features/modeling/behavior/GroupBehavior.js new file mode 100644 index 00000000..4e5bfb5d --- /dev/null +++ b/lib/features/modeling/behavior/GroupBehavior.js @@ -0,0 +1,192 @@ +import inherits from 'inherits'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { + add as collectionAdd, + remove as collectionRemove +} from 'diagram-js/lib/util/Collections'; + +import { + getBusinessObject, + is +} from '../../../util/ModelUtil'; + + +/** + * BPMN specific Group behavior + */ +export default function GroupBehavior(eventBus, bpmnFactory, canvas, elementRegistry) { + + CommandInterceptor.call(this, eventBus); + + /** + * Gets process definitions + * + * @return {ModdleElement} definitions + */ + function getDefinitions() { + var rootElement = canvas.getRootElement(), + businessObject = getBusinessObject(rootElement); + + return businessObject.$parent; + } + + /** + * Removes a referenced category value for a given group shape + * + * @param {djs.model.Shape} shape + */ + function removeReferencedCategoryValue(shape) { + + var businessObject = getBusinessObject(shape), + categoryValue = businessObject.categoryValueRef, + category = categoryValue.$parent; + + if (!categoryValue) { + return; + } + + collectionRemove(category.categoryValue, categoryValue); + + // cleanup category if it is empty + if (category && !category.categoryValue.length) { + removeCategory(category); + } + } + + /** + * Removes a given category from the definitions + * + * @param {ModdleElement} category + */ + function removeCategory(category) { + + var definitions = getDefinitions(); + + collectionRemove(definitions.get('rootElements'), category); + } + + /** + * Returns all group element in the current registry + * + * @return {Array} a list of group shapes + */ + function getGroupElements() { + return elementRegistry.filter(function(e) { + return is(e, 'bpmn:Group'); + }); + } + + /** + * Returns true if given categoryValue is referenced in one of the given elements + * + * @param {Array} elements + * @param {ModdleElement} categoryValue + * @return {Boolean} + */ + function isReferenced(elements, categoryValue) { + return elements.some(function(e) { + + var businessObject = getBusinessObject(e); + + return businessObject.categoryValueRef + && businessObject.categoryValueRef === categoryValue; + }); + } + + /** + * remove referenced category + value when group was deleted + */ + this.executed('shape.delete', function(event) { + + var context = event.context, + shape = context.shape; + + if (is(shape, 'bpmn:Group')) { + + var businessObject = getBusinessObject(shape), + categoryValueRef = businessObject.categoryValueRef, + groupElements = getGroupElements(); + + if (!isReferenced(groupElements, categoryValueRef)) { + removeReferencedCategoryValue(shape); + } + } + }); + + /** + * re-attach removed category + */ + this.reverted('shape.delete', function(event) { + + var context = event.context, + shape = context.shape; + + if (is(shape, 'bpmn:Group')) { + + var businessObject = getBusinessObject(shape), + categoryValueRef = businessObject.categoryValueRef, + definitions = getDefinitions(), + category = categoryValueRef ? categoryValueRef.$parent : null; + + collectionAdd(category.get('categoryValue'), categoryValueRef); + collectionAdd(definitions.get('rootElements'), category); + } + }); + + /** + * create new category + value when group was created + */ + this.execute('shape.create', function(event) { + + var context = event.context, + shape = context.shape, + businessObject = getBusinessObject(shape); + + if (is(businessObject, 'bpmn:Group') && !businessObject.categoryValueRef) { + + var definitions = getDefinitions(); + + var categoryValue = bpmnFactory.create('bpmn:CategoryValue'), + category = bpmnFactory.create('bpmn:Category', { + categoryValue: [ categoryValue ] + }); + + // add to correct place + collectionAdd(definitions.get('rootElements'), category); + getBusinessObject(category).$parent = definitions; + getBusinessObject(categoryValue).$parent = category; + + // link the reference to the Group + businessObject.categoryValueRef = categoryValue; + + } + + }); + + + this.revert('shape.create', function(event) { + + var context = event.context, + shape = context.shape; + + if (is(shape, 'bpmn:Group')) { + + removeReferencedCategoryValue(shape); + + delete getBusinessObject(shape).categoryValueRef; + + } + }); + +} + +GroupBehavior.$inject = [ + 'eventBus', + 'bpmnFactory', + 'canvas', + 'elementRegistry' +]; + +inherits(GroupBehavior, CommandInterceptor); \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 6f31a5a4..2057c340 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -10,6 +10,7 @@ import DataStoreBehavior from './DataStoreBehavior'; import DeleteLaneBehavior from './DeleteLaneBehavior'; import DropOnFlowBehavior from './DropOnFlowBehavior'; import EventBasedGatewayBehavior from './EventBasedGatewayBehavior'; +import GroupBehavior from './GroupBehavior'; import ImportDockingFix from './ImportDockingFix'; import IsHorizontalFix from './IsHorizontalFix'; import LabelBehavior from './LabelBehavior'; @@ -39,6 +40,7 @@ export default { 'deleteLaneBehavior', 'dropOnFlowBehavior', 'eventBasedGatewayBehavior', + 'groupBehavior', 'importDockingFix', 'isHorizontalFix', 'labelBehavior', @@ -66,6 +68,7 @@ export default { deleteLaneBehavior: [ 'type', DeleteLaneBehavior ], dropOnFlowBehavior: [ 'type', DropOnFlowBehavior ], eventBasedGatewayBehavior: [ 'type', EventBasedGatewayBehavior ], + groupBehavior: [ 'type', GroupBehavior ], importDockingFix: [ 'type', ImportDockingFix ], isHorizontalFix: [ 'type', IsHorizontalFix ], labelBehavior: [ 'type', LabelBehavior ], diff --git a/test/spec/features/modeling/behavior/GroupBehaviorSpec.bpmn b/test/spec/features/modeling/behavior/GroupBehaviorSpec.bpmn new file mode 100644 index 00000000..940a1d75 --- /dev/null +++ b/test/spec/features/modeling/behavior/GroupBehaviorSpec.bpmn @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/GroupBehaviorSpec.js b/test/spec/features/modeling/behavior/GroupBehaviorSpec.js new file mode 100644 index 00000000..7e3882ba --- /dev/null +++ b/test/spec/features/modeling/behavior/GroupBehaviorSpec.js @@ -0,0 +1,274 @@ +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + +import { + getBusinessObject +} from 'lib/util/ModelUtil'; + +import { + indexOf as collectionIndexOf +} from 'diagram-js/lib/util/Collections'; + +import modelingModule from 'lib/features/modeling'; +import coreModule from 'lib/core'; + + +describe('features/modeling/behavior - groups', function() { + + var testModules = [ coreModule, modelingModule ]; + + + var processDiagramXML = require('./GroupBehaviorSpec.bpmn'); + + beforeEach(bootstrapModeler(processDiagramXML, { modules: testModules.concat(modelingModule) })); + + function expectIncludedOrNot(collection, object, expected) { + var isIncluded = collectionIndexOf(collection, object) >= 0; + + expect(isIncluded).to.equal(expected); + } + + describe('creation', function() { + + describe('should create new Category for every new Group', function() { + + it('execute', inject(function(canvas, elementFactory, modeling) { + + // given + var group = elementFactory.createShape({ type: 'bpmn:Group' }), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + var groupShape = modeling.createShape(group, { x: 100, y: 100 }, root), + categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + // then + expect(categoryValueRef).to.exist; + expect(category).to.exist; + + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + true + ); + + expectIncludedOrNot( + definitions.get('rootElements'), + category, + true + ); + + })); + + + it('undo', inject(function(canvas, elementFactory, modeling, commandStack) { + + // given + var group = elementFactory.createShape({ type: 'bpmn:Group' }), + root = canvas.getRootElement(); + + // when + var groupShape = modeling.createShape(group, { x: 100, y: 100 }, root); + + commandStack.undo(); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef; + + // then + expect(categoryValueRef).not.to.exist; + + })); + + + it('redo', inject(function(canvas, elementFactory, modeling, commandStack) { + + // given + var group = elementFactory.createShape({ type: 'bpmn:Group' }), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + var groupShape = modeling.createShape(group, { x: 100, y: 100 }, root); + + commandStack.undo(); + commandStack.redo(); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + // then + expect(categoryValueRef).to.exist; + expect(categoryValueRef.$parent).to.exist; + + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + true + ); + + expectIncludedOrNot( + definitions.get('rootElements'), + category, + true + ); + + })); + + }); + + }); + + + describe('deletion', function() { + + it('should NOT remove CategoryValue if it is still referenced somewhere', inject( + function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_1'); + + // when + modeling.removeShape(groupShape); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + // then + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + true + ); + + } + )); + + + it('should NOT remove Category if it still has CategoryValues', inject( + function(canvas, elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_3'), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + modeling.removeShape(groupShape); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef; + + // then + expectIncludedOrNot( + definitions.get('rootElements'), + categoryValueRef.$parent, + true + ); + + } + )); + + + describe('should remove referenced Category + Value when Group was deleted', function() { + + it('execute', inject(function(canvas, elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_4'), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + modeling.removeShape(groupShape); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + + // then + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + false + ); + + expectIncludedOrNot( + definitions.get('rootElements'), + category, + false + ); + + })); + + + it('undo', inject(function(canvas, elementRegistry, modeling, commandStack) { + + // given + var groupShape = elementRegistry.get('Group_4'), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + modeling.removeShape(groupShape); + + commandStack.undo(); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + // then + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + true + ); + + expectIncludedOrNot( + definitions.get('rootElements'), + category, + true + ); + + })); + + + it('redo', inject(function(canvas, elementRegistry, modeling, commandStack) { + + // given + var groupShape = elementRegistry.get('Group_4'), + root = canvas.getRootElement(), + definitions = getBusinessObject(root).$parent; + + // when + modeling.removeShape(groupShape); + + commandStack.undo(); + commandStack.redo(); + + var categoryValueRef = getBusinessObject(groupShape).categoryValueRef, + category = categoryValueRef.$parent; + + + // then + expectIncludedOrNot( + category.get('categoryValue'), + categoryValueRef, + false + ); + + expectIncludedOrNot( + definitions.get('rootElements'), + category, + false + ); + + })); + + }); + + }); + +});