diff --git a/lib/Modeler.js b/lib/Modeler.js index 14e92f7a..e40e01a0 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -67,10 +67,13 @@ Modeler.prototype._modelingModules = [ require('diagram-js/lib/features/snapping'), require('diagram-js/lib/features/move'), require('diagram-js/lib/features/resize'), + require('diagram-js/lib/features/drop'), require('./features/modeling'), require('./features/context-pad'), require('./features/palette'), - require('./features/resize') + require('./features/resize'), + require('./features/rules'), + require('./features/drop') ]; diff --git a/lib/features/drop/BpmnDrop.js b/lib/features/drop/BpmnDrop.js new file mode 100644 index 00000000..103e3c76 --- /dev/null +++ b/lib/features/drop/BpmnDrop.js @@ -0,0 +1,25 @@ +'use strict'; + +var _ = require('lodash'); + + + +function BpmnDrop(drop, openSequenceflowHandler, updateSequenceFlowParentHandler) { + + var actions = { + 'updateSequenceFlowParent': updateSequenceFlowParentHandler, + 'removeOpenSequenceflow': openSequenceflowHandler + }; + + var self = this; + + this._drop = drop; + + _.forEach(actions, function(action, key) { + self._drop.registerAfterDropAction(key, action.execute); + }); +} + +BpmnDrop.$inject = [ 'drop', 'openSequenceflowHandler', 'updateSequenceFlowParentHandler' ]; + +module.exports = BpmnDrop; diff --git a/lib/features/drop/OpenSequenceflowHandler.js b/lib/features/drop/OpenSequenceflowHandler.js new file mode 100644 index 00000000..1d7170b2 --- /dev/null +++ b/lib/features/drop/OpenSequenceflowHandler.js @@ -0,0 +1,67 @@ +'use strict'; + +var _ = require('lodash'); + +var self; +function OpenSequenceflowHandler(modeling) { + + self = this; + this._modeling = modeling; +} + +OpenSequenceflowHandler.$inject = [ 'modeling' ]; + +module.exports = OpenSequenceflowHandler; + + +/** + * Removes sequence flows that source or target does not have same parent. + */ +OpenSequenceflowHandler.prototype.execute = function(context) { + + var shapes = context.shapes, + connections = context.connections, + target = context.target; + + self._removeConnections(shapes, connections, target); +}; + + +OpenSequenceflowHandler.prototype._removeConnections = function(shapes, connections, target) { + + var modeling = self._modeling; + + var removeableConnections = getRemoveableConnections(shapes, connections); + + _.forEach(removeableConnections, function(connection) { + + var sourceParent = connection.source.parent, + targetParent = connection.target.parent; + + if (sourceParent.id !== targetParent.id) { + delete connections[connection.id]; + modeling.removeConnection(connection); + } + }); +}; + +function getRemoveableConnections(shapes, connections) { + + var connectionsToRemove = {}; + + _.forEach(shapes, function(shape) { + + var allConnections = _.union(shape.incoming, shape.outgoing); + + _.forEach(allConnections, function(connection) { + // if one of the connection endpoints points to a shape that is not part of the map + // delete the connection + if (!(shapes[connection.source.id] && shapes[connection.target.id])) { + connectionsToRemove[connection.id] = connection; + } + }); + + }); + + return connectionsToRemove; +} diff --git a/lib/features/drop/UpdateSequenceFlowParentHandler.js b/lib/features/drop/UpdateSequenceFlowParentHandler.js new file mode 100644 index 00000000..0a5a8884 --- /dev/null +++ b/lib/features/drop/UpdateSequenceFlowParentHandler.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('lodash'); + +var self; +function UpdateSequenceFlowParentHandler(modeling) { + + self = this; + this._modeling = modeling; +} + +UpdateSequenceFlowParentHandler.$inject = [ 'modeling' ]; + +module.exports = UpdateSequenceFlowParentHandler; + + + +UpdateSequenceFlowParentHandler.prototype.execute = function(context) { + + var shapes = context.shapes, + target = context.target; + + _.forEach(shapes, function(shape) { + handleFlow(shape.incoming); + handleFlow(shape.outgoing); + }); + + + function handleFlow(flows) { + + _.forEach(flows, function(flow) { + if (shapes[flow.source.id] && shapes[flow.target.id]) { + flow.parent = target; + } + }); + } +}; diff --git a/lib/features/drop/index.js b/lib/features/drop/index.js new file mode 100644 index 00000000..9df6c56f --- /dev/null +++ b/lib/features/drop/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + __init__: [ 'bpmnDrop', 'openSequenceflowHandler', 'updateSequenceFlowParentHandler' ], + __depends__: [ + require('diagram-js/lib/features/drop') + ], + bpmnDrop: [ 'type', require('./BpmnDrop') ], + openSequenceflowHandler: [ 'type', require('./OpenSequenceflowHandler') ], + updateSequenceFlowParentHandler: [ 'type', require('./UpdateSequenceFlowParentHandler') ] +}; diff --git a/lib/features/modeling/BpmnFactory.js b/lib/features/modeling/BpmnFactory.js index 36a5ea62..fd2aa3aa 100644 --- a/lib/features/modeling/BpmnFactory.js +++ b/lib/features/modeling/BpmnFactory.js @@ -80,4 +80,4 @@ BpmnFactory.prototype.createDiEdge = function(semantic, waypoints, attrs) { }; -module.exports = BpmnFactory; \ No newline at end of file +module.exports = BpmnFactory; diff --git a/lib/features/modeling/BpmnUpdater.js b/lib/features/modeling/BpmnUpdater.js index d6ab99ce..8499efdf 100644 --- a/lib/features/modeling/BpmnUpdater.js +++ b/lib/features/modeling/BpmnUpdater.js @@ -96,6 +96,7 @@ BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory', 'connectionDocking']; BpmnUpdater.prototype.updateShapeParent = function(shape) { + var parentShape = shape.parent; var businessObject = shape.businessObject, @@ -171,6 +172,7 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent) BpmnUpdater.prototype.updateConnectionWaypoints = function(connection) { + connection.businessObject.di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints)); }; @@ -233,7 +235,6 @@ BpmnUpdater.prototype.reverted = function(commands, callback) { }; BpmnUpdater.prototype.on = function(commands, suffix, callback) { - commands = _.isArray(commands) ? commands : [ commands ]; _.forEach(commands, function(c) { diff --git a/lib/features/modeling/LabelSupport.js b/lib/features/modeling/LabelSupport.js index 28e6b55d..fbd32188 100644 --- a/lib/features/modeling/LabelSupport.js +++ b/lib/features/modeling/LabelSupport.js @@ -95,4 +95,4 @@ function LabelSupport(eventBus, modeling, bpmnFactory) { LabelSupport.$inject = [ 'eventBus', 'modeling', 'bpmnFactory' ]; -module.exports = LabelSupport; \ No newline at end of file +module.exports = LabelSupport; diff --git a/lib/features/modeling/Modeling.js b/lib/features/modeling/Modeling.js index 4ca3727d..253b6749 100644 --- a/lib/features/modeling/Modeling.js +++ b/lib/features/modeling/Modeling.js @@ -7,6 +7,7 @@ var BaseModeling = require('diagram-js/lib/features/modeling/Modeling'); var CreateShapeHandler = require('diagram-js/lib/features/modeling/cmd/CreateShapeHandler'), DeleteShapeHandler = require('diagram-js/lib/features/modeling/cmd/DeleteShapeHandler'), MoveShapeHandler = require('diagram-js/lib/features/modeling/cmd/MoveShapeHandler'), + MoveShapesHandler = require('diagram-js/lib/features/modeling/cmd/MoveShapesHandler'), ResizeShapeHandler = require('diagram-js/lib/features/modeling/cmd/ResizeShapeHandler'), AppendShapeHandler = require('diagram-js/lib/features/modeling/cmd/AppendShapeHandler'), @@ -16,6 +17,7 @@ var CreateShapeHandler = require('diagram-js/lib/features/modeling/cmd/CreateSha CreateConnectionHandler = require('diagram-js/lib/features/modeling/cmd/CreateConnectionHandler'), DeleteConnectionHandler = require('diagram-js/lib/features/modeling/cmd/DeleteConnectionHandler'), MoveConnectionHandler = require('diagram-js/lib/features/modeling/cmd/MoveConnectionHandler'), + MoveConnectionsHandler = require('diagram-js/lib/features/modeling/cmd/MoveConnectionsHandler'), LayoutConnectionHandler = require('diagram-js/lib/features/modeling/cmd/LayoutConnectionHandler'); @@ -40,6 +42,7 @@ Modeling.prototype.registerHandlers = function(commandStack) { commandStack.registerHandler('shape.create', CreateShapeHandler); commandStack.registerHandler('shape.delete', DeleteShapeHandler); commandStack.registerHandler('shape.move', MoveShapeHandler); + commandStack.registerHandler('shapes.move', MoveShapesHandler); commandStack.registerHandler('shape.resize', ResizeShapeHandler); commandStack.registerHandler('shape.append', AppendShapeHandler); @@ -49,6 +52,7 @@ Modeling.prototype.registerHandlers = function(commandStack) { commandStack.registerHandler('connection.create', CreateConnectionHandler); commandStack.registerHandler('connection.delete', DeleteConnectionHandler); commandStack.registerHandler('connection.move', MoveConnectionHandler); + commandStack.registerHandler('connections.move', MoveConnectionsHandler); commandStack.registerHandler('connection.layout', LayoutConnectionHandler); }; @@ -85,20 +89,6 @@ Modeling.prototype.appendTextAnnotation = function(source, type, position) { return this.appendShape(source, { type: type }, position, null, { attrs: { type: 'bpmn:Association' } }); }; - -Modeling.prototype.canConnect = function(source, target) { - if (source.labelTarget || target.labelTarget) { - return null; - } - - return source.businessObject.$parent === target.businessObject.$parent && - source.businessObject.$instanceOf('bpmn:FlowNode') && - !source.businessObject.$instanceOf('bpmn:EndEvent') && - !target.businessObject.$instanceOf('bpmn:StartEvent') && - target.businessObject.$instanceOf('bpmn:FlowElement'); -}; - - Modeling.prototype.connect = function(source, target, attrs) { var sourceBo = source.businessObject, @@ -117,4 +107,4 @@ Modeling.prototype.connect = function(source, target, attrs) { } return this.createConnection(source, target, attrs, source.parent); -}; \ No newline at end of file +}; diff --git a/lib/features/modeling/index.js b/lib/features/modeling/index.js index 0845fb21..c9296b58 100644 --- a/lib/features/modeling/index.js +++ b/lib/features/modeling/index.js @@ -3,7 +3,8 @@ module.exports = { __depends__: [ require('../label-editing'), require('diagram-js/lib/command'), - require('diagram-js/lib/features/change-support') + require('diagram-js/lib/features/change-support'), + require('diagram-js/lib/features/drop') ], bpmnFactory: [ 'type', require('./BpmnFactory') ], bpmnUpdater: [ 'type', require('./BpmnUpdater') ], @@ -12,4 +13,4 @@ module.exports = { labelSupport: [ 'type', require('./LabelSupport') ], layouter: [ 'type', require('./Layouter') ], connectionDocking: [ 'type', require('diagram-js/lib/layout/CroppingConnectionDocking') ] -}; \ No newline at end of file +}; diff --git a/lib/features/rules/BpmnRules.js b/lib/features/rules/BpmnRules.js new file mode 100644 index 00000000..413b78aa --- /dev/null +++ b/lib/features/rules/BpmnRules.js @@ -0,0 +1,15 @@ +'use strict'; + +var _ = require('lodash'); + +var DropAction = require('./DropRules'), + ConnectHandler = require('./ConnectRules'); + + +function BpmnRules(rules) { + rules.registerRule('drop', 'validateSubProcess', DropAction.validateSubProcess); +} + +BpmnRules.$inject = [ 'rules' ]; + +module.exports = BpmnRules; diff --git a/lib/features/rules/ConnectRules.js b/lib/features/rules/ConnectRules.js new file mode 100644 index 00000000..5ac460c7 --- /dev/null +++ b/lib/features/rules/ConnectRules.js @@ -0,0 +1,22 @@ +'use strict'; + +var _ = require('lodash'); + + +function can(context) { + + var source = context.source, + target = context.target; + + if (source.labelTarget || target.labelTarget) { + return null; + } + + return source.businessObject.$parent === target.businessObject.$parent && + source.businessObject.$instanceOf('bpmn:FlowNode') && + !source.businessObject.$instanceOf('bpmn:EndEvent') && + !target.businessObject.$instanceOf('bpmn:StartEvent') && + target.businessObject.$instanceOf('bpmn:FlowElement'); +} + +module.exports.can = can; diff --git a/lib/features/rules/DropRules.js b/lib/features/rules/DropRules.js new file mode 100644 index 00000000..2c373593 --- /dev/null +++ b/lib/features/rules/DropRules.js @@ -0,0 +1,26 @@ +'use strict'; + +var _ = require('lodash'); + +function validateSubProcess(context) { + + var target = context.target, + shapes = context.shape, + di = target.businessObject.di; + + // can't drop anything to a collapsed subprocess + if (!di.isExpanded) { + return false; + } + + // Elements that can't be dropped to a subprocess + _.forEach(shapes, function(shape) { + if (_.contains(['bpmn:Participant', 'bpmn:Lane'], shape.type)) { + return false; + } + }); + + return true; +} + +module.exports.validateSubProcess = validateSubProcess; diff --git a/lib/features/rules/index.js b/lib/features/rules/index.js new file mode 100644 index 00000000..2061c19d --- /dev/null +++ b/lib/features/rules/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + __init__: [ 'bpmnRules' ], + bpmnRules: [ 'type', require('./BpmnRules') ] +}; diff --git a/test/fixtures/bpmn/features/drop/drop.bpmn b/test/fixtures/bpmn/features/drop/drop.bpmn new file mode 100644 index 00000000..784bf965 --- /dev/null +++ b/test/fixtures/bpmn/features/drop/drop.bpmn @@ -0,0 +1,41 @@ + + + + + + ID_Sequenceflow_1 + + + ID_Sequenceflow_1 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/bpmn/features/drop/recursive-task.bpmn b/test/fixtures/bpmn/features/drop/recursive-task.bpmn new file mode 100644 index 00000000..39f0c17a --- /dev/null +++ b/test/fixtures/bpmn/features/drop/recursive-task.bpmn @@ -0,0 +1,37 @@ + + + + + + + ID_sequenceflow_1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/ModelerSpec.js b/test/spec/ModelerSpec.js index a2ae611a..1a9df85c 100644 --- a/test/spec/ModelerSpec.js +++ b/test/spec/ModelerSpec.js @@ -146,10 +146,11 @@ describe('modeler', function() { createModeler(xml, function(err, modeler) { expect(modeler.get('bpmnjs')).toBe(modeler); + done(err); }); }); }); -}); \ No newline at end of file +}); diff --git a/test/spec/features/context-pad/ContextPadProviderSpec.js b/test/spec/features/context-pad/ContextPadProviderSpec.js index f97eb9c5..d37f7743 100644 --- a/test/spec/features/context-pad/ContextPadProviderSpec.js +++ b/test/spec/features/context-pad/ContextPadProviderSpec.js @@ -31,5 +31,4 @@ describe('features - context-pad', function() { })); }); - -}); \ No newline at end of file +}); diff --git a/test/spec/features/drop/DropSpec.js b/test/spec/features/drop/DropSpec.js new file mode 100644 index 00000000..56d2da66 --- /dev/null +++ b/test/spec/features/drop/DropSpec.js @@ -0,0 +1,132 @@ +'use strict'; + +var Matchers = require('../../../Matchers'), + TestHelper = require('../../../TestHelper'); + +/* global bootstrapModeler, inject */ + +var _ = require('lodash'); + +var fs = require('fs'); + +var modelingModule = require('../../../../lib/features/modeling'), + dropModule = require('../../../../lib/features/drop'), + coreModule = require('../../../../lib/core'); + + +describe('features/drop ', function() { + + beforeEach(Matchers.addDeepEquals); + + + var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/drop/drop.bpmn', 'utf-8'); + var diagramXML2 = fs.readFileSync('test/fixtures/bpmn/features/drop/recursive-task.bpmn', 'utf-8'); + + var testModules = [ coreModule, modelingModule, dropModule ]; + + + describe('elements', function() { + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); + + it('should update parent', inject(function(elementRegistry, modeling, drop) { + + // given + + var task_1 = elementRegistry.get('ID_Task_1'), + parent = elementRegistry.get('ID_SubProcess_1'); + + // when + modeling.moveShape(task_1, { x: 0, y: 200 }, parent); + + // then + expect(task_1.parent).toBe(parent); + expect(task_1.businessObject.$parent).toBe(parent.businessObject); + })); + + it('should update parents', inject(function(elementRegistry, modeling, drop) { + + // given + var task_1 = elementRegistry.getById('ID_Task_1'), + task_2 = elementRegistry.get('ID_Task_2'), + parent = elementRegistry.getById('ID_SubProcess_1'); + + // when + modeling.moveShapes([ task_1, task_2 ], { x: 0, y: 200 }, parent); + + // then + expect(task_1.parent).toBe(parent); + expect(task_1.businessObject.$parent).toBe(parent.businessObject); + expect(task_2.parent).toBe(parent); + expect(task_2.businessObject.$parent).toBe(parent.businessObject); + })); + }); + + + + describe('Sequence Flows', function() { + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); + + + it('should remove flow if target and source have different parents', + inject(function(elementRegistry, modeling, drop) { + + // given + var task_1 = elementRegistry.get('ID_Task_1'), + parent = elementRegistry.get('ID_SubProcess_1'), + flow = elementRegistry.get('ID_Sequenceflow_1'); + + // when + modeling.moveShape(task_1, { x: 0, y: 200 }, parent); + + + // then + expect(flow.parent).toBe(null); + expect(flow.businessObject.$parent).toBe(null); + })); + + + it('should update flow parent if target and source have same parents', + inject(function(elementRegistry, modeling, drop) { + + // given + var task_1 = elementRegistry.get('ID_Task_1'), + task_2 = elementRegistry.get('ID_Task_2'), + parent = elementRegistry.get('ID_SubProcess_1'), + flow = elementRegistry.get('ID_Sequenceflow_1'); + + // when + modeling.moveShapes([task_1, task_2], { x: 0, y: 250 }, parent); + + // then + expect(flow.parent).toBe(parent); + expect(flow.businessObject.$parent).toBe(parent.businessObject); + })); + }); + + describe('recursion', function() { + + beforeEach(bootstrapModeler(diagramXML2, { modules: testModules })); + + it('should update parent', inject(function(elementRegistry, modeling, drop) { + + // given + + var task_1 = elementRegistry.get('ID_task_1'), + parent = elementRegistry.get('ID_subprocess_1'), + sequenceFlow = elementRegistry.get('ID_sequenceflow_1'); + + // when + modeling.moveShape(task_1, { x: 0, y: 200 }, parent); + + // then + expect(task_1.parent).toBe(parent); + expect(task_1.businessObject.$parent).toBe(parent.businessObject); + + expect(sequenceFlow.parent).toBe(parent); + expect(sequenceFlow.businessObject.$parent).toBe(parent.businessObject); + })); + }); + +}); diff --git a/test/spec/features/modeling/AppendShapeSpec.js b/test/spec/features/modeling/AppendShapeSpec.js index 052ef18e..0b1904cf 100644 --- a/test/spec/features/modeling/AppendShapeSpec.js +++ b/test/spec/features/modeling/AppendShapeSpec.js @@ -459,4 +459,4 @@ describe('features/modeling - append shape', function() { }); -}); \ No newline at end of file +}); diff --git a/test/spec/features/modeling/MoveConnectionSpec.js b/test/spec/features/modeling/MoveConnectionSpec.js index b782883f..63fddf74 100644 --- a/test/spec/features/modeling/MoveConnectionSpec.js +++ b/test/spec/features/modeling/MoveConnectionSpec.js @@ -106,4 +106,4 @@ describe('features/modeling - move connection', function() { }); -}); \ No newline at end of file +}); diff --git a/test/spec/features/modeling/MoveShapeSpec.js b/test/spec/features/modeling/MoveShapeSpec.js index fc65a857..eda0d49c 100644 --- a/test/spec/features/modeling/MoveShapeSpec.js +++ b/test/spec/features/modeling/MoveShapeSpec.js @@ -207,4 +207,4 @@ describe('features/modeling - move shape', function() { }); -}); \ No newline at end of file +});