bpmn-js/lib/features/rules/BpmnRules.js

960 lines
22 KiB
JavaScript

import {
every,
find,
forEach,
some
} from 'min-dash';
import inherits from 'inherits';
import {
is,
getBusinessObject
} from '../../util/ModelUtil';
import {
getParent,
isAny
} from '../modeling/util/ModelingUtil';
import {
isLabel
} from '../../util/LabelUtil';
import {
isExpanded,
isEventSubProcess,
isInterrupting,
hasErrorEventDefinition,
hasEscalationEventDefinition,
hasCompensateEventDefinition
} from '../../util/DiUtil';
import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';
import {
getBoundaryAttachment as isBoundaryAttachment
} from '../snapping/BpmnSnappingUtil';
/**
* BPMN specific modeling rule
*/
export default function BpmnRules(eventBus) {
RuleProvider.call(this, eventBus);
}
inherits(BpmnRules, RuleProvider);
BpmnRules.$inject = [ 'eventBus' ];
BpmnRules.prototype.init = function() {
this.addRule('connection.start', function(context) {
var source = context.source;
return canStartConnection(source);
});
this.addRule('connection.create', function(context) {
var source = context.source,
target = context.target,
hints = context.hints || {},
targetParent = hints.targetParent,
targetAttach = hints.targetAttach;
// don't allow incoming connections on
// newly created boundary events
// to boundary events
if (targetAttach) {
return false;
}
// temporarily set target parent for scoping
// checks to work
if (targetParent) {
target.parent = targetParent;
}
try {
return canConnect(source, target);
} finally {
// unset temporary target parent
if (targetParent) {
target.parent = null;
}
}
});
this.addRule('connection.reconnect', function(context) {
var connection = context.connection,
source = context.source,
target = context.target;
return canConnect(source, target, connection);
});
this.addRule('connection.updateWaypoints', function(context) {
return {
type: context.connection.type
};
});
this.addRule('shape.resize', function(context) {
var shape = context.shape,
newBounds = context.newBounds;
return canResize(shape, newBounds);
});
this.addRule('elements.create', function(context) {
var elements = context.elements,
position = context.position,
target = context.target;
if (isConnection(target) && !canInsert(elements, target, position)) {
return false;
}
return every(elements, function(element) {
if (isConnection(element)) {
return canConnect(element.source, element.target, element);
}
if (element.host) {
return canAttach(element, element.host, null, position);
}
return canCreate(element, target, null, position);
});
});
this.addRule('elements.move', function(context) {
var target = context.target,
shapes = context.shapes,
position = context.position;
return canAttach(shapes, target, null, position) ||
canReplace(shapes, target, position) ||
canMove(shapes, target, position) ||
canInsert(shapes, target, position);
});
this.addRule('shape.create', function(context) {
return canCreate(
context.shape,
context.target,
context.source,
context.position
);
});
this.addRule('shape.attach', function(context) {
return canAttach(
context.shape,
context.target,
null,
context.position
);
});
this.addRule('element.copy', function(context) {
var element = context.element,
elements = context.elements;
return canCopy(elements, element);
});
};
BpmnRules.prototype.canConnectMessageFlow = canConnectMessageFlow;
BpmnRules.prototype.canConnectSequenceFlow = canConnectSequenceFlow;
BpmnRules.prototype.canConnectDataAssociation = canConnectDataAssociation;
BpmnRules.prototype.canConnectAssociation = canConnectAssociation;
BpmnRules.prototype.canMove = canMove;
BpmnRules.prototype.canAttach = canAttach;
BpmnRules.prototype.canReplace = canReplace;
BpmnRules.prototype.canDrop = canDrop;
BpmnRules.prototype.canInsert = canInsert;
BpmnRules.prototype.canCreate = canCreate;
BpmnRules.prototype.canConnect = canConnect;
BpmnRules.prototype.canResize = canResize;
BpmnRules.prototype.canCopy = canCopy;
/**
* Utility functions for rule checking
*/
/**
* Checks if given element can be used for starting connection.
*
* @param {Element} source
* @return {boolean}
*/
function canStartConnection(element) {
if (nonExistingOrLabel(element)) {
return null;
}
return isAny(element, [
'bpmn:FlowNode',
'bpmn:InteractionNode',
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
'bpmn:Group',
'bpmn:TextAnnotation'
]);
}
function nonExistingOrLabel(element) {
return !element || isLabel(element);
}
function isSame(a, b) {
return a === b;
}
function getOrganizationalParent(element) {
do {
if (is(element, 'bpmn:Process')) {
return getBusinessObject(element);
}
if (is(element, 'bpmn:Participant')) {
return (
getBusinessObject(element).processRef ||
getBusinessObject(element)
);
}
} while ((element = element.parent));
}
function isTextAnnotation(element) {
return is(element, 'bpmn:TextAnnotation');
}
function isGroup(element) {
return is(element, 'bpmn:Group') && !element.labelTarget;
}
function isCompensationBoundary(element) {
return is(element, 'bpmn:BoundaryEvent') &&
hasEventDefinition(element, 'bpmn:CompensateEventDefinition');
}
function isForCompensation(e) {
return getBusinessObject(e).isForCompensation;
}
function isSameOrganization(a, b) {
var parentA = getOrganizationalParent(a),
parentB = getOrganizationalParent(b);
return parentA === parentB;
}
function isMessageFlowSource(element) {
return (
is(element, 'bpmn:InteractionNode') &&
!is(element, 'bpmn:BoundaryEvent') && (
!is(element, 'bpmn:Event') || (
is(element, 'bpmn:ThrowEvent') &&
hasEventDefinitionOrNone(element, 'bpmn:MessageEventDefinition')
)
)
);
}
function isMessageFlowTarget(element) {
return (
is(element, 'bpmn:InteractionNode') &&
!isForCompensation(element) && (
!is(element, 'bpmn:Event') || (
is(element, 'bpmn:CatchEvent') &&
hasEventDefinitionOrNone(element, 'bpmn:MessageEventDefinition')
)
) && !(
is(element, 'bpmn:BoundaryEvent') &&
!hasEventDefinition(element, 'bpmn:MessageEventDefinition')
)
);
}
function getScopeParent(element) {
var parent = element;
while ((parent = parent.parent)) {
if (is(parent, 'bpmn:FlowElementsContainer')) {
return getBusinessObject(parent);
}
if (is(parent, 'bpmn:Participant')) {
return getBusinessObject(parent).processRef;
}
}
return null;
}
function isSameScope(a, b) {
var scopeParentA = getScopeParent(a),
scopeParentB = getScopeParent(b);
return scopeParentA === scopeParentB;
}
function hasEventDefinition(element, eventDefinition) {
var bo = getBusinessObject(element);
return !!find(bo.eventDefinitions || [], function(definition) {
return is(definition, eventDefinition);
});
}
function hasEventDefinitionOrNone(element, eventDefinition) {
var bo = getBusinessObject(element);
return (bo.eventDefinitions || []).every(function(definition) {
return is(definition, eventDefinition);
});
}
function isSequenceFlowSource(element) {
return (
is(element, 'bpmn:FlowNode') &&
!is(element, 'bpmn:EndEvent') &&
!isEventSubProcess(element) &&
!(is(element, 'bpmn:IntermediateThrowEvent') &&
hasEventDefinition(element, 'bpmn:LinkEventDefinition')
) &&
!isCompensationBoundary(element) &&
!isForCompensation(element)
);
}
function isSequenceFlowTarget(element) {
return (
is(element, 'bpmn:FlowNode') &&
!is(element, 'bpmn:StartEvent') &&
!is(element, 'bpmn:BoundaryEvent') &&
!isEventSubProcess(element) &&
!(is(element, 'bpmn:IntermediateCatchEvent') &&
hasEventDefinition(element, 'bpmn:LinkEventDefinition')
) &&
!isForCompensation(element)
);
}
function isEventBasedTarget(element) {
return (
is(element, 'bpmn:ReceiveTask') || (
is(element, 'bpmn:IntermediateCatchEvent') && (
hasEventDefinition(element, 'bpmn:MessageEventDefinition') ||
hasEventDefinition(element, 'bpmn:TimerEventDefinition') ||
hasEventDefinition(element, 'bpmn:ConditionalEventDefinition') ||
hasEventDefinition(element, 'bpmn:SignalEventDefinition')
)
)
);
}
function isConnection(element) {
return element.waypoints;
}
function getParents(element) {
var parents = [];
while (element) {
element = element.parent;
if (element) {
parents.push(element);
}
}
return parents;
}
function isParent(possibleParent, element) {
var allParents = getParents(element);
return allParents.indexOf(possibleParent) !== -1;
}
function canConnect(source, target, connection) {
if (nonExistingOrLabel(source) || nonExistingOrLabel(target)) {
return null;
}
if (!is(connection, 'bpmn:DataAssociation')) {
if (canConnectMessageFlow(source, target)) {
return { type: 'bpmn:MessageFlow' };
}
if (canConnectSequenceFlow(source, target)) {
return { type: 'bpmn:SequenceFlow' };
}
}
var connectDataAssociation = canConnectDataAssociation(source, target);
if (connectDataAssociation) {
return connectDataAssociation;
}
if (isCompensationBoundary(source) && isForCompensation(target)) {
return {
type: 'bpmn:Association',
associationDirection: 'One'
};
}
if (canConnectAssociation(source, target)) {
return {
type: 'bpmn:Association'
};
}
return false;
}
/**
* Can an element be dropped into the target element
*
* @return {boolean}
*/
function canDrop(element, target, position) {
// can move labels and groups everywhere
if (isLabel(element) || isGroup(element)) {
return true;
}
// disallow to create elements on collapsed pools
if (is(target, 'bpmn:Participant') && !isExpanded(target)) {
return false;
}
// allow to create new participants on
// existing collaboration and process diagrams
if (is(element, 'bpmn:Participant')) {
return is(target, 'bpmn:Process') || is(target, 'bpmn:Collaboration');
}
// allow moving DataInput / DataOutput within its original container only
if (isAny(element, [ 'bpmn:DataInput', 'bpmn:DataOutput' ])) {
if (element.parent) {
return target === element.parent;
}
}
// allow creating lanes on participants and other lanes only
if (is(element, 'bpmn:Lane')) {
return is(target, 'bpmn:Participant') || is(target, 'bpmn:Lane');
}
// disallow dropping boundary events which cannot replace with intermediate event
if (is(element, 'bpmn:BoundaryEvent') && !isDroppableBoundaryEvent(element)) {
return false;
}
// drop flow elements onto flow element containers
// and participants
if (is(element, 'bpmn:FlowElement') && !is(element, 'bpmn:DataStoreReference')) {
if (is(target, 'bpmn:FlowElementsContainer')) {
return isExpanded(target);
}
return isAny(target, [ 'bpmn:Participant', 'bpmn:Lane' ]);
}
// disallow dropping data store reference if there is no process to append to
if (is(element, 'bpmn:DataStoreReference') && is(target, 'bpmn:Collaboration')) {
return some(getBusinessObject(target).get('participants'), function(participant) {
return !!participant.get('processRef');
});
}
// account for the fact that data associations are always
// rendered and moved to top (Process or Collaboration level)
//
// artifacts may be placed wherever, too
if (isAny(element, [ 'bpmn:Artifact', 'bpmn:DataAssociation', 'bpmn:DataStoreReference' ])) {
return isAny(target, [
'bpmn:Collaboration',
'bpmn:Lane',
'bpmn:Participant',
'bpmn:Process',
'bpmn:SubProcess' ]);
}
if (is(element, 'bpmn:MessageFlow')) {
return is(target, 'bpmn:Collaboration')
|| element.source.parent == target
|| element.target.parent == target;
}
return false;
}
function isDroppableBoundaryEvent(event) {
return getBusinessObject(event).cancelActivity && (
hasNoEventDefinition(event) || hasCommonBoundaryIntermediateEventDefinition(event)
);
}
function isBoundaryEvent(element) {
return !isLabel(element) && is(element, 'bpmn:BoundaryEvent');
}
function isLane(element) {
return is(element, 'bpmn:Lane');
}
/**
* We treat IntermediateThrowEvents as boundary events during create,
* this must be reflected in the rules.
*/
function isBoundaryCandidate(element) {
if (isBoundaryEvent(element)) {
return true;
}
if (is(element, 'bpmn:IntermediateThrowEvent') && hasNoEventDefinition(element)) {
return true;
}
return (
is(element, 'bpmn:IntermediateCatchEvent') &&
hasCommonBoundaryIntermediateEventDefinition(element)
);
}
function hasNoEventDefinition(element) {
var bo = getBusinessObject(element);
return bo && !(bo.eventDefinitions && bo.eventDefinitions.length);
}
function hasCommonBoundaryIntermediateEventDefinition(element) {
return hasOneOfEventDefinitions(element, [
'bpmn:MessageEventDefinition',
'bpmn:TimerEventDefinition',
'bpmn:SignalEventDefinition',
'bpmn:ConditionalEventDefinition'
]);
}
function hasOneOfEventDefinitions(element, eventDefinitions) {
return eventDefinitions.some(function(definition) {
return hasEventDefinition(element, definition);
});
}
function isReceiveTaskAfterEventBasedGateway(element) {
return (
is(element, 'bpmn:ReceiveTask') &&
find(element.incoming, function(incoming) {
return is(incoming.source, 'bpmn:EventBasedGateway');
})
);
}
function canAttach(elements, target, source, position) {
if (!Array.isArray(elements)) {
elements = [ elements ];
}
// only (re-)attach one element at a time
if (elements.length !== 1) {
return false;
}
var element = elements[0];
// do not attach labels
if (isLabel(element)) {
return false;
}
// only handle boundary events
if (!isBoundaryCandidate(element)) {
return false;
}
// disallow drop on event sub processes
if (isEventSubProcess(target)) {
return false;
}
// only allow drop on non compensation activities
if (!is(target, 'bpmn:Activity') || isForCompensation(target)) {
return false;
}
// only attach to subprocess border
if (position && !isBoundaryAttachment(position, target)) {
return false;
}
// do not attach on receive tasks after event based gateways
if (isReceiveTaskAfterEventBasedGateway(target)) {
return false;
}
return 'attach';
}
/**
* Defines how to replace elements for a given target.
*
* Returns an array containing all elements which will be replaced.
*
* @example
*
* [{ id: 'IntermediateEvent_2',
* type: 'bpmn:StartEvent'
* },
* { id: 'IntermediateEvent_5',
* type: 'bpmn:EndEvent'
* }]
*
* @param {Array} elements
* @param {Object} target
*
* @return {Object} an object containing all elements which have to be replaced
*/
function canReplace(elements, target, position) {
if (!target) {
return false;
}
var canExecute = {
replacements: []
};
forEach(elements, function(element) {
if (!isEventSubProcess(target)) {
if (is(element, 'bpmn:StartEvent') &&
element.type !== 'label' &&
canDrop(element, target)) {
// replace a non-interrupting start event by a blank interrupting start event
// when the target is not an event sub process
if (!isInterrupting(element)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'bpmn:StartEvent'
});
}
// replace an error/escalation/compensate start event by a blank interrupting start event
// when the target is not an event sub process
if (hasErrorEventDefinition(element) ||
hasEscalationEventDefinition(element) ||
hasCompensateEventDefinition(element)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'bpmn:StartEvent'
});
}
// replace a typed start event by a blank interrupting start event
// when the target is a sub process but not an event sub process
if (hasOneOfEventDefinitions(element,
[
'bpmn:MessageEventDefinition',
'bpmn:TimerEventDefinition',
'bpmn:SignalEventDefinition',
'bpmn:ConditionalEventDefinition'
]) &&
is(target, 'bpmn:SubProcess')) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'bpmn:StartEvent'
});
}
}
}
if (!is(target, 'bpmn:Transaction')) {
if (hasEventDefinition(element, 'bpmn:CancelEventDefinition') &&
element.type !== 'label') {
if (is(element, 'bpmn:EndEvent') && canDrop(element, target)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'bpmn:EndEvent'
});
}
if (is(element, 'bpmn:BoundaryEvent') && canAttach(element, target, null, position)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'bpmn:BoundaryEvent'
});
}
}
}
});
return canExecute.replacements.length ? canExecute : false;
}
function canMove(elements, target) {
// do not move selection containing lanes
if (some(elements, isLane)) {
return false;
}
// allow default move check to start move operation
if (!target) {
return true;
}
return elements.every(function(element) {
return canDrop(element, target);
});
}
function canCreate(shape, target, source, position) {
if (!target) {
return false;
}
if (isLabel(shape) || isGroup(shape)) {
return true;
}
if (isSame(source, target)) {
return false;
}
// ensure we do not drop the element
// into source
if (source && isParent(source, target)) {
return false;
}
return canDrop(shape, target, position) || canInsert(shape, target, position);
}
function canResize(shape, newBounds) {
if (is(shape, 'bpmn:SubProcess')) {
return (
isExpanded(shape) && (
!newBounds || (newBounds.width >= 100 && newBounds.height >= 80)
)
);
}
if (is(shape, 'bpmn:Lane')) {
return !newBounds || (newBounds.width >= 130 && newBounds.height >= 60);
}
if (is(shape, 'bpmn:Participant')) {
return !newBounds || (newBounds.width >= 250 && newBounds.height >= 50);
}
if (isTextAnnotation(shape)) {
return true;
}
if (isGroup(shape)) {
return true;
}
return false;
}
/**
* Check, whether one side of the relationship
* is a text annotation.
*/
function isOneTextAnnotation(source, target) {
var sourceTextAnnotation = isTextAnnotation(source),
targetTextAnnotation = isTextAnnotation(target);
return (
(sourceTextAnnotation || targetTextAnnotation) &&
(sourceTextAnnotation !== targetTextAnnotation)
);
}
function canConnectAssociation(source, target) {
// do not connect connections
if (isConnection(source) || isConnection(target)) {
return false;
}
// compensation boundary events are exception
if (isCompensationBoundary(source) && isForCompensation(target)) {
return true;
}
// don't connect parent <-> child
if (isParent(target, source) || isParent(source, target)) {
return false;
}
// allow connection of associations between <!TextAnnotation> and <TextAnnotation>
if (isOneTextAnnotation(source, target)) {
return true;
}
// can connect associations where we can connect
// data associations, too (!)
return !!canConnectDataAssociation(source, target);
}
function canConnectMessageFlow(source, target) {
// during connect user might move mouse out of canvas
// https://github.com/bpmn-io/bpmn-js/issues/1033
if (getRootElement(source) && !getRootElement(target)) {
return false;
}
return (
isMessageFlowSource(source) &&
isMessageFlowTarget(target) &&
!isSameOrganization(source, target)
);
}
function canConnectSequenceFlow(source, target) {
if (
isEventBasedTarget(target) &&
target.incoming.length > 0 &&
areOutgoingEventBasedGatewayConnections(target.incoming) &&
!is(source, 'bpmn:EventBasedGateway')
) {
return false;
}
return isSequenceFlowSource(source) &&
isSequenceFlowTarget(target) &&
isSameScope(source, target) &&
!(is(source, 'bpmn:EventBasedGateway') && !isEventBasedTarget(target));
}
function canConnectDataAssociation(source, target) {
if (isAny(source, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ]) &&
isAny(target, [ 'bpmn:Activity', 'bpmn:ThrowEvent' ])) {
return { type: 'bpmn:DataInputAssociation' };
}
if (isAny(target, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ]) &&
isAny(source, [ 'bpmn:Activity', 'bpmn:CatchEvent' ])) {
return { type: 'bpmn:DataOutputAssociation' };
}
return false;
}
function canInsert(shape, flow, position) {
if (!flow) {
return false;
}
if (Array.isArray(shape)) {
if (shape.length !== 1) {
return false;
}
shape = shape[0];
}
if (flow.source === shape ||
flow.target === shape) {
return false;
}
// return true if we can drop on the
// underlying flow parent
//
// at this point we are not really able to talk
// about connection rules (yet)
return (
isAny(flow, [ 'bpmn:SequenceFlow', 'bpmn:MessageFlow' ]) &&
!isLabel(flow) &&
is(shape, 'bpmn:FlowNode') &&
!is(shape, 'bpmn:BoundaryEvent') &&
canDrop(shape, flow.parent, position));
}
function includes(elements, element) {
return (elements && element) && elements.indexOf(element) !== -1;
}
function canCopy(elements, element) {
if (isLabel(element)) {
return true;
}
if (is(element, 'bpmn:Lane') && !includes(elements, element.parent)) {
return false;
}
return true;
}
function isOutgoingEventBasedGatewayConnection(connection) {
if (connection && connection.source) {
return is(connection.source, 'bpmn:EventBasedGateway');
}
}
function areOutgoingEventBasedGatewayConnections(connections) {
connections = connections || [];
return connections.some(isOutgoingEventBasedGatewayConnection);
}
function getRootElement(element) {
return getParent(element, 'bpmn:Process') || getParent(element, 'bpmn:Collaboration');
}