feat(modeling): add property update mechanism

This adds the modeling#updateProperties(element, props) method to the
modeler that can be used to set BPMN 2.0 properties on elements.

By assigning the properties this way, the modeler is aware of the
elements that got changed and can update / redraw the elements
accordingly.

This hooks up with the modelers undo/redo chain, too.

Related to #167
This commit is contained in:
Nico Rehwaldt 2015-01-02 16:15:18 +01:00
parent 07ba58d805
commit 2019c658df
4 changed files with 391 additions and 0 deletions

View File

@ -10,6 +10,8 @@ var CreateShapeHandler = require('diagram-js/lib/features/modeling/cmd/CreateSha
MoveShapesHandler = require('diagram-js/lib/features/modeling/cmd/MoveShapesHandler'),
ResizeShapeHandler = require('diagram-js/lib/features/modeling/cmd/ResizeShapeHandler'),
UpdatePropertiesHandler = require('./cmd/UpdatePropertiesHandler'),
AppendShapeHandler = require('diagram-js/lib/features/modeling/cmd/AppendShapeHandler'),
CreateLabelHandler = require('diagram-js/lib/features/modeling/cmd/CreateLabelHandler'),
@ -49,6 +51,8 @@ Modeling.prototype.registerHandlers = function(commandStack) {
commandStack.registerHandler('label.create', CreateLabelHandler);
commandStack.registerHandler('element.updateProperties', UpdatePropertiesHandler);
commandStack.registerHandler('connection.create', CreateConnectionHandler);
commandStack.registerHandler('connection.delete', DeleteConnectionHandler);
commandStack.registerHandler('connection.move', MoveConnectionHandler);
@ -83,3 +87,11 @@ Modeling.prototype.connect = function(source, target, attrs) {
return this.createConnection(source, target, attrs, source.parent);
};
Modeling.prototype.updateProperties = function(element, properties) {
this._commandStack.execute('element.updateProperties', {
element: element,
properties: properties
});
};

View File

@ -0,0 +1,96 @@
'use strict';
var _ = require('lodash');
var DEFAULT_FLOW = 'default',
NAME = 'name';
/**
* A handler that implements a BPMN 2.0 property update.
*
* This should be used to set simple properties on elements with
* an underlying BPMN business object.
*
* Use respective diagram-js provided handlers if you would
* like to perform automated modeling.
*/
function UpdatePropertiesHandler(elementRegistry) {
this._elementRegistry = elementRegistry;
}
UpdatePropertiesHandler.$inject = [ 'elementRegistry' ];
module.exports = UpdatePropertiesHandler;
////// api /////////////////////////////////////////////
/**
* Updates a BPMN element with a list of new properties
*
* @param {Object} context
* @param {djs.model.Base} context.element the element to update
* @param {Object} context.properties a list of properties to set on the element's
* businessObject (the BPMN model element)
*
* @return {Array<djs.mode.Base>} the updated element
*/
UpdatePropertiesHandler.prototype.execute = function(context) {
var element = context.element,
changed = [ element ];
if (!element) {
throw new Error('element required');
}
var elementRegistry = this._elementRegistry;
var businessObject = element.businessObject,
properties = context.properties,
oldProperties = context.oldProperties || _.pick(businessObject, _.keys(properties));
// correctly indicate visual changes on default flow updates
if (DEFAULT_FLOW in properties) {
if (properties[DEFAULT_FLOW]) {
changed.push(elementRegistry.get(properties[DEFAULT_FLOW].id));
}
if (businessObject[DEFAULT_FLOW]) {
changed.push(elementRegistry.get(businessObject[DEFAULT_FLOW].id));
}
}
if (NAME in properties && element.label) {
changed.push(element.label);
}
// update properties
_.assign(businessObject, properties);
// store old values
context.oldProperties = oldProperties;
context.changed = changed;
// indicate changed on objects affected by the update
return changed;
};
/**
* Reverts the update on a BPMN elements properties.
*
* @param {Object} context
*
* @return {djs.mode.Base} the updated element
*/
UpdatePropertiesHandler.prototype.revert = function(context) {
var element = context.element,
businessObject = element.businessObject;
_.assign(businessObject, context.oldProperties);
return context.changed;
};

78
test/fixtures/bpmn/conditions.bpmn vendored Normal file
View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="_G5HDsJKJEeSY3uHQ7B6T_A" exporter="camunda modeler" exporterVersion="2.6.0" targetNamespace="http://activiti.org/bpmn">
<bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:serviceTask id="ServiceTask_1">
<bpmn2:incoming>SequenceFlow_1</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_3</bpmn2:outgoing>
<bpmn2:outgoing>SequenceFlow_4</bpmn2:outgoing>
</bpmn2:serviceTask>
<bpmn2:sequenceFlow id="SequenceFlow_3" name="conditional" sourceRef="ServiceTask_1" targetRef="EndEvent_1">
<bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">${foo > bar}</bpmn2:conditionExpression>
</bpmn2:sequenceFlow>
<bpmn2:task id="Task_2">
<bpmn2:incoming>SequenceFlow_2</bpmn2:incoming>
<bpmn2:incoming>SequenceFlow_4</bpmn2:incoming>
</bpmn2:task>
<bpmn2:exclusiveGateway id="ExclusiveGateway_1" default="SequenceFlow_1">
<bpmn2:outgoing>SequenceFlow_1</bpmn2:outgoing>
<bpmn2:outgoing>SequenceFlow_2</bpmn2:outgoing>
</bpmn2:exclusiveGateway>
<bpmn2:sequenceFlow id="SequenceFlow_1" name="default" sourceRef="ExclusiveGateway_1" targetRef="ServiceTask_1"/>
<bpmn2:sequenceFlow id="SequenceFlow_2" name="" sourceRef="ExclusiveGateway_1" targetRef="Task_2">
<bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression"><![CDATA[${foo < bar}]]></bpmn2:conditionExpression>
</bpmn2:sequenceFlow>
<bpmn2:sequenceFlow id="SequenceFlow_4" name="" sourceRef="ServiceTask_1" targetRef="Task_2"/>
<bpmn2:endEvent id="EndEvent_1">
<bpmn2:incoming>SequenceFlow_3</bpmn2:incoming>
</bpmn2:endEvent>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_ExclusiveGateway_2" bpmnElement="ExclusiveGateway_1" isMarkerVisible="true">
<dc:Bounds height="50.0" width="50.0" x="372.0" y="204.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="0.0" width="0.0" x="397.0" y="259.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="ServiceTask_1">
<dc:Bounds height="80.0" width="100.0" x="492.0" y="84.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow_1" sourceElement="_BPMNShape_ExclusiveGateway_2" targetElement="_BPMNShape_Task_2">
<di:waypoint xsi:type="dc:Point" x="397.0" y="204.0"/>
<di:waypoint xsi:type="dc:Point" x="397.0" y="124.0"/>
<di:waypoint xsi:type="dc:Point" x="492.0" y="124.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="21.0" width="44.0" x="348.0" y="138.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_Task_3" bpmnElement="Task_2">
<dc:Bounds height="80.0" width="100.0" x="492.0" y="300.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_2" bpmnElement="SequenceFlow_2" sourceElement="_BPMNShape_ExclusiveGateway_2" targetElement="_BPMNShape_Task_3">
<di:waypoint xsi:type="dc:Point" x="397.0" y="254.0"/>
<di:waypoint xsi:type="dc:Point" x="397.0" y="340.0"/>
<di:waypoint xsi:type="dc:Point" x="492.0" y="340.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="6.0" width="6.0" x="394.0" y="279.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_EndEvent_2" bpmnElement="EndEvent_1">
<dc:Bounds height="36.0" width="36.0" x="732.0" y="106.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="0.0" width="0.0" x="750.0" y="147.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_3" bpmnElement="SequenceFlow_3" sourceElement="_BPMNShape_Task_2" targetElement="_BPMNShape_EndEvent_2">
<di:waypoint xsi:type="dc:Point" x="592.0" y="124.0"/>
<di:waypoint xsi:type="dc:Point" x="732.0" y="124.0"/>
<bpmndi:BPMNLabel>
<dc:Bounds height="21.0" width="68.0" x="612.0" y="128.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_4" bpmnElement="SequenceFlow_4" sourceElement="_BPMNShape_Task_2" targetElement="_BPMNShape_Task_3">
<di:waypoint xsi:type="dc:Point" x="542.0" y="164.0"/>
<di:waypoint xsi:type="dc:Point" x="542.0" y="300.0"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>

View File

@ -0,0 +1,205 @@
'use strict';
var TestHelper = require('../../../TestHelper');
/* global bootstrapModeler, inject */
var _ = require('lodash');
var fs = require('fs');
var modelingModule = require('../../../../lib/features/modeling'),
coreModule = require('../../../../lib/core');
describe('features/modeling - update properties', function() {
var diagramXML = fs.readFileSync('test/fixtures/bpmn/conditions.bpmn', 'utf8');
var testModules = [ coreModule, modelingModule ];
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
var updatedElements;
beforeEach(inject(function(eventBus) {
eventBus.on([ 'commandStack.execute', 'commandStack.revert' ], function() {
updatedElements = [];
});
eventBus.on('element.changed', function(event) {
updatedElements.push(event.element);
});
}));
describe('should execute', function() {
it('setting loop characteristics', inject(function(elementRegistry, modeling, moddle) {
// given
var loopCharacteristics = moddle.create('bpmn:MultiInstanceLoopCharacteristics');
var taskShape = elementRegistry.get('ServiceTask_1');
// when
modeling.updateProperties(taskShape, { loopCharacteristics: loopCharacteristics });
// then
expect(taskShape.businessObject.loopCharacteristics).toBe(loopCharacteristics);
// task shape got updated
expect(updatedElements).toContain(taskShape);
}));
it('updating default flow', inject(function(elementRegistry, modeling) {
// given
var gatewayShape = elementRegistry.get('ExclusiveGateway_1');
// when
modeling.updateProperties(gatewayShape, { 'default': undefined });
// then
expect(gatewayShape.businessObject['default']).not.toBeDefined();
// flow got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1'));
}));
it('updating label', inject(function(elementRegistry, modeling) {
// given
var flowConnection = elementRegistry.get('SequenceFlow_1');
// when
modeling.updateProperties(flowConnection, { name: 'FOO BAR' });
// then
expect(flowConnection.businessObject.name).toBe('FOO BAR');
// flow label got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1_label'));
}));
});
describe('should undo', function() {
it('setting loop characteristics', inject(function(elementRegistry, modeling, commandStack, moddle) {
// given
var loopCharactersistics = moddle.create('bpmn:MultiInstanceLoopCharacteristics');
var taskShape = elementRegistry.get('ServiceTask_1');
// when
modeling.updateProperties(taskShape, { loopCharacteristics: loopCharactersistics });
commandStack.undo();
// then
expect(taskShape.businessObject.loopCharactersistics).not.toBeDefined();
}));
it('updating default flow', inject(function(elementRegistry, commandStack, modeling) {
// given
var gatewayShape = elementRegistry.get('ExclusiveGateway_1');
// when
modeling.updateProperties(gatewayShape, { 'default': undefined });
commandStack.undo();
// then
expect(gatewayShape.businessObject['default']).toBeDefined();
// flow got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1'));
}));
it('updating name', inject(function(elementRegistry, commandStack, modeling) {
// given
var flowConnection = elementRegistry.get('SequenceFlow_1');
// when
modeling.updateProperties(flowConnection, { name: 'FOO BAR' });
commandStack.undo();
// then
expect(flowConnection.businessObject.name).toBe('default');
// flow got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1_label'));
}));
});
describe('should redo', function() {
it('setting loop characteristics', inject(function(elementRegistry, modeling, commandStack, moddle) {
// given
var loopCharacteristics = moddle.create('bpmn:MultiInstanceLoopCharacteristics');
var taskShape = elementRegistry.get('ServiceTask_1');
// when
modeling.updateProperties(taskShape, { loopCharacteristics: loopCharacteristics });
commandStack.undo();
commandStack.redo();
// then
expect(taskShape.businessObject.loopCharacteristics).toBe(loopCharacteristics);
}));
it('updating default flow', inject(function(elementRegistry, commandStack, modeling) {
// given
var gatewayShape = elementRegistry.get('ExclusiveGateway_1');
// when
modeling.updateProperties(gatewayShape, { 'default': undefined });
commandStack.undo();
commandStack.redo();
// then
expect(gatewayShape.businessObject['default']).not.toBeDefined();
// flow got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1'));
}));
it('updating name', inject(function(elementRegistry, commandStack, modeling) {
// given
var flowConnection = elementRegistry.get('SequenceFlow_1');
// when
modeling.updateProperties(flowConnection, { name: 'FOO BAR' });
commandStack.undo();
commandStack.redo();
// then
expect(flowConnection.businessObject.name).toBe('FOO BAR');
// flow got updated, too
expect(updatedElements).toContain(elementRegistry.get('SequenceFlow_1_label'));
}));
});
});