diff --git a/lib/features/modeling/behavior/RootElementReferenceBehavior.js b/lib/features/modeling/behavior/RootElementReferenceBehavior.js new file mode 100644 index 00000000..8d6aef0a --- /dev/null +++ b/lib/features/modeling/behavior/RootElementReferenceBehavior.js @@ -0,0 +1,165 @@ +import inherits from 'inherits'; + +import { + find, + isArray, + matchPattern, + some +} from 'min-dash'; + +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'; + +import { hasEventDefinition } from '../../../util/DiUtil'; + +var LOW_PRIORITY = 500; + + +/** + * Add referenced root elements (error, escalation, message, signal) if they don't exist. + * Copy referenced root elements on copy & paste. + */ +export default function RootElementReferenceBehavior(bpmnjs, eventBus, injector) { + injector.invoke(CommandInterceptor, this); + + function hasRootElement(rootElement) { + var definitions = bpmnjs.getDefinitions(), + rootElements = definitions.get('rootElements'); + + return !!find(rootElements, matchPattern({ id: rootElement.id })); + } + + function getRootElementReferencePropertyName(eventDefinition) { + if (is(eventDefinition, 'bpmn:ErrorEventDefinition')) { + return 'errorRef'; + } else if (is(eventDefinition, 'bpmn:EscalationEventDefinition')) { + return 'escalationRef'; + } else if (is(eventDefinition, 'bpmn:MessageEventDefinition')) { + return 'messageRef'; + } else if (is(eventDefinition, 'bpmn:SignalEventDefinition')) { + return 'signalRef'; + } + } + + function getRootElementReferenced(eventDefinition) { + return eventDefinition.get(getRootElementReferencePropertyName(eventDefinition)); + } + + // create shape + this.executed('shape.create', function(context) { + var shape = context.shape; + + if (!hasAnyEventDefinition(shape, [ + 'bpmn:ErrorEventDefinition', + 'bpmn:EscalationEventDefinition', + 'bpmn:MessageEventDefinition', + 'bpmn:SignalEventDefinition' + ])) { + return; + } + + var businessObject = getBusinessObject(shape), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ], + rootElement = getRootElementReferenced(eventDefinition), + rootElements; + + if (rootElement && !hasRootElement(rootElement)) { + rootElements = bpmnjs.getDefinitions().get('rootElements'); + + // add root element + collectionAdd(rootElements, rootElement); + + context.addedRootElement = rootElement; + } + }, true); + + this.reverted('shape.create', function(context) { + var addedRootElement = context.addedRootElement; + + if (!addedRootElement) { + return; + } + + var rootElements = bpmnjs.getDefinitions().get('rootElements'); + + // remove root element + collectionRemove(rootElements, addedRootElement); + }, true); + + eventBus.on('copyPaste.copyElement', function(context) { + var descriptor = context.descriptor, + element = context.element; + + if (!hasAnyEventDefinition(element, [ + 'bpmn:ErrorEventDefinition', + 'bpmn:EscalationEventDefinition', + 'bpmn:MessageEventDefinition', + 'bpmn:SignalEventDefinition' + ])) { + return; + } + + var businessObject = getBusinessObject(element), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ], + rootElement = getRootElementReferenced(eventDefinition); + + if (rootElement) { + descriptor.referencedRootElement = rootElement; + } + }); + + eventBus.on('copyPaste.pasteElement', LOW_PRIORITY, function(context) { + var descriptor = context.descriptor, + businessObject = descriptor.businessObject; + + if (!hasAnyEventDefinition(businessObject, [ + 'bpmn:ErrorEventDefinition', + 'bpmn:EscalationEventDefinition', + 'bpmn:MessageEventDefinition', + 'bpmn:SignalEventDefinition' + ])) { + return; + } + + var eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ], + referencedRootElement = descriptor.referencedRootElement; + + if (!referencedRootElement) { + return; + } + + eventDefinition.set(getRootElementReferencePropertyName(eventDefinition), referencedRootElement); + }); +} + +RootElementReferenceBehavior.$inject = [ + 'bpmnjs', + 'eventBus', + 'injector' +]; + +inherits(RootElementReferenceBehavior, CommandInterceptor); + +// helpers ////////// + +function hasAnyEventDefinition(element, types) { + if (!isArray(types)) { + types = [ types ]; + } + + return some(types, function(type) { + return hasEventDefinition(element, type); + }); +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 2979ef83..d26c4e19 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -3,6 +3,7 @@ import AppendBehavior from './AppendBehavior'; import AssociationBehavior from './AssociationBehavior'; import AttachEventBehavior from './AttachEventBehavior'; import BoundaryEventBehavior from './BoundaryEventBehavior'; +import RootElementReferenceBehavior from './RootElementReferenceBehavior'; import CreateBehavior from './CreateBehavior'; import FixHoverBehavior from './FixHoverBehavior'; import CreateDataObjectBehavior from './CreateDataObjectBehavior'; @@ -37,6 +38,7 @@ export default { 'associationBehavior', 'attachEventBehavior', 'boundaryEventBehavior', + 'rootElementReferenceBehavior', 'createBehavior', 'fixHoverBehavior', 'createDataObjectBehavior', @@ -69,6 +71,7 @@ export default { associationBehavior: [ 'type', AssociationBehavior ], attachEventBehavior: [ 'type', AttachEventBehavior ], boundaryEventBehavior: [ 'type', BoundaryEventBehavior ], + rootElementReferenceBehavior: [ 'type', RootElementReferenceBehavior ], createBehavior: [ 'type', CreateBehavior ], fixHoverBehavior: [ 'type', FixHoverBehavior ], createDataObjectBehavior: [ 'type', CreateDataObjectBehavior ], diff --git a/test/spec/features/modeling/behavior/RootElementReferenceBehavior.bpmn b/test/spec/features/modeling/behavior/RootElementReferenceBehavior.bpmn new file mode 100644 index 00000000..f4f8083c --- /dev/null +++ b/test/spec/features/modeling/behavior/RootElementReferenceBehavior.bpmn @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/RootElementReferenceBehaviorSpec.js b/test/spec/features/modeling/behavior/RootElementReferenceBehaviorSpec.js new file mode 100644 index 00000000..6b4e6a3b --- /dev/null +++ b/test/spec/features/modeling/behavior/RootElementReferenceBehaviorSpec.js @@ -0,0 +1,265 @@ +import { + bootstrapModeler, + getBpmnJS, + inject +} from 'test/TestHelper'; + +import coreModule from 'lib/core'; +import modelingModule from 'lib/features/modeling'; + +import { + getBusinessObject, + is +} from 'lib/util/ModelUtil'; + +import { + remove as collectionRemove +} from 'diagram-js/lib/util/Collections'; + +import { + filter, + find, + forEach, + matchPattern +} from 'min-dash'; + +var testModules = [ + coreModule, + modelingModule +]; + + +describe('features/modeling - root element reference behavior', function() { + + var diagramXML = require('./RootElementReferenceBehavior.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); + + + describe('add root element', function() { + + forEach([ + 'error', + 'escalation', + 'message', + 'signal' + ], function(type) { + + describe(type, function() { + + var id = capitalizeFirstChar(type) + 'BoundaryEvent_1'; + + var boundaryEvent, + host, + rootElement; + + describe('should add', function() { + + beforeEach(inject(function(bpmnjs, copyPaste, elementRegistry, modeling) { + + // given + boundaryEvent = elementRegistry.get(id); + + host = elementRegistry.get('Task_2'); + + var businessObject = getBusinessObject(boundaryEvent), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ]; + + rootElement = getRootElementReferenced(eventDefinition); + + // when + copyPaste.copy(boundaryEvent); + + modeling.removeShape(boundaryEvent); + + collectionRemove(bpmnjs.getDefinitions().get('rootElements'), rootElement); + + expect(hasRootElement(rootElement)).to.be.false; + + boundaryEvent = copyPaste.paste({ + element: host, + point: { + x: host.x, + y: host.y + }, + hints: { + attach: 'attach' + } + })[0]; + })); + + + it('', function() { + + // then + expect(hasRootElement(rootElement)).to.be.true; + }); + + + it('', inject(function(commandStack) { + + // when + commandStack.undo(); + + // then + expect(hasRootElement(rootElement)).to.be.false; + })); + + + it('', inject(function(commandStack) { + + // given + commandStack.undo(); + + // when + commandStack.redo(); + + // then + expect(hasRootElement(rootElement)).to.be.true; + })); + + }); + + + it('should NOT add', inject(function(bpmnFactory, bpmnjs, copyPaste, elementRegistry, moddleCopy, modeling) { + + // given + boundaryEvent = elementRegistry.get(id); + + host = elementRegistry.get('Task_2'); + + var businessObject = getBusinessObject(boundaryEvent), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ], + rootElements = bpmnjs.getDefinitions().get('rootElements'); + + rootElement = getRootElementReferenced(eventDefinition); + + copyPaste.copy(boundaryEvent); + + modeling.removeShape(boundaryEvent); + + collectionRemove(rootElements, rootElement); + + expect(hasRootElement(rootElement)).to.be.false; + + var rootElementWithSameId = bpmnFactory.create(rootElement.$type); + + moddleCopy.copyElement(rootElement, rootElementWithSameId); + + collectionRemove(rootElements, rootElementWithSameId); + + // when + boundaryEvent = copyPaste.paste({ + element: host, + point: { + x: host.x, + y: host.y + }, + hints: { + attach: 'attach' + } + })[0]; + + // then + var rootElementsOfType = filter(rootElements, matchPattern({ $type: rootElement.$type })); + + expect(rootElementsOfType).to.have.lengthOf(1); + })); + + }); + + }); + + }); + + + describe('copy root element reference', function() { + + forEach([ + 'error', + 'escalation', + 'message', + 'signal' + ], function(type) { + + describe(type, function() { + + var id = capitalizeFirstChar(type) + 'BoundaryEvent_1'; + + var boundaryEvent, + host, + rootElement; + + beforeEach(inject(function(copyPaste, elementRegistry) { + + // given + boundaryEvent = elementRegistry.get(id); + + host = elementRegistry.get('Task_2'); + + var businessObject = getBusinessObject(boundaryEvent), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ]; + + rootElement = getRootElementReferenced(eventDefinition); + + copyPaste.copy(boundaryEvent); + + // when + boundaryEvent = copyPaste.paste({ + element: host, + point: { + x: host.x, + y: host.y + }, + hints: { + attach: 'attach' + } + })[0]; + })); + + + it('should copy root element reference', function() { + + // then + var businessObject = getBusinessObject(boundaryEvent), + eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = eventDefinitions[ 0 ]; + + expect(getRootElementReferenced(eventDefinition)).to.equal(rootElement); + }); + + }); + + }); + + }); + +}); + +// helpers ////////// + +function getRootElementReferenced(eventDefinition) { + if (is(eventDefinition, 'bpmn:ErrorEventDefinition')) { + return eventDefinition.get('errorRef'); + } else if (is(eventDefinition, 'bpmn:EscalationEventDefinition')) { + return eventDefinition.get('escalationRef'); + } else if (is(eventDefinition, 'bpmn:MessageEventDefinition')) { + return eventDefinition.get('messageRef'); + } else if (is(eventDefinition, 'bpmn:SignalEventDefinition')) { + return eventDefinition.get('signalRef'); + } +} + +function hasRootElement(rootElement) { + var definitions = getBpmnJS().getDefinitions(), + rootElements = definitions.get('rootElements'); + + return !!find(rootElements, matchPattern({ id: rootElement.id })); +} + +function capitalizeFirstChar(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} \ No newline at end of file