From 2019c658dfb7c3512c85cdb762915a5516b8785d Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Fri, 2 Jan 2015 16:15:18 +0100 Subject: [PATCH] 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 --- lib/features/modeling/Modeling.js | 12 + .../modeling/cmd/UpdatePropertiesHandler.js | 96 ++++++++ test/fixtures/bpmn/conditions.bpmn | 78 +++++++ .../features/modeling/UpdatePropertiesSpec.js | 205 ++++++++++++++++++ 4 files changed, 391 insertions(+) create mode 100644 lib/features/modeling/cmd/UpdatePropertiesHandler.js create mode 100644 test/fixtures/bpmn/conditions.bpmn create mode 100644 test/spec/features/modeling/UpdatePropertiesSpec.js diff --git a/lib/features/modeling/Modeling.js b/lib/features/modeling/Modeling.js index fa2e5ee7..5431a06a 100644 --- a/lib/features/modeling/Modeling.js +++ b/lib/features/modeling/Modeling.js @@ -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 + }); +}; \ No newline at end of file diff --git a/lib/features/modeling/cmd/UpdatePropertiesHandler.js b/lib/features/modeling/cmd/UpdatePropertiesHandler.js new file mode 100644 index 00000000..296a780e --- /dev/null +++ b/lib/features/modeling/cmd/UpdatePropertiesHandler.js @@ -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} 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; +}; \ No newline at end of file diff --git a/test/fixtures/bpmn/conditions.bpmn b/test/fixtures/bpmn/conditions.bpmn new file mode 100644 index 00000000..12d7e655 --- /dev/null +++ b/test/fixtures/bpmn/conditions.bpmn @@ -0,0 +1,78 @@ + + + + + SequenceFlow_1 + SequenceFlow_3 + SequenceFlow_4 + + + ${foo > bar} + + + SequenceFlow_2 + SequenceFlow_4 + + + SequenceFlow_1 + SequenceFlow_2 + + + + + + + + SequenceFlow_3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/features/modeling/UpdatePropertiesSpec.js b/test/spec/features/modeling/UpdatePropertiesSpec.js new file mode 100644 index 00000000..d7b975bd --- /dev/null +++ b/test/spec/features/modeling/UpdatePropertiesSpec.js @@ -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')); + })); + + }); + +});