diff --git a/lib/base.js b/lib/base.js index c255e33e..bba399af 100644 --- a/lib/base.js +++ b/lib/base.js @@ -12,6 +12,7 @@ import TokenCountModule from './features/token-count'; import SetAnimationSpeedModule from './features/set-animation-speed'; import ExclusiveGatewaySettingsModule from './features/exclusive-gateway-settings'; +import InclusiveGatewaySettingsModule from './features/inclusive-gateway-settings'; import PreserveElementColorsModule from './features/preserve-element-colors'; import TokenSimulationPaletteModule from './features/palette'; @@ -30,6 +31,7 @@ export default { TokenCountModule, SetAnimationSpeedModule, ExclusiveGatewaySettingsModule, + InclusiveGatewaySettingsModule, PreserveElementColorsModule, TokenSimulationPaletteModule ] diff --git a/lib/features/context-pads/ContextPads.js b/lib/features/context-pads/ContextPads.js index 0f133755..145b62d6 100644 --- a/lib/features/context-pads/ContextPads.js +++ b/lib/features/context-pads/ContextPads.js @@ -17,6 +17,7 @@ import { } from 'min-dom'; import ExclusiveGatewayHandler from './handler/ExclusiveGatewayHandler'; +import InclusiveGatewayHandler from './handler/InclusiveGatewayHandler'; import PauseHandler from './handler/PauseHandler'; import TriggerHandler from './handler/TriggerHandler'; @@ -45,6 +46,7 @@ export default function ContextPads( this._handlers = []; this.registerHandler('bpmn:ExclusiveGateway', ExclusiveGatewayHandler); + this.registerHandler('bpmn:InclusiveGateway', InclusiveGatewayHandler); this.registerHandler('bpmn:Activity', PauseHandler); diff --git a/lib/features/context-pads/handler/InclusiveGatewayHandler.js b/lib/features/context-pads/handler/InclusiveGatewayHandler.js new file mode 100644 index 00000000..d9e142ef --- /dev/null +++ b/lib/features/context-pads/handler/InclusiveGatewayHandler.js @@ -0,0 +1,47 @@ +import { + ForkIcon +} from '../../../icons'; + +import { getBusinessObject } from '../../../util/ElementHelper'; +import { isSequenceFlow } from '../../../simulator/util/ModelUtil'; + +export default function InclusiveGatewayHandler(inclusiveGatewaySettings) { + this._inclusiveGatewaySettings = inclusiveGatewaySettings; +} + +InclusiveGatewayHandler.prototype.createContextPads = function(element) { + const outgoingFlows = element.outgoing.filter(isSequenceFlow); + + if (outgoingFlows.length < 2) { + return; + } + + const nonDefaultFlows = outgoingFlows.filter(outgoing => { + const flowBo = getBusinessObject(outgoing), + gatewayBo = getBusinessObject(element); + + return gatewayBo.default !== flowBo; + }); + + const html = ` +
+ ${ForkIcon()} +
+ `; + + return nonDefaultFlows.map(sequenceFlow => { + const action = () => { + this._inclusiveGatewaySettings.toggleSequenceFlow(element, sequenceFlow); + }; + + return { + action, + element: sequenceFlow, + html + }; + }); +}; + +InclusiveGatewayHandler.$inject = [ + 'inclusiveGatewaySettings' +]; \ No newline at end of file diff --git a/lib/features/element-support/ElementSupport.js b/lib/features/element-support/ElementSupport.js index a7443273..915cd47b 100644 --- a/lib/features/element-support/ElementSupport.js +++ b/lib/features/element-support/ElementSupport.js @@ -16,7 +16,6 @@ import { const UNSUPPORTED_ELEMENTS = [ - 'bpmn:InclusiveGateway', 'bpmn:ComplexGateway' ]; diff --git a/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js b/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js new file mode 100644 index 00000000..dba76a0a --- /dev/null +++ b/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js @@ -0,0 +1,139 @@ +import { + TOGGLE_MODE_EVENT +} from '../../util/EventHelper'; + + +const SELECTED_COLOR = '--token-simulation-grey-darken-30'; +const NOT_SELECTED_COLOR = '--token-simulation-grey-lighten-56'; + +import { + getBusinessObject, + is, + isSequenceFlow +} from '../../simulator/util/ModelUtil'; + + +export default function InclusiveGatewaySettings( + eventBus, elementRegistry, + elementColors, simulator, simulationStyles) { + + this._elementRegistry = elementRegistry; + this._elementColors = elementColors; + this._simulator = simulator; + this._simulationStyles = simulationStyles; + + eventBus.on(TOGGLE_MODE_EVENT, event => { + if (event.active) { + this.setDefaults(); + } else { + this.reset(); + } + }); +} + +InclusiveGatewaySettings.prototype.setDefaults = function() { + const inclusiveGateways = this._elementRegistry.filter(element => { + return is(element, 'bpmn:InclusiveGateway'); + }); + + inclusiveGateways.forEach(inclusiveGateway => { + if (inclusiveGateway.outgoing.filter(isSequenceFlow).length > 1) { + this._setGatewayDefaults(inclusiveGateway); + } + }); +}; + +InclusiveGatewaySettings.prototype.reset = function() { + const inclusiveGateways = this._elementRegistry.filter(element => { + return is(element, 'bpmn:InclusiveGateway'); + }); + + inclusiveGateways.forEach(inclusiveGateway => { + if (inclusiveGateway.outgoing.filter(isSequenceFlow).length > 1) { + this._resetGateway(inclusiveGateway); + } + }); +}; + +InclusiveGatewaySettings.prototype.toggleSequenceFlow = function(gateway, sequenceFlow) { + const activeOutgoing = this._getActiveOutgoing(gateway), + defaultFlow = getDefaultFlow(gateway); + + let newActiveOutgoing; + if (activeOutgoing.includes(sequenceFlow)) { + newActiveOutgoing = without(activeOutgoing, sequenceFlow); + } else { + newActiveOutgoing = without(activeOutgoing, defaultFlow).concat(sequenceFlow); + } + + if (!newActiveOutgoing.length && defaultFlow) { + return this._setActiveOutgoing(gateway, [ defaultFlow ]); + } + + this._setActiveOutgoing(gateway, newActiveOutgoing); +}; + +InclusiveGatewaySettings.prototype._getActiveOutgoing = function(gateway) { + const { + activeOutgoing + } = this._simulator.getConfig(gateway); + + return activeOutgoing; +}; + +InclusiveGatewaySettings.prototype._setActiveOutgoing = function(gateway, activeOutgoing) { + this._simulator.setConfig(gateway, { activeOutgoing }); + + const sequenceFlows = gateway.outgoing.filter(isSequenceFlow); + + // set colors + sequenceFlows.forEach(outgoing => { + + const style = (!activeOutgoing || activeOutgoing.includes(outgoing)) ? + SELECTED_COLOR : NOT_SELECTED_COLOR; + const stroke = this._simulationStyles.get(style); + + this._elementColors.set(outgoing, { + stroke + }); + }); +}; + +InclusiveGatewaySettings.prototype._setGatewayDefaults = function(gateway) { + const sequenceFlows = gateway.outgoing.filter(isSequenceFlow); + + const defaultFlow = getDefaultFlow(gateway); + const nonDefaultFlows = without(sequenceFlows, defaultFlow); + + this._setActiveOutgoing(gateway, nonDefaultFlows); +}; + +InclusiveGatewaySettings.prototype._resetGateway = function(gateway) { + this._setActiveOutgoing(gateway, undefined); +}; + +InclusiveGatewaySettings.$inject = [ + 'eventBus', + 'elementRegistry', + 'elementColors', + 'simulator', + 'simulationStyles' +]; + +function getDefaultFlow(gateway) { + const defaultFlow = getBusinessObject(gateway).default; + + if (!defaultFlow) { + return; + } + + return gateway.outgoing.find(flow => { + const flowBo = getBusinessObject(flow); + + return flowBo === defaultFlow; + }); +} + +function without(array, element) { + return array.filter(arrayElement => arrayElement !== element); +} diff --git a/lib/features/inclusive-gateway-settings/index.js b/lib/features/inclusive-gateway-settings/index.js new file mode 100644 index 00000000..9b338b1c --- /dev/null +++ b/lib/features/inclusive-gateway-settings/index.js @@ -0,0 +1,11 @@ +import InclusiveGatewaySettings from './InclusiveGatewaySettings'; +import ElementColorsModule from '../element-colors'; +import SimlationStylesModule from '../simulation-styles'; + +export default { + __depends__: [ + ElementColorsModule, + SimlationStylesModule + ], + inclusiveGatewaySettings: [ 'type', InclusiveGatewaySettings ] +}; \ No newline at end of file diff --git a/lib/simulator/behaviors/InclusiveGatewayBehavior.js b/lib/simulator/behaviors/InclusiveGatewayBehavior.js new file mode 100644 index 00000000..dfea7f37 --- /dev/null +++ b/lib/simulator/behaviors/InclusiveGatewayBehavior.js @@ -0,0 +1,146 @@ +import { + filterSequenceFlows, isSequenceFlow +} from '../util/ModelUtil'; + + +export default function InclusiveGatewayBehavior( + simulator, + activityBehavior) { + + this._simulator = simulator; + this._activityBehavior = activityBehavior; + + simulator.registerBehavior('bpmn:InclusiveGateway', this); +} + +InclusiveGatewayBehavior.prototype.enter = function(context) { + const { + scope, + element + } = context; + + if (this._isGuaranteedToJoin(context)) { + return this._join(context); + } + + const { + parent: parentScope + } = scope; + + const sameParentScopes = this._simulator.findScopes(scope => ( + scope.parent === parentScope && scope.element !== element)); + + // There are still some tokens to wait for. + if (this._canReachAnyScope(sameParentScopes, element)) { + return; + } + + this._join(context); +}; + +InclusiveGatewayBehavior.prototype.exit = function(context) { + + const { + element, + scope + } = context; + + // depends on UI to properly configure activeOutgoing for + // each inclusive gateway + + const outgoings = filterSequenceFlows(element.outgoing); + + if (outgoings.length === 1) { + return this._simulator.enter({ + element: outgoings[0], + scope: scope.parent + }); + } + + const { + activeOutgoing + } = this._simulator.getConfig(element); + + if (!activeOutgoing.length) { + throw new Error('no outgoing configured'); + } + + for (const outgoing of activeOutgoing) { + this._simulator.enter({ + element: outgoing, + scope: scope.parent + }); + } +}; + +/** + * Number of tokens waiting at the gateway cannot be higher than the number of incoming flows. + */ +InclusiveGatewayBehavior.prototype._isGuaranteedToJoin = function(context) { + const elementScopes = this._getElementScopes(context); + + const incomingSequenceFlows = filterSequenceFlows(context.element.incoming); + + return elementScopes.length >= incomingSequenceFlows.length; +}; + +InclusiveGatewayBehavior.prototype._join = function(context) { + const elementScopes = this._getElementScopes(context); + + for (const childScope of elementScopes) { + + if (childScope !== context.scope) { + + // complete joining child scope + this._simulator.destroyScope(childScope.complete(), context.scope); + } + } + + this._simulator.exit(context); +}; + +InclusiveGatewayBehavior.prototype._getElementScopes = function(context) { + const { + element, + parent + } = context; + + return this._simulator.findScopes({ + parent, + element + }); +}; + +InclusiveGatewayBehavior.prototype._canReachAnyScope = function(scopes, currentElement, traversed = new Set()) { + if (traversed.has(currentElement)) { + return false; + } + + if (anyScopeIsOnElement(scopes, currentElement)) { + return true; + } + + if (isSequenceFlow(currentElement)) { + return this._canReachAnyScope(scopes, currentElement.source, traversed); + } + + + const incomingFlows = filterSequenceFlows(currentElement.incoming); + + for (const flow of incomingFlows) { + if (this._canReachAnyScope(scopes, flow, traversed)) { + return true; + } + } + + return false; +}; + +InclusiveGatewayBehavior.$inject = [ + 'simulator', + 'activityBehavior' +]; + +function anyScopeIsOnElement(scopes, element) { + return scopes.some(scope => scope.element === element); +} diff --git a/lib/simulator/behaviors/index.js b/lib/simulator/behaviors/index.js index c2a30cfc..9ffd87ac 100644 --- a/lib/simulator/behaviors/index.js +++ b/lib/simulator/behaviors/index.js @@ -7,6 +7,7 @@ import IntermediateThrowEventBehavior from './IntermediateThrowEventBehavior'; import ExclusiveGatewayBehavior from './ExclusiveGatewayBehavior'; import ParallelGatewayBehavior from './ParallelGatewayBehavior'; import EventBasedGatewayBehavior from './EventBasedGatewayBehavior'; +import InclusiveGatewayBehavior from './InclusiveGatewayBehavior'; import ActivityBehavior from './ActivityBehavior'; import SubProcessBehavior from './SubProcessBehavior'; @@ -31,6 +32,7 @@ export default { 'exclusiveGatewayBehavior', 'parallelGatewayBehavior', 'eventBasedGatewayBehavior', + 'inclusiveGatewayBehavior', 'subProcessBehavior', 'sequenceFlowBehavior', 'messageFlowBehavior', @@ -44,6 +46,7 @@ export default { exclusiveGatewayBehavior: [ 'type', ExclusiveGatewayBehavior ], parallelGatewayBehavior: [ 'type', ParallelGatewayBehavior ], eventBasedGatewayBehavior: [ 'type', EventBasedGatewayBehavior ], + inclusiveGatewayBehavior: [ 'type', InclusiveGatewayBehavior ], activityBehavior: [ 'type', ActivityBehavior ], subProcessBehavior: [ 'type', SubProcessBehavior ], sequenceFlowBehavior: [ 'type', SequenceFlowBehavior ], diff --git a/test/spec/ModelerSpec.js b/test/spec/ModelerSpec.js index 06fe1cd3..72a03de3 100644 --- a/test/spec/ModelerSpec.js +++ b/test/spec/ModelerSpec.js @@ -21,8 +21,10 @@ describe('modeler extension', function() { const diagram = require('./simple.bpmn'); beforeEach(bootstrapModeler(diagram, { + keyboard: { + bindTo: document + }, additionalModules: [ - ...Modeler.prototype._modules, TokenSimulationModelerModules ] })); @@ -118,7 +120,7 @@ describe('modeler extension', function() { // then expect( elementSupport.getUnsupportedElements() - ).to.have.length(2); + ).to.have.length(1); })); }); diff --git a/test/spec/ViewerSpec.js b/test/spec/ViewerSpec.js index 80bba18c..96bd01ec 100644 --- a/test/spec/ViewerSpec.js +++ b/test/spec/ViewerSpec.js @@ -53,7 +53,7 @@ describe('viewer extension', function() { // then expect( elementSupport.getUnsupportedElements() - ).to.have.length(2); + ).to.have.length(1); })); }); diff --git a/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn new file mode 100644 index 00000000..4bf34083 --- /dev/null +++ b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn @@ -0,0 +1,60 @@ + + + + + Flow_2 + + + + Flow_3 + + + + Flow_1 + + + + Flow_2 + Flow_3 + Flow_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json new file mode 100644 index 00000000..812ae0de --- /dev/null +++ b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json @@ -0,0 +1,26 @@ +[ + "createScope:Process_1:null", + "signal:Process_1:B", + "createScope:START:B", + "signal:START:C", + "exit:START:C", + "createScope:Flow_2:B", + "destroyScope:START:C", + "enter:Flow_2:B", + "exit:Flow_2:D", + "createScope:GATE:B", + "destroyScope:Flow_2:D", + "enter:GATE:B", + "exit:GATE:E", + "createScope:Flow_1:B", + "destroyScope:GATE:E", + "enter:Flow_1:B", + "exit:Flow_1:F", + "createScope:END:B", + "destroyScope:Flow_1:F", + "enter:END:B", + "exit:END:G", + "destroyScope:END:G", + "exit:Process_1:B", + "destroyScope:Process_1:B" +] \ No newline at end of file diff --git a/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn b/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn new file mode 100644 index 00000000..2560882f --- /dev/null +++ b/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn @@ -0,0 +1,102 @@ + + + + + Flow_2 + + + Flow_1 + + + + + + + + Flow_2 + Flow_3 + Flow_4 + Flow_5 + + + Flow_3 + Flow_4 + Flow_5 + Flow_1 + + + FAKE + + + + FAKE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/simulator/Simulator.inclusive-gateway-sync.json b/test/spec/simulator/Simulator.inclusive-gateway-sync.json new file mode 100644 index 00000000..72d41b52 --- /dev/null +++ b/test/spec/simulator/Simulator.inclusive-gateway-sync.json @@ -0,0 +1,41 @@ +[ + "createScope:Process_1:null", + "signal:Process_1:B", + "createScope:START:B", + "signal:START:C", + "exit:START:C", + "createScope:Flow_2:B", + "destroyScope:START:C", + "enter:Flow_2:B", + "exit:Flow_2:D", + "createScope:F_GATE:B", + "destroyScope:Flow_2:D", + "enter:F_GATE:B", + "exit:F_GATE:E", + "createScope:Flow_3:B", + "createScope:Flow_5:B", + "destroyScope:F_GATE:E", + "enter:Flow_3:B", + "enter:Flow_5:B", + "exit:Flow_3:F", + "createScope:J_GATE:B", + "destroyScope:Flow_3:F", + "exit:Flow_5:G", + "createScope:J_GATE:B", + "destroyScope:Flow_5:G", + "enter:J_GATE:B", + "enter:J_GATE:B", + "destroyScope:J_GATE:H", + "exit:J_GATE:I", + "createScope:Flow_1:B", + "destroyScope:J_GATE:I", + "enter:Flow_1:B", + "exit:Flow_1:J", + "createScope:END:B", + "destroyScope:Flow_1:J", + "enter:END:B", + "exit:END:K", + "destroyScope:END:K", + "exit:Process_1:B", + "destroyScope:Process_1:B" +] \ No newline at end of file diff --git a/test/spec/simulator/SimulatorSpec.js b/test/spec/simulator/SimulatorSpec.js index 48db2611..6f6ad913 100644 --- a/test/spec/simulator/SimulatorSpec.js +++ b/test/spec/simulator/SimulatorSpec.js @@ -657,6 +657,37 @@ describe('simulator', function() { }); + describe('inclusive gateway', function() { + + verify('inclusive-gateway-sync', (fixture) => { + + // given + setConfig(element('F_GATE'), { + activeOutgoing: [ element('Flow_3'), element('Flow_5') ] + }); + + // when + trigger({ + element: element('START') + }); + + // then + expectTrace(fixture()); + }); + + verify('inclusive-gateway-single-token-pass-through', (fixture) => { + + // when + trigger({ + element: element('START') + }); + + // then + expectTrace(fixture()); + }); + }); + + describe('end event', function() { verify('end-event', (fixture) => {