Last month Tim from Sparta Systems visited us in our Berlin office with requirements for a custom BPMN modeler. Sparta Systems, a Camunda customer, employs bpmn-js to provide customizable, predefined workflows to their very own customers. In this post he talks about their specific requirements and how he effectively locked down our modeler to create a custom BPMN modeling tool on top of it.
A big thanks to Tim Sneed from Sparta Systems for writing this guest blog post.
Here at Sparta Systems, we are working on a customized version of bpmn-js, effectively called sparta-bpmn-js. Our main goal is to prototype a restricted version of the web modeler so that our users will only be able to edit certain areas of the workflow.
In our first round of prototyping, we had two objectives. The first objective was to determine if there was a way to prevent users from modifying a workflow that has loaded in our web modeler. This would include preventing palette items from being dropped anywhere on the workflow, and removing any modifiable actions from the context pad of an element.
For the second objective, we wanted to introduce a palette that would allow an item to be dropped only on a "special" SequenceFlow and nothing else.
For example, here is a simple workflow. Notice the "special" SequenceFlow decorated as a red dashed line.
A user is not allowed to place a palette item on a non-"special" SequenceFlow.
A user should be allowed, however, to place the palette item on the "special" SequenceFlow.
This post will describe the steps we took to achieve both objectives and come out with a demonstrable prototype.
The Requirements
Given the problem that we wanted to solve, we came up with the following requirements:
Lock down everything!!
We at Sparta supply our users with best practice workflows. It is important that the integrity of the workflow remain intact with the goal of little to no customization needed. The very conservative approach is to "lock-down" the entire workflow. However we know after many years of experience in the quality management business that clients will need to make perhaps a few minor tweaks to suit the specific nuances of their business. So while our desire is to permit a little customization as possible we recognize some will be necessary.
Customized palette
For this proof-of-concept, our palette will contain only one model element, a Call Activity to some other process. This will require us to create our own palette provider feature module.
Customized context pad
Given that we do not want the users to manipulate existing workflow elements, we need to completely control the context pad. So, like the customized palette, we want to create our own context pad provider feature module.
Certain SequenceFlows can accept palette items
Now that the modeler is effectively locked down, we want to identify certain SequenceFlows in a workflow that would allow the Call Activity palette item to be dropped onto it. Once dropped, the palette item must be allowed to be removed. Upon removal, the initial SequenceFlow between the Call Activity’s source and target elements must be automatically re-established (since a user would not be allowed to re-establish their own SequenceFlows).
The Prototype
Now that we have our requirements, let’s get to work. To make things simpler, we are using the same project structure as bpmn-js in the sparta-bpmn-js project. From there we depend on bpmn-js and require the features needed to build our own custom modeler.
Controlling the Palette
Since we only need a single palette item for this prototype, we created our own PaletteProvider
module and replaced the bpmn-js PaletteProvider
with our own.
assign(actions, {
'lasso-tool': {
group: 'tools',
className: 'icon-lasso-tool',
title: 'Activate the lasso tool',
action: {
click: function(event) {
lassoTool.activateSelection(event);
}
}
},
'space-tool': {
group: 'tools',
className: 'icon-space-tool',
title: 'Activate the create/remove space tool',
action: {
click: function(event) {
spaceTool.activateSelection(event);
}
}
},
'tool-separator': {
group: 'tools',
separator: true
},
'create.call-activity': createAction(
'bpmn:CallActivity', 'event', 'icon-call-activity', 'Add Investigation Process', {
labelText: 'Investigation',
moddleAttrs: {
calledElement: 'Investigation-800',
'ssi:droppable': 'true'
}
}
)
});
Take note of the
ssi:droppable
moddle attribute defined for thecreate.call-activity
palette item, that will be explained later in this post.
Controlling the Context Pad
Like the Palette
, we needed to customize the context pad completely. Therefore we added our own ContextPadProvider
without actions that would modify an element.
if (is(element, 'bpmn:FlowNode') || is(element, 'bpmn:InteractionNode')) {
assign(actions, {
'append.text-annotation': appendAction('bpmn:TextAnnotation', 'icon-text-annotation'),
});
}
We leave in the text annotation to allow users to make notes if they wish.
if (element.businessObject.get('ssi:droppable') === 'true' || is(element, 'bpmn:TextAnnotation')) {
assign(actions, {
'delete': {
group: 'edit',
className: 'icon-trash',
title: 'Remove',
action: {
click: removeElement,
dragstart: removeElement
}
}
});
}
Only elements that contain the attribute
ssi:droppable
and value of "true" do we allow them to be removed. This prevents other "locked-down" elements from being removed.
Customize diagram interaction using the Rules API
To start with a "locked-down" approach, we want to first prevent any element (or shape) created from the Palette from being dropped onto the workflow. Thankfully, the diagram-js library provides us with an interface to add our own rules. We inherit from the RuleProvider class and make use of the "addRule" function.
Every time an element is created from the palette, a shape.create
event is fired. When adding a rule, if the evaluation returns false, then the action to drop the element will not be allowed. Easy enough, we just always return false.
function SpartaRules(eventBus) {
RuleProvider.call(this, eventBus);
}
inherits(SpartaRules, RuleProvider);
SpartaRules.$inject = ['eventBus'];
SpartaRules.prototype.init = function() {
this.addRule('shape.create', function(context) {
return false;
});
};
At this point, no item from the palette can be dropped onto the workflow.
NOTE: Your custom rules module MUST be the first module in the
Modeler._modules
array, otherwise the custom rules would not be evaluated before other rule modules and thus cannot overrule them.
Since we want to allow users to eventually add custom palette items onto the workflow, we need to continue to allow users to move elements and connections around the diagram as they may see fit. But we do not want allow them modify a connection’s source and target elements. Using the Rules
API this is easily achieved by adding rules for the connection.reconnectStart
and connection.reconnectEnd
events.
this.addRule('connection.reconnectStart', function(context) {
var target = context.hover;
var connection = context.connection;
return canReconnectStart(connection, target);
});
this.addRule('connection.reconnectEnd', function(context) {
var target = context.hover;
var connection = context.connection;
return canReconnectEnd(connection, target);
});
function canReconnectStart(connection, target) {
return target.id === connection.source.id;
}
function canReconnectEnd(connection, target) {
return target.id === connection.target.id;
}
By validating the hover element’s id attribute against the connection’s source or target element id
attribute, we can prevent users from modifying connection’s source/target element.
"Special" SequenceFlows
At this point, we still cannot add any elements from the palette since the shape.create
rule always returns false
. We want to allow specific elements to be dropped on certain SequenceFlows.
This next objective is to configure a SequenceFlow in an existing workflow to allow a Call Activity to be dropped onto it. We do this first by adding the attribute ssi:allowDrop
to a SequenceFlow element in our BPMN file. The value is the element type that the SequenceFlow will allowed to be dropped on it. In this case, bpmn:CallActivity
.
<bpmn2:sequenceFlow id="SequenceFlow_2"
ssi:allowDrop="bpmn:CallActivity"
sourceRef="someSource"
targetRef="someTarget" />
Now we want to display these "special" SequenceFlows differently. In this example we will decorate the SequenceFlow as a red, dashed line. This can be achieved by making use of the event bus and listening to certain events that will allow us to re-style the "special" SequenceFlows.
eventBus.on(['canvas.init'], function(event) {
svg = event.svg;
});
We must first obtain a reference to the SVG library (Snap.svg) when the canvas is initialized.
eventBus.on([
'connection.added',
'connection.changed',
'bendpoint.move.move',
'bendpoint.move.cleanup'
], function(event) {
var element = event.element || event.connection;
var bo = element.businessObject;
if (bo.get('ssi:allowDrop')) {
//...
//modify connection color
//...
}
});
Then we listen to various connection
events, as well as bendpoint
events to re-style the connection when users are moving connections around. Check out the rest of the source to see how we re-style the connections.
Now, are "special" SequenceFlow went from looking like this
to this
Next, in our custom rule module, we modify our evaluation of the shape.create
rule to allow specific palette items to be dropped on these "special" SequenceFlows.
this.addRule('shape.create', function(context) {
var target = context.parent;
var shape = context.shape;
return canCreate(shape, target);
});
function canCreate(shape, target) {
var allowDrop = target.businessObject.get('ssi:allowDrop');
var droppable = shape.businessObject.get('ssi:droppable');
if (droppable === 'true') {
if ((!allowDrop || !ModelUtil.is(shape, allowDrop))) {
return false;
} else {
return true;
}
} else {
return false;
}
}
Here the shape being dropped is checked to ensure that the ssi:droppable
attribute is defined (we defined this before in the custom PaletteProvider
). If defined, we ensure that the target connection accepts the shape type that is being dropped.
Because the palette item we added contains the attribute ssi:droppable
, we can now drop the CallActivity onto the "special" SequenceFlow!
Removing the CallActivity
Given our current customization of bpmn-js, a user cannot establish their own SequenceFlows in the modeler tool. Therefore, when removing the CallActivity we just dropped onto the "special" SequenceFlow, we need to re-establish the "special" SequenceFlow.
To do this, we make use of the CommandInterceptor
API and add a behavior. Prior to the shape.delete
event from executing, we want to take the incoming connection to the CallActivity and re-attach its end to the CallActivity’s target element.
function RemoveDroppableBehavior(eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
this.preExecute('shape.delete', function(context) {
var shape = context.shape;
if (shape.businessObject.get('ssi:droppable') === 'true') {
var incoming = shape.incoming[0];
var outgoing = shape.outgoing[0];
var incomingWaypoints = incoming.waypoints.slice();
var outgoingWaypoints = outgoing.waypoints.slice();
var target = outgoing.target;
var newWaypoints;
//remove waypoints that connected between the call activity element
incomingWaypoints.pop();
outgoingWaypoints.shift();
newWaypoints = incomingWaypoints.concat(outgoingWaypoints);
//reconnect incoming connection's target to the call activity's outgoing connection's target
modeling.reconnectEnd(incoming, target, newWaypoints);
}
}, true);
}
With the custom modeling behavior the delete interaction now looks like this:
Conclusion
This post showed how we built our very own customized BPMN 2.0 modeler using bpmn-js. Through the use of the Rules
and CommandInterceptor
APIs we discovered how to introduce custom rules and behaviors. Thanks to the feature-based pattern of bpmn-js, we could easily swap out the context pad and the palette with our own. This all proves that strong customization is possible with bpmn-js, and we are extremely happy to come out with a working prototype. A big thanks to the Camunda team for assisting us in this endeavor!
We have open sourced an example project to our GitHub, feel free to check it out.
Are you passionate about JavaScript, modeling, and the web?
Join Camunda and build modeling tools people .