feat(features/bpmn-modeling): implement appendNode

Related to #6
This commit is contained in:
Nico Rehwaldt 2014-06-30 17:03:35 +02:00
parent 5185c55f68
commit 4fe5bbc0f5
8 changed files with 515 additions and 1 deletions

View File

@ -0,0 +1,91 @@
'use strict';
var _ = require('lodash');
var BpmnModdle = require('bpmn-moddle');
function BpmnFactory() {
this._model = BpmnModdle.instance();
this._uuid = 1;
}
BpmnFactory.$inject = [ ];
BpmnFactory.prototype._ensureId = function(element) {
if (element.id === undefined) {
element.id = '' + (++this._uuid);
}
};
BpmnFactory.prototype.create = function(type, attrs) {
var element = this._model.create(type, attrs);
this._ensureId(element);
return element;
};
BpmnFactory.prototype.createDiShape = function(semantic, position, attrs) {
position = position || { x: 0, y: 0 };
var bounds;
if (semantic.$instanceOf('bpmn:Task')) {
bounds = { width: 100, height: 80 };
} else {
bounds = { width: 50, height: 50 };
}
_.extend(bounds, {
x: (position.x || 0) - bounds.width / 2,
y: (position.y || 0) - bounds.height / 2
});
return this.create('bpmndi:BPMNShape', _.extend({
bpmnElement: semantic,
bounds: this.createDiBounds(bounds)
}, attrs));
};
BpmnFactory.prototype.createDiBounds = function(bounds) {
return this.create('dc:Bounds', bounds);
};
BpmnFactory.prototype.createDiWaypoint = function(point) {
return this.create('dc:Point', point);
};
BpmnFactory.prototype.createDiEdge = function(sequenceFlow, points, attrs) {
var sourceDi = sequenceFlow.sourceRef.di,
targetDi = sequenceFlow.targetRef.di;
var waypoints = _.map(points, function(pos) {
return this.createDiWaypoint(pos);
}, this);
return this.create('bpmndi:BPMNEdge', _.extend({
bpmnElement: sequenceFlow,
waypoint: waypoints
}, attrs));
};
BpmnFactory.prototype.createSequenceFlow = function(source, target, attrs) {
var sequenceFlow = this.create('bpmn:SequenceFlow', _.extend({
sourceRef: source,
targetRef: target
}, attrs));
source.get('outgoing').push(sequenceFlow);
target.get('incoming').push(sequenceFlow);
return sequenceFlow;
};
module.exports = BpmnFactory;

View File

@ -0,0 +1,37 @@
'use strict';
var AppendFlowNodeHandler = require('./cmd/AppendFlowNodeHandler');
/**
* BPMN 2.0 modeling features activator
*
* @param {CommandStack} commandStack
*/
function BpmnModeling(commandStack) {
commandStack.registerHandler('shape.appendNode', AppendFlowNodeHandler);
this._commandStack = commandStack;
}
BpmnModeling.$inject = [ 'commandStack' ];
/**
* Append a flow node to the element with the given source
* at the specified position.
*/
BpmnModeling.prototype.appendFlowNode = function(source, parent, type, position) {
var context = {
source: source,
type: type,
parent: parent,
position: position
};
this._commandStack.execute('shape.appendNode', context);
return context.target;
};
module.exports = BpmnModeling;

View File

@ -0,0 +1,15 @@
'use strict';
var getMidPoint = module.exports.getMidPoint = function(bounds) {
return {
x: bounds.x + bounds.width / 2,
y: bounds.y + bounds.height / 2
};
};
module.exports.getDirectConnectionPoints = function(boundsA, boundsB) {
return [
getMidPoint(boundsA),
getMidPoint(boundsB)
];
};

View File

@ -0,0 +1,142 @@
'use strict';
var LayoutUtil = require('../LayoutUtil');
var AppendShapeHandler = require('diagram-js/lib/features/modeling/cmd/AppendShapeHandler');
var Refs = require('object-refs');
var diRefs = new Refs({ name: 'bpmnElement', enumerable: true }, { name: 'di' });
function AppendFlowNodeHandler(canvas, bpmnFactory, bpmnImporter) {
AppendShapeHandler.call(this);
this._bpmnImporter = bpmnImporter;
this._bpmnFactory = bpmnFactory;
this._canvas = canvas;
}
AppendFlowNodeHandler.prototype = Object.create(AppendShapeHandler.prototype);
AppendFlowNodeHandler.$inject = [ 'canvas', 'bpmnFactory', 'bpmnImporter' ];
AppendFlowNodeHandler.prototype.createShape = function(source, position, parent, context) {
var sourceSemantic = source.businessObject,
parentSemantic = parent.businessObject;
var target = context.target,
targetSemantic,
targetDi;
// create semantic
targetSemantic = this._bpmnFactory.create(context.type, {
id: target && target.businessObject.id
});
// add to model
parentSemantic.get('flowElements').push(targetSemantic);
targetSemantic.$parent = parentSemantic;
// create di
targetDi = this._bpmnFactory.createDiShape(targetSemantic, position, {
id: targetSemantic.id + '_di'
});
diRefs.bind(targetSemantic, 'di');
targetSemantic.di = targetDi;
// add to model
sourceSemantic.di.$parent.get('planeElement').push(targetDi);
targetDi.$parent = sourceSemantic.di.$parent;
return this._bpmnImporter.add(targetSemantic, parent);
};
AppendFlowNodeHandler.prototype.createConnection = function(source, target, parent, context) {
var sourceSemantic = source.businessObject,
targetSemantic = target.businessObject,
parentSemantic = parent.businessObject;
var flowSemantic,
flowDi;
var connection = context.connection;
// create semantic
flowSemantic = this._bpmnFactory.createSequenceFlow(sourceSemantic, targetSemantic, {
id: connection && connection.businessObject.id
});
// add to model
parentSemantic.get('flowElements').push(flowSemantic);
flowSemantic.$parent = parentSemantic;
// create di
var waypoints = LayoutUtil.getDirectConnectionPoints(sourceSemantic.di.bounds, targetSemantic.di.bounds);
flowDi = this._bpmnFactory.createDiEdge(flowSemantic, waypoints, {
id: flowSemantic.id + '_di'
});
diRefs.bind(flowSemantic, 'di');
flowSemantic.di = flowDi;
// add to model
sourceSemantic.di.$parent.get('planeElement').push(flowDi);
flowDi.$parent = sourceSemantic.di.$parent;
return this._bpmnImporter.add(flowSemantic, parent);
};
AppendFlowNodeHandler.prototype.removeShape = function(shape) {
var semantic = shape.businessObject,
parentSemantic = semantic.$parent;
// remove semantic
parentSemantic.get('flowElements').splice(parentSemantic.get('flowElements').indexOf(semantic), 1);
semantic.$parent = null;
// remove di
var di = semantic.di;
di.$parent.get('planeElement').splice(di.$parent.get('planeElement').indexOf(di), 1);
di.$parent = null;
// actual remove shape
return this._canvas.removeShape(shape);
};
AppendFlowNodeHandler.prototype.removeConnection = function(connection) {
var semantic = connection.businessObject,
parentSemantic = semantic.$parent;
// remove semantic
parentSemantic.get('flowElements').splice(parentSemantic.get('flowElements').indexOf(semantic), 1);
// remove di
var di = semantic.di;
di.$parent.get('planeElement').splice(di.$parent.get('planeElement').indexOf(di), 1);
di.$parent = null;
// remove refs in source / target
semantic.sourceRef.outgoing.splice(semantic.sourceRef.outgoing.indexOf(semantic), 1);
semantic.targetRef.incoming.splice(semantic.targetRef.incoming.indexOf(semantic), 1);
semantic.sourceRef = null;
semantic.targetRef = null;
semantic.$parent = null;
// actual remove connection
return this._canvas.removeConnection(connection);
};
module.exports = AppendFlowNodeHandler;

View File

@ -0,0 +1,9 @@
module.exports = {
__init__: [ 'bpmnModeling' ],
__depends__: [
require('../../core'),
require('diagram-js/lib/cmd')
],
bpmnFactory: [ 'type', require('./BpmnFactory') ],
bpmnModeling: [ 'type', require('./BpmnModeling') ]
};

View File

@ -59,6 +59,7 @@
"didi": "~0.0.4",
"jquery": "~2.1.0",
"jquery-mousewheel": "~3.1.11",
"lodash": "~2.4.0"
"lodash": "~2.4.0",
"object-refs": "^0.1.0"
}
}

View File

@ -0,0 +1,56 @@
'use strict';
var Matchers = require('../../../Matchers'),
TestHelper = require('../../../TestHelper');
/* global bootstrapBpmnJS, inject */
var fs = require('fs');
var bpmnFactoryModule = require('../../../../../lib/features/bpmn-modeling');
xdescribe('features - bpmn-factory', function() {
beforeEach(Matchers.add);
var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8');
var testModules = [ bpmnFactoryModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));
describe('create task', function() {
it('should create', inject(function(bpmnFactory) {
var result = bpmnFactory.createNode('bpmn:Task');
expect(result.semantic.id).toBeDefined();
expect(result.di.id).toBeDefined();
expect(result.di.bounds.width).toBe(100);
expect(result.di.bounds.height).toBe(80);
expect(result.di.bounds.x).toBe(-50);
expect(result.di.bounds.y).toBe(-40);
}));
it('should create with position', inject(function(bpmnFactory) {
var result = bpmnFactory.createNode('bpmn:Task', { x: 100, y: 100 });
expect(result.semantic.id).toBeDefined();
expect(result.di.id).toBeDefined();
expect(result.di.bounds.width).toBe(100);
expect(result.di.bounds.height).toBe(80);
expect(result.di.bounds.x).toBe(50);
expect(result.di.bounds.y).toBe(60);
}));
});
});

View File

@ -0,0 +1,163 @@
'use strict';
var Matchers = require('../../../Matchers'),
TestHelper = require('../../../TestHelper');
/* global bootstrapBpmnJS, inject */
var _ = require('lodash');
var fs = require('fs');
var bpmnFactoryModule = require('../../../../../lib/features/bpmn-modeling'),
bpmnDrawModule = require('../../../../../lib/draw');
describe('features - bpmn-modeling', function() {
beforeEach(Matchers.add);
var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8');
var testModules = [ bpmnFactoryModule, bpmnDrawModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));
describe('commands', function() {
describe('shape.appendNode', function() {
it('should execute', inject(function(elementRegistry, bpmnModeling) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
// when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
// then
expect(targetShape).toBeDefined();
expect(target.$instanceOf('bpmn:Task')).toBe(true);
}));
it('should create DI', inject(function(elementRegistry, bpmnModeling) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
// when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
// then
expect(target.di).toBeDefined();
expect(target.di.$parent).toBe(startEvent.di.$parent);
}));
it('should add to parent (sub process)', inject(function(elementRegistry, bpmnModeling) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
// when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
// then
expect(subProcess.get('flowElements')).toContain(target);
}));
it('should add connection', inject(function(elementRegistry, bpmnModeling) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
// when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) {
return e.sourceRef === startEvent && e.targetRef === target;
});
// then
expect(connection).toBeDefined();
expect(connection.$instanceOf('bpmn:SequenceFlow')).toBe(true);
}));
describe('undo support', function() {
it('should undo add to parent', inject(function(elementRegistry, bpmnModeling, commandStack) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
// when
commandStack.undo();
// then
expect(subProcess.get('flowElements')).not.toContain(target);
expect(subProcess.di.$parent.get('planeElement')).not.toContain(target.di);
}));
it('should undo add connection', inject(function(elementRegistry, bpmnModeling, commandStack) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'),
target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) {
return e.sourceRef === startEvent && e.targetRef === target;
});
// when
commandStack.undo();
// then
expect(connection.sourceRef).toBe(null);
expect(connection.targetRef).toBe(null);
expect(connection.$parent).toBe(null);
expect(subProcess.di.$parent.get('planeElement')).not.toContain(connection.di);
}));
});
});
});
});