cmmn-js
Version:
A cmmn 1.1 toolkit
1,041 lines (748 loc) • 22.3 kB
JavaScript
'use strict';
var inherits = require('inherits');
var some = require('min-dash').some,
forEach = require('min-dash').forEach,
assign = require('min-dash').assign,
filter = require('min-dash').filter;
var ModelUtil = require('../../util/ModelUtil'),
is = ModelUtil.is,
isCasePlanModel = ModelUtil.isCasePlanModel,
getDefinition = ModelUtil.getDefinition,
getBusinessObject = ModelUtil.getBusinessObject;
var isCollapsed = require('../../util/DiUtil').isCollapsed;
var ModelingUtil = require('../modeling/util/ModelingUtil'),
isAny = ModelingUtil.isAny,
isSame = ModelingUtil.isSame,
isSameCase = ModelingUtil.isSameCase,
getParent = ModelingUtil.getParent,
getParents = ModelingUtil.getParents;
var PlanItemDefinitionUtil = require('../modeling/util/PlanItemDefinitionUtil'),
isDiscretionaryToHumanTask = PlanItemDefinitionUtil.isDiscretionaryToHumanTask,
isHumanTask = PlanItemDefinitionUtil.isHumanTask,
isDiscretionaryItem = PlanItemDefinitionUtil.isDiscretionaryItem;
var isCriterionAttachment = require('../snapping/CmmnSnappingUtil').getCriterionAttachment;
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider').default;
/**
* CMMN specific modeling rule
*/
function CmmnRules(eventBus) {
RuleProvider.call(this, eventBus);
}
inherits(CmmnRules, RuleProvider);
CmmnRules.$inject = [ 'eventBus' ];
module.exports = CmmnRules;
CmmnRules.prototype.init = function() {
var self = this;
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,
allowed = false,
hints = context.hints || {},
targetParent = hints.targetParent,
targetAttach = hints.targetAttach;
// don't allow incoming connections on
// attach from context-pad
if (targetAttach) {
return false;
}
// temporarily set target parent for scoping
// checks to work
if (targetParent) {
target.parent = targetParent;
}
try {
allowed = self.canConnect(source, target) ||
self.canReplaceConnectionEnd(source, target, 'target');
if (!allowed) {
allowed = self.canConnect(target, source) ||
self.canReplaceConnectionEnd(target, source, 'source');
if (allowed) {
assign(allowed, {
reverse: true
});
}
}
} finally {
// unset temporary target parent
if (targetParent) {
target.parent = null;
}
}
return allowed;
});
this.addRule('connection.reconnectStart', function(context) {
var connection = context.connection,
hover = context.hover,
source = hover || context.source,
target = connection.target,
allowed = false;
allowed = self.canConnect(source, target, connection);
if (!allowed && hover) {
allowed = self.canReplaceConnectionEnd(hover, target, 'source');
if (!allowed) {
allowed = self.canConnect(target, hover, connection) ||
self.canReplaceConnectionEnd(target, hover, 'target');
if (allowed) {
assign(allowed, {
reverse: true
});
}
}
}
return allowed;
});
this.addRule('connection.reconnectEnd', function(context) {
var connection = context.connection,
source = connection.source,
hover = context.hover,
target = hover || context.target,
allowed = false;
allowed = self.canConnect(source, target, connection);
if (!allowed && hover) {
allowed = self.canReplaceConnectionEnd(source, hover, 'target');
if (!allowed) {
allowed = self.canConnect(hover, source, connection) ||
self.canReplaceConnectionEnd(hover, source, 'source');
if (allowed) {
assign(allowed, {
reverse: true
});
}
}
}
return allowed;
});
this.addRule('connection.updateWaypoints', function(context) {
var connection = context.connection;
return {
type: connection.type,
businessObject: connection.businessObject
};
});
this.addRule('shape.attach', function(context) {
return self.canAttach(
context.shape,
context.target,
null,
context.position
);
});
this.addRule('shape.create', function(context) {
var target = context.target,
shape = context.shape,
source = context.source,
position = context.position,
shapes = [ shape ];
return self.canCreate(shape, target, source, position) ||
self.canReplace(shapes, target, position, source);
});
this.addRule('shape.resize', function(context) {
var shape = context.shape,
newBounds = context.newBounds;
return self.canResize(shape, newBounds);
});
this.addRule('elements.move', function(context) {
var target = context.target,
shapes = context.shapes,
position = context.position;
return self.canAttach(shapes, target, null, position) ||
self.canMove(shapes, target, position) ||
self.canReplace(shapes, target, position);
});
this.addRule('shape.replace', function(context) {
var element = context.element,
host;
if (isCriterion(element)) {
host = element.host;
if (isCasePlanModel(host)) {
return false;
}
if (isTask(host) && !isBlocking(host)) {
return false;
}
if (isMilestone(host) || isEventListener(host)) {
return false;
}
}
});
this.addRule([ 'elements.delete' ], function(context) {
return self.canRemove(context.elements);
});
};
CmmnRules.prototype.canRemove = function(elements) {
// do not allow deletion of labels
return filter(elements, function(e) {
return !isLabel(e);
});
};
CmmnRules.prototype.canMove = function(elements, target) {
var self = this;
// allow default move check to start move operation
if (!target) {
return true;
}
return elements.every(function(element) {
if (isDiscretionaryItem(element)) {
if (!self.canMoveDiscretionaryItem(element, elements, target)) {
return false;
}
}
return self.canDrop(element, target);
});
};
CmmnRules.prototype.canMoveDiscretionaryItem = function(element, elements, target) {
return !isPlanFragment(target) || some(element.incoming, function(connection) {
var source = connection.source;
if (isDiscretionaryConnection(connection)) {
return isParent(target, source) || elements.indexOf(source) !== -1;
}
});
};
CmmnRules.prototype.canCreate = function(shape, target, source, position) {
var self = this;
if (!target) {
return false;
}
if (isLabel(target)) {
return null;
}
if (isSame(source, target)) {
return false;
}
// ensure we do not drop the element
// into source
if (source && isParent(source, target)) {
return false;
}
if (isDiscretionaryItem(shape)) {
if (!self.canCreateDiscretionaryItem(shape, target, source)) {
return false;
}
}
return self.canDrop(shape, target, position);
};
CmmnRules.prototype.canCreateDiscretionaryItem = function(shape, target, source) {
return !isPlanFragment(target) || isParent(target, source);
};
CmmnRules.prototype.canDrop = function(element, target) {
// can move labels everywhere
if (isLabel(element) && !isConnection(target)) {
return true;
}
if (isArtifact(element)) {
return is(target, 'cmmndi:CMMNDiagram') ||
isPlanFragment(target, true);
}
if (isCriterion(element)) {
return false;
}
if (isCasePlanModel(element)) {
// allow casePlanModels to drop only on root (CMMNDiagram)
return is(target, 'cmmndi:CMMNDiagram');
}
// allow any other element to drop on a case plan model or on an expanded stage
if (!isPlanFragment(target, true)) {
return false;
}
return !isCollapsed(target);
};
CmmnRules.prototype.canResize = function(shape, newBounds) {
if (isPlanFragment(shape, true)) {
return (!isCollapsed(shape)) && (
!newBounds || (newBounds.width >= 100 && newBounds.height >= 80)
);
}
if (isTextAnnotation(shape)) {
return true;
}
return false;
};
CmmnRules.prototype.canConnect = function(source, target, connection) {
if (nonExistingOrLabel(source) || nonExistingOrLabel(target)) {
return null;
}
// Disallow connections with same target and source element.
if (isSame(source, target)) {
return false;
}
if (this.canConnectPlanItemOnPartConnection(source, target)) {
return {
type: 'cmmn:PlanItemOnPart',
isStandardEventVisible: true
};
}
if (this.canConnectCaseFileItemOnPartConnection(source, target)) {
return {
type: 'cmmn:CaseFileItemOnPart',
isStandardEventVisible: true
};
}
if (this.canConnectDiscretionaryConnection(source, target, connection)) {
return {
type: 'cmmndi:CMMNEdge'
};
}
if (connection &&
is(connection.businessObject.cmmnElementRef, 'cmmn:Association') &&
this.canConnectAssociation(source, target)) {
return {
type: 'cmmn:Association'
};
}
if (isTextAnnotation(source) || isTextAnnotation(target)) {
return {
type: 'cmmn:Association'
};
}
return false;
};
CmmnRules.prototype.canConnectDiscretionaryConnection = function(source, target, connection) {
if (!isHumanTask(getDefinition(source))) {
return false;
}
if (!isBlocking(source)) {
return false;
}
if (!isDiscretionaryItem(target)) {
return false;
}
// A HumanTask MUST NOT be discretionary to itself.
var sourceDefinition = getDefinition(source);
var targetDefinition = getDefinition(target);
if (isSame(sourceDefinition, targetDefinition)) {
return false;
}
var sourceParent = getParent(source);
var targetParent = getParent(target);
if (!isSame(sourceParent, targetParent)) {
return false;
}
// if the target discretionary item is already
// part of a human task, then it should not be
// possible to create a connection to it in some
// cases.
if (isDiscretionaryToHumanTask(target)) {
if (!isUniqueDiscretionaryConnection(source, target, connection)) {
return false;
}
var parent = getParent(getBusinessObject(target), 'cmmn:HumanTask');
if (!isSame(getDefinition(source), parent)) {
if (hasIncomingDiscretionaryConnections(target, connection)) {
return false;
}
}
}
return true;
};
CmmnRules.prototype.canConnectPlanItemOnPartConnection = function(source, target) {
if (!isSameCase(source, target)) {
return false;
}
if (isExitCriterion(source)) {
source = source.host;
}
if (isSame(source, target.host)) {
return false;
}
if (isParent(source, target.host)) {
return false;
}
if (isEntryCriterion(target) && isParent(target.host, source)) {
return false;
}
return isPlanItem(source) && isCriterion(target);
};
CmmnRules.prototype.canConnectCaseFileItemOnPartConnection = function(source, target) {
return !!(isSameCase(source, target) &&
isCaseFileItem(source) &&
isCriterion(target));
};
CmmnRules.prototype.canConnectAssociation = function(source, target) {
// do not connect connections
if (isConnection(source) || isConnection(target)) {
return false;
}
// connect if different parent
return !isParent(target, source) &&
!isParent(source, target);
};
CmmnRules.prototype.canAttach = function(elements, target, source, position) {
if (isSame(source, target)) {
return false;
}
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 entry/exit criterion
if (!isCriterion(element)) {
return false;
}
// allow default move operation
if (!target) {
return true;
}
if (!this.canAttachCriterion(element, target, position, source)) {
return false;
}
return 'attach';
};
CmmnRules.prototype.canAttachEntryCriterion = function(element, target, position, source) {
if (!this.canAttachCriterion(element, target, position, source)) {
return false;
}
if (source && isParent(target, source)) {
return false;
}
// disallow drop entry criterion...
// ... on case plan model
if (isCasePlanModel(target)) {
return false;
}
if (isPlanFragment(target, true)) {
// ... when element has an incoming connection which source
// is a child of the target
return !some(element.incoming, function(connection) {
if (is(connection.businessObject.cmmnElementRef, 'cmmn:PlanItemOnPart')) {
return isParent(target, connection.source);
}
});
}
return true;
};
CmmnRules.prototype.canAttachExitCriterion = function(element, target, position, source) {
if (!this.canAttachCriterion(element, target, position, source)) {
return false;
}
// disallow drop exit criterion...
// ... on milestone
if (isMilestone(target)) {
return false;
}
// ... on non blocking task
if (isTask(target) && !isBlocking(target)) {
return false;
}
return true;
};
CmmnRules.prototype.canAttachCriterion = function(element, target, position, source) {
if (source && isParent(source, target)) {
return false;
}
// disallow drop criterion...
// ... on another criterion
if (isCriterion(target)) {
return false;
}
// ... on event listener
if (isEventListener(target)) {
return false;
}
// a plan fragment does not have any execution semantic,
// that why it should not be possible to attach an criterion
if (isPlanFragment(target)) {
return false;
}
// ... on a text annotation
if (isTextAnnotation(target)) {
return false;
}
// ... on a case file item
if (isCaseFileItem(target)) {
return false;
}
// only attach to border
if (position && !isCriterionAttachment(position, target)) {
return false;
}
return true;
};
CmmnRules.prototype.canReplace = function(elements, target, position, source) {
if (!target) {
return false;
}
if (isSame(source, target)) {
return false;
}
var canExecute = {
replacements: []
};
var self = this;
forEach(elements, function(element) {
if (isEntryCriterion(element) && !self.canAttachEntryCriterion(element, target, position, source)) {
if (self.canAttachExitCriterion(element, target, position, source)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'cmmn:ExitCriterion'
});
}
}
if (isExitCriterion(element) && !self.canAttachExitCriterion(element, target, position, source)) {
if (self.canAttachEntryCriterion(element, target, position, source)) {
canExecute.replacements.push({
oldElementId: element.id,
newElementType: 'cmmn:EntryCriterion'
});
}
}
if (isPlanFragment(target) &&
isDiscretionaryItem(element) &&
self.canDrop(element, target) &&
!self.canMoveDiscretionaryItem(element, elements, target)) {
var replacement = {
oldElementId: element.id,
newElementType: 'cmmn:PlanItem'
};
if (isPlanFragment(element)) {
assign(replacement, {
newDefinitionType: 'cmmn:Stage'
});
}
canExecute.replacements.push(replacement);
}
});
return canExecute.replacements.length ? canExecute : false;
};
CmmnRules.prototype.canReplaceConnectionEnd = function(source, target, side) {
if (!source || !target) {
return false;
}
if (isSame(source, target)) {
return false;
}
if (!isSameCase(source, target)) {
return false;
}
return side === 'source' ? this.canReplaceSource(source, target) : this.canReplaceTarget(source, target);
};
CmmnRules.prototype.canReplaceSource = function(source, target) {
if (isDiscretionaryItem(source) && isCriterion(target)) {
if (isSame(source, target.host)) {
return false;
}
if (isPlanFragment(source)) {
return false;
}
if (isParent(source, target.host)) {
return false;
}
if (isEntryCriterion(target) && isParent(target.host, source)) {
return false;
}
return {
type: 'cmmn:PlanItemOnPart',
isStandardEventVisible: true,
replacements: [{
oldElementId: source.id,
newElementType: 'cmmn:PlanItem'
}]
};
}
if (isEntryCriterion(source) && isEntryCriterion(target)) {
if (isSame(source.host, target.host)) {
return false;
}
if (isParent(target.host, source.host) ||
isParent(source.host, target.host)) {
return false;
}
if (hasIncomingOnPartConnections(source)) {
return false;
}
if (!this.canAttachExitCriterion(source, source.host)) {
return false;
}
return {
type: 'cmmn:PlanItemOnPart',
isStandardEventVisible: true,
replacements: [{
oldElementId: source.id,
newElementType: 'cmmn:ExitCriterion'
}]
};
}
return false;
};
CmmnRules.prototype.canReplaceTarget = function(source, target) {
if (isHumanTask(getDefinition(source)) && isPlanItem(target)) {
if (!isBlocking(source)) {
return false;
}
if (isEventListener(target) || isMilestone(target)) {
return false;
}
if (isSameDefinition(source, target)) {
return false;
}
if (!isSameParent(source, target)) {
return false;
}
return {
type: 'cmmndi:CMMNEdge',
replacements: [{
oldElementId: target.id,
newElementType: 'cmmn:DiscretionaryItem'
}]
};
}
if ((isPlanItem(source) || isExitCriterion(source)) && isEntryCriterion(target)) {
if (isSame(source.host || source, target.host)) {
return false;
}
if (!isParent(target.host, source.host || source)) {
return false;
}
if (hasIncomingOnPartConnections(target)) {
return false;
}
if (!this.canAttachExitCriterion(target, target.host)) {
return false;
}
return {
type: 'cmmn:PlanItemOnPart',
isStandardEventVisible: true,
replacements: [{
oldElementId: target.id,
newElementType: 'cmmn:ExitCriterion'
}]
};
}
return false;
};
CmmnRules.prototype.canSetRepetitionRule = function(element) {
return isPlanItemControlCapable(element);
};
CmmnRules.prototype.canSetRequiredRule = function(element) {
return isPlanItemControlCapable(element);
};
CmmnRules.prototype.canSetManualActivationRule = function(element) {
if (isMilestone(element)) {
return false;
}
return isPlanItemControlCapable(element);
};
/**
* 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, [
'cmmn:CaseFileItem',
'cmmn:Criterion',
'cmmn:DiscretionaryItem',
'cmmn:PlanItem'
]);
}
function isParent(possibleParent, element) {
var allParents = getParents(element);
return allParents.indexOf(possibleParent) !== -1;
}
function isPlanItemControlCapable(element) {
return isTask(element) || isMilestone(element) || is(getDefinition(element), 'cmmn:Stage');
}
function nonExistingOrLabel(element) {
return !element || isLabel(element);
}
function isLabel(element) {
return element.labelTarget;
}
function isConnection(element) {
return element.waypoints;
}
function isUniqueDiscretionaryConnection(source, target, connection) {
return !some(target.incoming, function(con) {
return con !== connection &&
isDiscretionaryConnection(con) &&
con.source === source &&
con.target === target;
});
}
function hasIncomingDiscretionaryConnections(target, connection) {
return some(target.incoming, function(con) {
return con !== connection && isDiscretionaryConnection(con);
});
}
function isDiscretionaryConnection(connection) {
return !connection.businessObject.cmmnElementRef;
}
function hasIncomingOnPartConnections(target) {
return some(target.incoming, function(con) {
return isOnPartConnection(con);
});
}
function isOnPartConnection(connection) {
return is(connection.businessObject.cmmnElementRef, 'cmmn:OnPart');
}
function isPlanFragment(element, isStage) {
var definition = getDefinition(element) || element;
if (!is(definition, 'cmmn:PlanFragment')) {
return false;
}
if (!isStage && is(definition, 'cmmn:Stage')) {
return false;
}
return true;
}
function isEventListener(element) {
return is(getDefinition(element), 'cmmn:EventListener');
}
function isMilestone(element) {
return is(getDefinition(element), 'cmmn:Milestone');
}
function isTask(element) {
return is(getDefinition(element), 'cmmn:Task');
}
function isBlocking(element) {
element = getDefinition(element);
return !!(element && element.isBlocking);
}
function isCriterion(element) {
return is(element, 'cmmn:Criterion');
}
function isEntryCriterion(element) {
return is(element, 'cmmn:EntryCriterion');
}
function isExitCriterion(element) {
return is(element, 'cmmn:ExitCriterion');
}
function isArtifact(element) {
if (isConnection(element)) {
element = element.businessObject.cmmnElementRef;
}
return is(element, 'cmmn:Artifact');
}
function isTextAnnotation(element) {
return is(element, 'cmmn:TextAnnotation');
}
function isCaseFileItem(element) {
return is(element, 'cmmn:CaseFileItem');
}
function isPlanItem(element) {
return is(element, 'cmmn:PlanItem');
}
function isSameParent(a, b) {
return isSame(getParent(a), getParent(b));
}
function isSameDefinition(a, b) {
return isSame(getDefinition(a), getDefinition(b));
}