Skip to content

Commit

Permalink
feat: add bpmn:InclusiveGateway support
Browse files Browse the repository at this point in the history
Closes #88
  • Loading branch information
barmac committed Feb 18, 2022
1 parent 1b3870a commit 075f686
Show file tree
Hide file tree
Showing 15 changed files with 615 additions and 4 deletions.
2 changes: 2 additions & 0 deletions lib/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -30,6 +31,7 @@ export default {
TokenCountModule,
SetAnimationSpeedModule,
ExclusiveGatewaySettingsModule,
InclusiveGatewaySettingsModule,
PreserveElementColorsModule,
TokenSimulationPaletteModule
]
Expand Down
2 changes: 2 additions & 0 deletions lib/features/context-pads/ContextPads.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);

Expand Down
47 changes: 47 additions & 0 deletions lib/features/context-pads/handler/InclusiveGatewayHandler.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div class="bts-context-pad" title="Set Sequence Flow">
${ForkIcon()}
</div>
`;

return nonDefaultFlows.map(sequenceFlow => {
const action = () => {
this._inclusiveGatewaySettings.toggleSequenceFlow(element, sequenceFlow);
};

return {
action,
element: sequenceFlow,
html
};
});
};

InclusiveGatewayHandler.$inject = [
'inclusiveGatewaySettings'
];
1 change: 0 additions & 1 deletion lib/features/element-support/ElementSupport.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {


const UNSUPPORTED_ELEMENTS = [
'bpmn:InclusiveGateway',
'bpmn:ComplexGateway'
];

Expand Down
139 changes: 139 additions & 0 deletions lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 11 additions & 0 deletions lib/features/inclusive-gateway-settings/index.js
Original file line number Diff line number Diff line change
@@ -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 ]
};
146 changes: 146 additions & 0 deletions lib/simulator/behaviors/InclusiveGatewayBehavior.js
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 075f686

Please sign in to comment.