feat(copy-paste): disallow pasting of participants on some scenarios

Participants are not allowed to be pasted onto a Process,
if the Process already has other elements.

Closes #526
This commit is contained in:
Ricardo Matias 2016-05-02 08:57:12 +02:00 committed by Nico Rehwaldt
parent 3f04e18398
commit 612b93db2e
3 changed files with 286 additions and 232 deletions

View File

@ -2,18 +2,19 @@
var is = require('../../../util/ModelUtil').is; var is = require('../../../util/ModelUtil').is;
var COLLAB_ERR_MSG = 'flow elements must be children of pools/participants'; var COLLAB_ERR_MSG = 'flow elements must be children of pools/participants',
PROCESS_ERR_MSG = 'participants cannot be pasted onto a non-empty process diagram';
function ModelingFeedback(eventBus, tooltips, translate) { function ModelingFeedback(eventBus, tooltips, translate) {
function showError(position, message) { function showError(position, message, timeout) {
tooltips.add({ tooltips.add({
position: { position: {
x: position.x + 5, x: position.x + 5,
y: position.y + 5 y: position.y + 5
}, },
type: 'error', type: 'error',
timeout: 2000, timeout: timeout || 2000,
html: '<div>' + message + '</div>' html: '<div>' + message + '</div>'
}); });
} }
@ -36,6 +37,10 @@ function ModelingFeedback(eventBus, tooltips, translate) {
if (is(target, 'bpmn:Collaboration')) { if (is(target, 'bpmn:Collaboration')) {
showError(position, translate(COLLAB_ERR_MSG)); showError(position, translate(COLLAB_ERR_MSG));
} }
if (is(target, 'bpmn:Process')) {
showError(position, translate(PROCESS_ERR_MSG), 3000);
}
}); });
} }

View File

@ -427,7 +427,8 @@ function canDrop(element, target, position) {
} }
function canPaste(tree, target) { function canPaste(tree, target) {
var topLevel = tree[0]; var topLevel = tree[0],
participants;
if (is(target, 'bpmn:Collaboration')) { if (is(target, 'bpmn:Collaboration')) {
return every(topLevel, function(e) { return every(topLevel, function(e) {
@ -435,6 +436,14 @@ function canPaste(tree, target) {
}); });
} }
if (is(target, 'bpmn:Process')) {
participants = any(topLevel, function(e) {
return e.type === 'bpmn:Participant';
});
return !(participants && target.children.length > 0);
}
// disallow to create elements on collapsed pools // disallow to create elements on collapsed pools
if (is(target, 'bpmn:Participant') && !isExpanded(target)) { if (is(target, 'bpmn:Participant') && !isExpanded(target)) {
return false; return false;

View File

@ -48,7 +48,7 @@ describe('features/copy-paste', function() {
collaborationMultipleXML = require('../../../fixtures/bpmn/features/copy-paste/collaboration-multiple.bpmn'); collaborationMultipleXML = require('../../../fixtures/bpmn/features/copy-paste/collaboration-multiple.bpmn');
var integrationTest = function(ids) { function integrationTest(ids) {
return function(canvas, elementRegistry, modeling, copyPaste, commandStack) { return function(canvas, elementRegistry, modeling, copyPaste, commandStack) {
// given // given
var shapes = elementRegistry.getAll(), var shapes = elementRegistry.getAll(),
@ -127,280 +127,320 @@ describe('features/copy-paste', function() {
}; };
}; };
describe('basic diagram', function() {
beforeEach(bootstrapModeler(basicXML, { modules: testModules })); describe('basic diagram', function() {
describe('copy', function() { beforeEach(bootstrapModeler(basicXML, { modules: testModules }));
it('selected elements', inject(function(elementRegistry, copyPaste) { describe('copy', function() {
// given it('selected elements', inject(function(elementRegistry, copyPaste) {
var subProcess, startEvent, boundaryEvent, textAnnotation, conditionalFlow, defaultFlow,
tree;
// when // given
copyPaste.copy(elementRegistry.get('SubProcess_1kd6ist')); var subProcess, startEvent, boundaryEvent, textAnnotation, conditionalFlow, defaultFlow,
tree;
tree = new DescriptorTree(copyPaste._tree); // when
copyPaste.copy(elementRegistry.get('SubProcess_1kd6ist'));
startEvent = tree.getElement('StartEvent_1'); tree = new DescriptorTree(copyPaste._tree);
boundaryEvent = tree.getElement('BoundaryEvent_1c94bi9');
subProcess = tree.getElement('SubProcess_1kd6ist');
textAnnotation = tree.getElement('TextAnnotation_0h1hhgg');
conditionalFlow = tree.getElement('SequenceFlow_07vo2r8');
defaultFlow = tree.getElement('Task_1fo63a7');
// then startEvent = tree.getElement('StartEvent_1');
expect(tree.getLength()).to.equal(3); boundaryEvent = tree.getElement('BoundaryEvent_1c94bi9');
subProcess = tree.getElement('SubProcess_1kd6ist');
textAnnotation = tree.getElement('TextAnnotation_0h1hhgg');
conditionalFlow = tree.getElement('SequenceFlow_07vo2r8');
defaultFlow = tree.getElement('Task_1fo63a7');
expect(tree.getDepthLength(0)).to.equal(1); // then
expect(tree.getDepthLength(1)).to.equal(3); expect(tree.getLength()).to.equal(3);
expect(tree.getDepthLength(2)).to.equal(15);
expect(subProcess.isExpanded).to.be.true; expect(tree.getDepthLength(0)).to.equal(1);
expect(startEvent.name).to.equal('hello'); expect(tree.getDepthLength(1)).to.equal(3);
expect(textAnnotation.text).to.equal('foo'); expect(tree.getDepthLength(2)).to.equal(15);
expect(boundaryEvent.eventDefinitions).to.contain('bpmn:TimerEventDefinition');
}));
}); expect(subProcess.isExpanded).to.be.true;
expect(startEvent.name).to.equal('hello');
describe('integration', function() { expect(textAnnotation.text).to.equal('foo');
expect(boundaryEvent.eventDefinitions).to.contain('bpmn:TimerEventDefinition');
it('should retain label\'s relative position', }));
inject(function(modeling, copyPaste, canvas, elementRegistry) {
// given
var startEvent = elementRegistry.get('StartEvent_1'),
startEventLabel = startEvent.label,
seqFlow = elementRegistry.get('SequenceFlow_1rtr33r'),
seqFlowLabel = seqFlow.label,
task = elementRegistry.get('Task_1fo63a7'),
rootElement = canvas.getRootElement(),
newStrtEvt, newSeqFlow;
// when
copyPaste.copy([ startEvent, task ]);
copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
newStrtEvt = elementRegistry.filter(function(element) {
return element.parent === rootElement && element.type === 'bpmn:StartEvent';
})[0];
newSeqFlow = elementRegistry.filter(function(element) {
return element.parent === rootElement && element.type === 'bpmn:SequenceFlow';
})[0];
// then
expect(newStrtEvt.label.x - newStrtEvt.x).to.equal(startEventLabel.x - startEvent.x);
expect(newStrtEvt.label.y - newStrtEvt.y).to.equal(startEventLabel.y - startEvent.y);
expect(newSeqFlow.label.x - newSeqFlow.waypoints[0].x).to.equal(seqFlowLabel.x - seqFlow.waypoints[0].x);
expect(newSeqFlow.label.y - newSeqFlow.waypoints[0].y).to.equal(seqFlowLabel.y - seqFlow.waypoints[0].y);
}));
it('should retain default & conditional flow property',
inject(function(elementRegistry, copyPaste, canvas, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_1kd6ist'),
rootElement = canvas.getRootElement(),
task, defaultFlow, conditionalFlow;
// when
copyPaste.copy(subProcess);
modeling.removeElements([ subProcess ]);
copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
task = elementRegistry.filter(function(element) {
return element.type === 'bpmn:Task';
})[0];
defaultFlow = elementRegistry.filter(function(element) {
return !!(element.type === 'bpmn:SequenceFlow' && task.businessObject.default.id === element.id);
})[0];
conditionalFlow = elementRegistry.filter(function(element) {
return !!(element.type === 'bpmn:SequenceFlow' && element.businessObject.conditionExpression);
})[0];
expect(defaultFlow).to.exist;
expect(conditionalFlow).to.exist;
}));
it('should retain loop characteristics',
inject(function(elementRegistry, copyPaste, canvas, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_0gev7mx'),
rootElement = canvas.getRootElement(),
loopCharacteristics;
// when
copyPaste.copy(subProcess);
modeling.removeElements([ subProcess ]);
copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
subProcess = elementRegistry.filter(function(element) {
return !!(element.id !== 'SubProcess_1kd6ist' && element.type === 'bpmn:SubProcess');
})[0];
loopCharacteristics = subProcess.businessObject.loopCharacteristics;
expect(loopCharacteristics.$type).to.equal('bpmn:MultiInstanceLoopCharacteristics');
expect(loopCharacteristics.isSequential).to.be.true;
}));
it('selected elements', inject(integrationTest([ 'SubProcess_1kd6ist' ])));
});
describe('rules', function() {
it('disallow individual boundary events copying', inject(function(copyPaste, elementRegistry, canvas) {
var boundaryEventA = elementRegistry.get('BoundaryEvent_1404oxd'),
boundaryEventB = elementRegistry.get('BoundaryEvent_1c94bi9'),
tree;
// when
copyPaste.copy([ boundaryEventA, boundaryEventB ]);
tree = new DescriptorTree(copyPaste._tree);
expect(tree.getLength()).to.equal(0);
}));
});
}); });
describe('basic collaboration', function() {
beforeEach(bootstrapModeler(collaborationXML, { modules: testModules })); describe('integration', function() {
describe('integration', function() { it('should retain label\'s relative position',
inject(function(modeling, copyPaste, canvas, elementRegistry) {
// given
var startEvent = elementRegistry.get('StartEvent_1'),
startEventLabel = startEvent.label,
seqFlow = elementRegistry.get('SequenceFlow_1rtr33r'),
seqFlowLabel = seqFlow.label,
task = elementRegistry.get('Task_1fo63a7'),
rootElement = canvas.getRootElement(),
newStrtEvt, newSeqFlow;
it('participant with including lanes + elements', inject(integrationTest([ 'Participant_0uu1rvj' ]))); // when
copyPaste.copy([ startEvent, task ]);
it('collapsed pool', inject(integrationTest([ 'Participant_145muai' ]))); copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
}); newStrtEvt = elementRegistry.filter(function(element) {
return element.parent === rootElement && element.type === 'bpmn:StartEvent';
})[0];
describe('rules', function () { newSeqFlow = elementRegistry.filter(function(element) {
return element.parent === rootElement && element.type === 'bpmn:SequenceFlow';
})[0];
it('disallow individual lanes copying', inject(function(copyPaste, elementRegistry, canvas) { // then
expect(newStrtEvt.label.x - newStrtEvt.x).to.equal(startEventLabel.x - startEvent.x);
expect(newStrtEvt.label.y - newStrtEvt.y).to.equal(startEventLabel.y - startEvent.y);
var laneA = elementRegistry.get('Lane_13h648l'), expect(newSeqFlow.label.x - newSeqFlow.waypoints[0].x).to.equal(seqFlowLabel.x - seqFlow.waypoints[0].x);
laneB = elementRegistry.get('Lane_1gl63sa'), expect(newSeqFlow.label.y - newSeqFlow.waypoints[0].y).to.equal(seqFlowLabel.y - seqFlow.waypoints[0].y);
tree; }));
// when
copyPaste.copy([ laneA, laneB ]);
tree = new DescriptorTree(copyPaste._tree);
expect(tree.getLength()).to.equal(0);
}));
it('pasting on a collaboration is disallowed when NOT every element is a Participant', it('should retain default & conditional flow property',
inject(function(copyPaste, elementRegistry, canvas, tooltips, eventBus) { inject(function(elementRegistry, copyPaste, canvas, modeling) {
var task = elementRegistry.get('Task_13xbgyg'), // given
participant = elementRegistry.get('Participant_145muai'), var subProcess = elementRegistry.get('SubProcess_1kd6ist'),
collaboration = canvas.getRootElement(), rootElement = canvas.getRootElement(),
tree; task, defaultFlow, conditionalFlow;
var pasteRejected = sinon.spy(function() {}); // when
copyPaste.copy(subProcess);
// when modeling.removeElements([ subProcess ]);
copyPaste.copy([ task, participant ]);
tree = new DescriptorTree(copyPaste._tree); copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
// then task = elementRegistry.filter(function(element) {
expect(tree.getDepthLength(0)).to.equal(2); return element.type === 'bpmn:Task';
})[0];
// when defaultFlow = elementRegistry.filter(function(element) {
eventBus.on('elements.paste.rejected', pasteRejected); return !!(element.type === 'bpmn:SequenceFlow' && task.businessObject.default.id === element.id);
})[0];
copyPaste.paste({ conditionalFlow = elementRegistry.filter(function(element) {
element: collaboration, return !!(element.type === 'bpmn:SequenceFlow' && element.businessObject.conditionExpression);
point: { })[0];
x: 1000,
y: 1000
}
});
expect(pasteRejected).to.have.been.called; expect(defaultFlow).to.exist;
})); expect(conditionalFlow).to.exist;
}));
});
it('should retain loop characteristics',
inject(function(elementRegistry, copyPaste, canvas, modeling) {
// given
var subProcess = elementRegistry.get('SubProcess_0gev7mx'),
rootElement = canvas.getRootElement(),
loopCharacteristics;
// when
copyPaste.copy(subProcess);
modeling.removeElements([ subProcess ]);
copyPaste.paste({
element: rootElement,
point: {
x: 1100,
y: 250
}
});
subProcess = elementRegistry.filter(function(element) {
return !!(element.id !== 'SubProcess_1kd6ist' && element.type === 'bpmn:SubProcess');
})[0];
loopCharacteristics = subProcess.businessObject.loopCharacteristics;
expect(loopCharacteristics.$type).to.equal('bpmn:MultiInstanceLoopCharacteristics');
expect(loopCharacteristics.isSequential).to.be.true;
}));
it('selected elements', inject(integrationTest([ 'SubProcess_1kd6ist' ])));
}); });
describe('complex collaboration', function() {
beforeEach(bootstrapModeler(collaborationMultipleXML, { modules: testModules })); describe('rules', function() {
describe('basics', function() { it('disallow individual boundary events copying', inject(function(copyPaste, elementRegistry, canvas) {
it('pasting on lane', inject(function(elementRegistry, copyPaste) { var boundaryEventA = elementRegistry.get('BoundaryEvent_1404oxd'),
// given boundaryEventB = elementRegistry.get('BoundaryEvent_1c94bi9'),
var lane = elementRegistry.get('Lane_1yo0kyz'), tree;
task = elementRegistry.get('Task_0n0k2nj'),
participant = elementRegistry.get('Participant_0pgdgt4');
// when // when
copyPaste.copy(task); copyPaste.copy([ boundaryEventA, boundaryEventB ]);
copyPaste.paste({ tree = new DescriptorTree(copyPaste._tree);
element: lane,
point: {
x: 200,
y: 75
}
});
// then expect(tree.getLength()).to.equal(0);
expect(lane.children).to.be.empty; }));
expect(lane.businessObject.flowNodeRef).to.have.length(2); });
expect(lane.parent.children).to.have.length(2); });
expect(participant.children).to.have.length(5);
}));
});
describe('integration', function() { describe('basic collaboration', function() {
it('multiple participants', inject(integrationTest([ 'Participant_0pgdgt4', 'Participant_1id96b4' ]))); beforeEach(bootstrapModeler(collaborationXML, { modules: testModules }));
}); describe('integration', function() {
it('participant with including lanes + elements', inject(integrationTest([ 'Participant_0uu1rvj' ])));
it('collapsed pool', inject(integrationTest([ 'Participant_145muai' ])));
}); });
describe('rules', function () {
it('disallow individual lanes copying', inject(function(copyPaste, elementRegistry, canvas) {
var laneA = elementRegistry.get('Lane_13h648l'),
laneB = elementRegistry.get('Lane_1gl63sa'),
tree;
// when
copyPaste.copy([ laneA, laneB ]);
tree = new DescriptorTree(copyPaste._tree);
expect(tree.getLength()).to.equal(0);
}));
it('pasting on a collaboration is disallowed when NOT every element is a Participant',
inject(function(copyPaste, elementRegistry, canvas, tooltips, eventBus) {
var task = elementRegistry.get('Task_13xbgyg'),
participant = elementRegistry.get('Participant_145muai'),
collaboration = canvas.getRootElement(),
tree;
var pasteRejected = sinon.spy(function() {});
// when
copyPaste.copy([ task, participant ]);
tree = new DescriptorTree(copyPaste._tree);
// then
expect(tree.getDepthLength(0)).to.equal(2);
// when
eventBus.on('elements.paste.rejected', pasteRejected);
copyPaste.paste({
element: collaboration,
point: {
x: 1000,
y: 1000
}
});
expect(pasteRejected).to.have.been.called;
}));
it('pasting participants on a process is disallowed when it\'s not a collaboration',
inject(function(copyPaste, elementRegistry, canvas, tooltips, eventBus, modeling, elementFactory) {
var participant = elementRegistry.get('Participant_145muai'),
otherParticipant = elementRegistry.get('Participant_0uu1rvj'),
startEvent = elementFactory.create('shape', { type: 'bpmn:StartEvent' }),
rootElement;
var pasteRejected = sinon.spy(function() {});
// when
copyPaste.copy([ participant ]);
modeling.removeElements([ participant, otherParticipant ]);
rootElement = canvas.getRootElement();
modeling.createShape(startEvent, { x: 50, y: 50 }, rootElement);
eventBus.on('elements.paste.rejected', pasteRejected);
copyPaste.paste({
element: rootElement,
point: {
x: 500,
y: 200
}
});
expect(pasteRejected).to.have.been.called;
}));
});
});
describe('complex collaboration', function() {
beforeEach(bootstrapModeler(collaborationMultipleXML, { modules: testModules }));
describe('basics', function() {
it('pasting on lane', inject(function(elementRegistry, copyPaste) {
// given
var lane = elementRegistry.get('Lane_1yo0kyz'),
task = elementRegistry.get('Task_0n0k2nj'),
participant = elementRegistry.get('Participant_0pgdgt4');
// when
copyPaste.copy(task);
copyPaste.paste({
element: lane,
point: {
x: 200,
y: 75
}
});
// then
expect(lane.children).to.be.empty;
expect(lane.businessObject.flowNodeRef).to.have.length(2);
expect(lane.parent.children).to.have.length(2);
expect(participant.children).to.have.length(5);
}));
});
describe('integration', function() {
it('multiple participants', inject(integrationTest([ 'Participant_0pgdgt4', 'Participant_1id96b4' ])));
});
});
}); });