feat(drop): basic implementation

This commit adds a basic implementation for model drop

* using rules to check whether the drop is allowed
* updating the model after drop

See #127
This commit is contained in:
jdotzki 2014-10-27 12:07:57 +01:00 committed by Nico Rehwaldt
parent c2fe8ec5d9
commit 51918b3493
22 changed files with 441 additions and 27 deletions

View File

@ -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')
];

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
});
}
};

View File

@ -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') ]
};

View File

@ -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) {

View File

@ -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,

View File

@ -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') ],

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,6 @@
'use strict';
module.exports = {
__init__: [ 'bpmnRules' ],
bpmnRules: [ 'type', require('./BpmnRules') ]
};

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.signavio.com/bpmn20"
typeLanguage="http://www.w3.org/2001/XMLSchema"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<process id="sid-a4cd48b2-5b11-4c1a-b7f7-ada3058503ee" isClosed="false" isExecutable="false" processType="None">
<task completionQuantity="1" id="ID_Task_1" isForCompensation="false" startQuantity="1">
<outgoing>ID_Sequenceflow_1</outgoing>
</task>
<task completionQuantity="1" id="ID_Task_2" isForCompensation="false" startQuantity="1">
<incoming>ID_Sequenceflow_1</incoming>
</task>
<subProcess completionQuantity="1" id="ID_SubProcess_1" isForCompensation="false" startQuantity="1" triggeredByEvent="false">
</subProcess>
<sequenceFlow id="ID_Sequenceflow_1" sourceRef="ID_Task_1" targetRef="ID_Task_2">
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="sid-fc24e517-57a5-49e4-8688-13200425c940">
<bpmndi:BPMNPlane bpmnElement="sid-a4cd48b2-5b11-4c1a-b7f7-ada3058503ee" id="sid-dd68914a-715b-49b8-b74a-29880342d6df">
<bpmndi:BPMNShape bpmnElement="ID_Task_1" id="ID_Task_1_gui">
<omgdc:Bounds height="80.0" width="100.0" x="105.0" y="90.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="ID_Task_2" id="ID_Task_2_gui">
<omgdc:Bounds height="80.0" width="100.0" x="345.0" y="90.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="ID_SubProcess_1" id="ID_SubProcess_1_gui" isExpanded="true">
<omgdc:Bounds height="219.0" width="432.0" x="60.0" y="215.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="ID_Sequenceflow_1" id="ID_Sequenceflow_1_gui">
<omgdi:waypoint x="205.0" y="130.0"/>
<omgdi:waypoint x="345.0" y="130.0"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
expressionLanguage="http://www.w3.org/1999/XPath"
id="sid-9462d855-17c5-4105-aa19-c6a95cc4e3be"
typeLanguage="http://www.w3.org/2001/XMLSchema"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<process id="sid-294f3941-0297-48ea-b5d2-24c576587517" isClosed="false" isExecutable="false" processType="None">
<subProcess completionQuantity="1" id="ID_subprocess_1" isForCompensation="false" startQuantity="1" triggeredByEvent="false">
</subProcess>
<task completionQuantity="1" id="ID_task_1" isForCompensation="false" startQuantity="1">
<outgoing>ID_sequenceflow_1</outgoing>
</task>
<sequenceFlow id="ID_sequenceflow_1" sourceRef="ID_task_1" targetRef="ID_task_1">
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="sid-fb46a8c1-9a89-400f-a5c5-e4057ab1d29a">
<bpmndi:BPMNPlane bpmnElement="sid-294f3941-0297-48ea-b5d2-24c576587517" id="sid-c2b161f4-5768-409e-8532-a7037b200d5d">
<bpmndi:BPMNShape bpmnElement="ID_subprocess_1" id="ID_subprocess_1_gui" isExpanded="true">
<omgdc:Bounds height="353.0" width="392.0" x="58.0" y="60.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="ID_task_1" id="ID_task_1_gui">
<omgdc:Bounds height="80.0" width="100.0" x="510.0" y="225.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="ID_sequenceflow_1" id="ID_sequenceflow_1_gui">
<omgdi:waypoint x="560.0" y="305.0"/>
<omgdi:waypoint x="560.0" y="364.0"/>
<omgdi:waypoint x="836.0" y="364.0"/>
<omgdi:waypoint x="836.0" y="265.0"/>
<omgdi:waypoint x="610.0" y="265.0"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

View File

@ -146,6 +146,7 @@ describe('modeler', function() {
createModeler(xml, function(err, modeler) {
expect(modeler.get('bpmnjs')).toBe(modeler);
done(err);
});
});

View File

@ -31,5 +31,4 @@ describe('features - context-pad', function() {
}));
});
});

View File

@ -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);
}));
});
});