feat(create): create multiple elements

* add <elements.create> rule for creating multiple elements
* handle creating multiple elements in CreateParticipantBehavior
* create sub process with start event through palette
This commit is contained in:
Philipp Fromme 2019-08-06 18:57:30 +02:00 committed by merge-me[bot]
parent 1f706fd9b8
commit 14bf3a32ee
11 changed files with 425 additions and 144 deletions

View File

@ -158,7 +158,9 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) {
function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, element);
create.start(event, shape, {
source: element
});
}

View File

@ -1,6 +1,7 @@
import {
assign,
forEach
forEach,
isObject
} from 'min-dash';
import inherits from 'inherits';
@ -164,10 +165,10 @@ ElementFactory.prototype._getDefaultSize = function(semantic) {
}
if (is(semantic, 'bpmn:Participant')) {
if (!isExpanded(semantic)) {
return { width: 400, height: 60 };
} else {
if (isExpanded(semantic)) {
return { width: 600, height: 250 };
} else {
return { width: 400, height: 60 };
}
}
@ -195,11 +196,23 @@ ElementFactory.prototype._getDefaultSize = function(semantic) {
};
ElementFactory.prototype.createParticipantShape = function(collapsed) {
/**
* Create participant.
*
* @param {boolean|Object} [attrs] attrs
*
* @returns {djs.model.Shape}
*/
ElementFactory.prototype.createParticipantShape = function(attrs) {
var attrs = { type: 'bpmn:Participant' };
if (!isObject(attrs)) {
attrs = { isExpanded: attrs };
}
if (!collapsed) {
attrs = assign({ type: 'bpmn:Participant' }, attrs || {});
// participants are expanded by default
if (attrs.isExpanded !== false) {
attrs.processRef = this._bpmnFactory.create('bpmn:Process');
}

View File

@ -8,7 +8,10 @@ import { isLabel } from '../../../util/LabelUtil';
import { getBBox } from 'diagram-js/lib/util/Elements';
import { assign } from 'min-dash';
import {
assign,
find
} from 'min-dash';
import { asTRBL } from 'diagram-js/lib/layout/LayoutUtil';
@ -85,71 +88,122 @@ export default function CreateParticipantBehavior(canvas, eventBus, modeling) {
}
});
// turn process into collaboration before adding participant
this.preExecute('shape.create', function(context) {
function ensureCollaboration(context) {
var parent = context.parent,
shape = context.shape,
position = context.position;
collaboration;
var rootElement = canvas.getRootElement();
if (
is(parent, 'bpmn:Process') &&
is(shape, 'bpmn:Participant') &&
!is(rootElement, 'bpmn:Collaboration')
) {
if (is(rootElement, 'bpmn:Collaboration')) {
collaboration = rootElement;
} else {
// this is going to detach the process root
// and set the returned collaboration element
// as the new root element
var collaborationElement = modeling.makeCollaboration();
// update root element by making collaboration
collaboration = modeling.makeCollaboration();
// monkey patch the create context
// so that the participant is being dropped
// onto the new collaboration root instead
context.position = position;
context.parent = collaborationElement;
// re-use process when creating first participant
context.process = parent;
}
context.processRoot = parent;
context.parent = collaboration;
}
// turn process into collaboration before adding participant
this.preExecute('shape.create', function(context) {
var parent = context.parent,
shape = context.shape;
if (is(shape, 'bpmn:Participant') && is(parent, 'bpmn:Process')) {
ensureCollaboration(context);
}
}, true);
this.execute('shape.create', function(context) {
var processRoot = context.processRoot,
var process = context.process,
shape = context.shape;
if (processRoot) {
if (process) {
context.oldProcessRef = shape.businessObject.processRef;
// assign the participant processRef
shape.businessObject.processRef = processRoot.businessObject;
// re-use process when creating first participant
shape.businessObject.processRef = process.businessObject;
}
}, true);
this.revert('shape.create', function(context) {
var processRoot = context.processRoot,
var process = context.process,
shape = context.shape;
if (processRoot) {
if (process) {
// assign the participant processRef
// re-use process when creating first participant
shape.businessObject.processRef = context.oldProcessRef;
}
}, true);
this.postExecute('shape.create', function(context) {
var processRoot = context.processRoot,
var process = context.process,
shape = context.shape;
if (processRoot) {
if (process) {
// process root is already detached at this point
var processChildren = processRoot.children.slice();
// move children from process to participant
var processChildren = process.children.slice();
modeling.moveElements(processChildren, { x: 0, y: 0 }, shape);
}
}, true);
// turn process into collaboration when creating participants
this.preExecute('elements.create', HIGH_PRIORITY, function(context) {
var elements = context.elements,
parent = context.parent,
participant;
var hasParticipants = findParticipant(elements);
if (hasParticipants && is(parent, 'bpmn:Process')) {
ensureCollaboration(context);
participant = findParticipant(elements);
context.oldProcessRef = participant.businessObject.processRef;
// re-use process when creating first participant
participant.businessObject.processRef = parent.businessObject;
}
}, true);
this.revert('elements.create', function(context) {
var elements = context.elements,
process = context.process,
participant;
if (process) {
participant = findParticipant(elements);
// re-use process when creating first participant
participant.businessObject.processRef = context.oldProcessRef;
}
}, true);
this.postExecute('elements.create', function(context) {
var elements = context.elements,
process = context.process,
participant;
if (process) {
participant = findParticipant(elements);
// move children from process to first participant
var processChildren = process.children.slice();
modeling.moveElements(processChildren, { x: 0, y: 0 }, participant);
}
}, true);
}
CreateParticipantBehavior.$inject = [
@ -168,9 +222,14 @@ function getParticipantBounds(shape, childrenBBox) {
height: childrenBBox.height + VERTICAL_PARTICIPANT_PADDING * 2
};
var width = Math.max(shape.width, childrenBBox.width),
height = Math.max(shape.height, childrenBBox.height);
return {
width: Math.max(shape.width, childrenBBox.width),
height: Math.max(shape.height, childrenBBox.height)
x: -width / 2,
y: -height / 2,
width: width,
height: height
};
}
@ -187,4 +246,10 @@ function getParticipantCreateConstraints(shape, childrenBBox) {
function isConnection(element) {
return !!element.waypoints;
}
function findParticipant(elements) {
return find(elements, function(element) {
return is(element, 'bpmn:Participant');
});
}

View File

@ -6,37 +6,17 @@ import { is } from '../../../util/ModelUtil';
import { isExpanded } from '../../../util/DiUtil.js';
/**
* Add start event child by default when creating an expanded subprocess
* with create.start or replacing a task with an expanded subprocess.
* Add start event replacing element with expanded sub process.
*
* @param {Injector} injector
* @param {Modeling} modeling
*/
export default function SubProcessStartEventBehavior(eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
eventBus.on('create.start', function(event) {
var shape = event.context.shape,
hints = event.context.hints;
hints.shouldAddStartEvent = is(shape, 'bpmn:SubProcess') && isExpanded(shape);
});
this.postExecuted('shape.create', function(event) {
var shape = event.context.shape,
hints = event.context.hints,
position;
if (!hints.shouldAddStartEvent) {
return;
}
position = calculatePositionRelativeToShape(shape);
modeling.createShape({ type: 'bpmn:StartEvent' }, position, shape);
});
export default function SubProcessStartEventBehavior(injector, modeling) {
injector.invoke(CommandInterceptor, this);
this.postExecuted('shape.replace', function(event) {
var oldShape = event.context.oldShape,
newShape = event.context.newShape,
position;
newShape = event.context.newShape;
if (
!is(newShape, 'bpmn:SubProcess') ||
@ -46,14 +26,14 @@ export default function SubProcessStartEventBehavior(eventBus, modeling) {
return;
}
position = calculatePositionRelativeToShape(newShape);
var position = getStartEventPosition(newShape);
modeling.createShape({ type: 'bpmn:StartEvent' }, position, newShape);
});
}
SubProcessStartEventBehavior.$inject = [
'eventBus',
'injector',
'modeling'
];
@ -61,7 +41,7 @@ inherits(SubProcessStartEventBehavior, CommandInterceptor);
// helpers //////////
function calculatePositionRelativeToShape(shape) {
function getStartEventPosition(shape) {
return {
x: shape.x + shape.width / 6,
y: shape.y + shape.height / 2

View File

@ -71,8 +71,26 @@ PaletteProvider.prototype.getPaletteEntries = function(element) {
};
}
function createParticipant(event, collapsed) {
create.start(event, elementFactory.createParticipantShape(collapsed));
function createSubprocess(event) {
var subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
x: 0,
y: 0,
isExpanded: true
});
var startEvent = elementFactory.createShape({
type: 'bpmn:StartEvent',
x: 40,
y: 82,
parent: subProcess
});
create.start(event, [ subProcess, startEvent ]);
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
@ -148,11 +166,15 @@ PaletteProvider.prototype.getPaletteEntries = function(element) {
'bpmn:DataStoreReference', 'data-store', 'bpmn-icon-data-store',
translate('Create DataStoreReference')
),
'create.subprocess-expanded': createAction(
'bpmn:SubProcess', 'activity', 'bpmn-icon-subprocess-expanded',
translate('Create expanded SubProcess'),
{ isExpanded: true }
),
'create.subprocess-expanded': {
group: 'activity',
className: 'bpmn-icon-subprocess-expanded',
title: translate('Create expanded SubProcess'),
action: {
dragstart: createSubprocess,
click: createSubprocess
}
},
'create.participant-expanded': {
group: 'collaboration',
className: 'bpmn-icon-participant',

View File

@ -13,6 +13,7 @@ import {
} from '../../util/ModelUtil';
import {
getParent,
isAny
} from '../modeling/util/ModelingUtil';
@ -117,6 +118,20 @@ BpmnRules.prototype.init = function() {
return canResize(shape, newBounds);
});
this.addRule('elements.create', function(context) {
var elements = context.elements,
position = context.position,
target = context.target;
return every(elements, function(element) {
if (isConnection(element)) {
return canConnect(element.source, element.target, element);
}
return canCreate(element, target, null, position);
});
});
this.addRule('elements.move', function(context) {
var target = context.target,
@ -320,7 +335,7 @@ function isSameScope(a, b) {
var scopeParentA = getScopeParent(a),
scopeParentB = getScopeParent(b);
return scopeParentA && (scopeParentA === scopeParentB);
return scopeParentA === scopeParentB;
}
function hasEventDefinition(element, eventDefinition) {
@ -852,9 +867,9 @@ function canConnectAssociation(source, target) {
function canConnectMessageFlow(source, target) {
// handle the case where target does not have a parent,
// because it is not dropped within the diagram (bpmn-io/bpmn-js#1033)
if (!target.parent) {
// during connect user might move mouse out of canvas
// https://github.com/bpmn-io/bpmn-js/issues/1033
if (getRootElement(source) && !getRootElement(target)) {
return false;
}
@ -955,8 +970,11 @@ function isOutgoingEventBasedGatewayConnection(connection) {
}
function areOutgoingEventBasedGatewayConnections(connections) {
connections = connections || [];
return connections.some(isOutgoingEventBasedGatewayConnection);
}
function getRootElement(element) {
return getParent(element, 'bpmn:Process') || getParent(element, 'bpmn:Collaboration');
}

View File

@ -277,19 +277,6 @@ describe('features/auto-resize', function() {
expect(participantShape).to.have.bounds(originalBounds);
}));
it('should not auto-resize when creating with { root: false } hint', inject(function(modeling) {
// given
var taskAttrs = { type: 'bpmn:Task' };
// when
modeling.createShape(taskAttrs, { x: 600, y: 320 }, participantShape, { root: false });
// then
expect(participantShape).to.have.bounds(originalBounds);
}));
});

View File

@ -28,7 +28,7 @@ describe('features/modeling - create participant', function() {
describe('process', function() {
describe('should turn diagram into collaboration', function() {
describe('should turn process into collaboration', function() {
var processDiagramXML = require('../../../../fixtures/bpmn/collaboration/process-empty.bpmn');
@ -39,13 +39,17 @@ describe('features/modeling - create participant', function() {
collaborationDi,
diRoot,
participant,
participant2,
participants,
participantBo,
participant2Bo,
participantDi,
participant2Di,
process,
processBo,
processDi;
beforeEach(inject(function(canvas, elementFactory, modeling) {
beforeEach(inject(function(canvas, elementFactory) {
// given
process = canvas.getRootElement();
@ -54,48 +58,116 @@ describe('features/modeling - create participant', function() {
diRoot = processBo.di.$parent;
participant = elementFactory.createParticipantShape();
participant = elementFactory.createParticipantShape({ x: 100, y: 100 });
participantBo = participant.businessObject;
participantDi = participantBo.di;
// when
modeling.createShape(participant, { x: 350, y: 200 }, process);
participant2 = elementFactory.createParticipantShape({ x: 100, y: 400 });
participant2Bo = participant2.businessObject;
participant2Di = participant2Bo.di;
collaboration = canvas.getRootElement();
collaborationBo = collaboration.businessObject;
collaborationDi = collaborationBo.di;
participants = [ participant, participant2 ];
}));
it('execute', function() {
describe('creating one participant', function() {
// then
expect(participantBo.$parent).to.equal(collaborationBo);
expect(participantBo.processRef).to.equal(processBo);
beforeEach(inject(function(canvas, modeling) {
expect(collaborationBo.$instanceOf('bpmn:Collaboration')).to.be.true;
expect(collaborationBo.$parent).to.equal(processBo.$parent);
expect(collaborationBo.participants).to.include(participantBo);
// when
modeling.createShape(participant, { x: 400, y: 225 }, process);
collaboration = canvas.getRootElement();
collaborationBo = collaboration.businessObject;
collaborationDi = collaborationBo.di;
}));
it('execute', function() {
// then
expect(participantBo.$parent).to.equal(collaborationBo);
expect(participantBo.processRef).to.equal(processBo);
expect(collaborationBo.$instanceOf('bpmn:Collaboration')).to.be.true;
expect(collaborationBo.$parent).to.equal(processBo.$parent);
expect(collaborationBo.participants).to.include(participantBo);
expect(participantDi.$parent).to.equal(collaborationDi);
expect(collaborationDi.$parent).to.equal(diRoot);
});
it('undo', inject(function(commandStack) {
// when
commandStack.undo();
// then
expect(participantBo.$parent).not.to.exist;
expect(participantBo.processRef).not.to.equal(processBo);
expect(collaborationBo.$parent).not.to.exist;
expect(collaborationBo.participants).not.to.include(participantBo);
expect(processDi.$parent).to.equal(diRoot);
}));
expect(participantDi.$parent).to.equal(collaborationDi);
expect(collaborationDi.$parent).to.equal(diRoot);
});
it('undo', inject(function(commandStack) {
describe('creating two participants', function() {
// when
commandStack.undo();
beforeEach(inject(function(canvas, modeling) {
// then
expect(participantBo.$parent).not.to.exist;
expect(participantBo.processRef).not.to.equal(processBo);
// when
modeling.createElements(participants, { x: 400, y: 375 }, process);
expect(collaborationBo.$parent).not.to.exist;
expect(collaborationBo.participants).not.to.include(participantBo);
collaboration = canvas.getRootElement();
collaborationBo = collaboration.businessObject;
collaborationDi = collaborationBo.di;
}));
expect(processDi.$parent).to.equal(diRoot);
}));
it('execute', function() {
// then
expect(participantBo.$parent).to.equal(collaborationBo);
expect(participantBo.processRef).to.equal(processBo);
expect(participant2Bo.$parent).to.equal(collaborationBo);
expect(participant2Bo.processRef).not.to.equal(processBo);
expect(collaborationBo.$instanceOf('bpmn:Collaboration')).to.be.true;
expect(collaborationBo.$parent).to.equal(processBo.$parent);
expect(collaborationBo.participants).to.include(participantBo);
expect(participantDi.$parent).to.equal(collaborationDi);
expect(participant2Di.$parent).to.equal(collaborationDi);
expect(collaborationDi.$parent).to.equal(diRoot);
});
it('undo', inject(function(commandStack) {
// when
commandStack.undo();
// then
expect(participantBo.$parent).not.to.exist;
expect(participantBo.processRef).not.to.equal(processBo);
expect(participant2Bo.$parent).not.to.exist;
expect(participant2Bo.processRef).not.to.equal(processBo);
expect(collaborationBo.$parent).not.to.exist;
expect(collaborationBo.participants).not.to.include(participantBo);
expect(collaborationBo.participants).not.to.include(participant2Bo);
expect(processDi.$parent).to.equal(diRoot);
}));
});
});

View File

@ -1,11 +1,17 @@
import {
bootstrapModeler,
getBpmnJS,
inject
} from 'test/TestHelper';
import coreModule from 'lib/core';
import createModule from 'diagram-js/lib/features/create';
import modelingModule from 'lib/features/modeling';
import paletteModule from 'lib/features/palette';
import coreModule from 'lib/core';
import { createMoveEvent } from 'diagram-js/lib/features/mouse/Mouse';
import { is } from 'lib/util/ModelUtil';
import {
query as domQuery,
@ -17,12 +23,17 @@ describe('features/palette', function() {
var diagramXML = require('../../../fixtures/bpmn/features/replace/01_replace.bpmn');
var testModules = [ coreModule, modelingModule, paletteModule ];
var testModules = [
coreModule,
createModule,
modelingModule,
paletteModule
];
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should provide BPMN modeling palette', inject(function(canvas, palette) {
it('should provide BPMN modeling palette', inject(function(canvas) {
// when
var paletteElement = domQuery('.djs-palette', canvas._container);
@ -32,4 +43,35 @@ describe('features/palette', function() {
expect(entries.length).to.equal(14);
}));
describe('sub process', function() {
it('should create sub process with start event', inject(function(dragging, palette) {
// when
triggerPaletteEntry('create.subprocess-expanded');
// then
var context = dragging.context(),
elements = context.data.elements;
expect(elements).to.have.length(2);
expect(is(elements[0], 'bpmn:SubProcess')).to.be.true;
expect(is(elements[1], 'bpmn:StartEvent')).to.be.true;
}));
});
});
// helpers //////////
function triggerPaletteEntry(id) {
getBpmnJS().invoke(function(palette) {
var entry = palette.getEntries()[ id ];
if (entry && entry.action && entry.action.click) {
entry.action.click(createMoveEvent(0, 0));
}
});
}

View File

@ -20,6 +20,69 @@ describe('features/modeling/rules - BpmnRules', function() {
var testModules = [ coreModule, modelingModule ];
describe('create elements', function() {
var testXML = require('./BpmnRules.process.bpmn');
beforeEach(bootstrapModeler(testXML, { modules: testModules }));
it('create tasks -> process', inject(function(elementFactory) {
// given
var task1 = elementFactory.createShape({ type: 'bpmn:Task' }),
task2 = elementFactory.createShape({ type: 'bpmn:Task' });
// then
expectCanCreate([ task1, task2 ], 'Process', true);
}));
it('create tasks -> task', inject(function(elementFactory) {
// given
var task1 = elementFactory.createShape({ type: 'bpmn:Task' }),
task2 = elementFactory.createShape({ type: 'bpmn:Task' });
// then
expectCanCreate([ task1, task2 ], 'Task', false);
}));
it('create tasks and sequence flow -> process', inject(function(elementFactory) {
// given
var task1 = elementFactory.createShape({ type: 'bpmn:Task' }),
task2 = elementFactory.createShape({ type: 'bpmn:Task' }),
sequenceFlow = elementFactory.createConnection({
type: 'bpmn:SequenceFlow',
source: task1,
target: task2
});
// then
expectCanCreate([ task1, task2, sequenceFlow ], 'Process', true);
}));
it('create tasks and message flow -> process', inject(function(elementFactory) {
// given
var task1 = elementFactory.createShape({ type: 'bpmn:Task' }),
task2 = elementFactory.createShape({ type: 'bpmn:Task' }),
sequenceFlow = elementFactory.createConnection({
type: 'bpmn:MessageFlow',
source: task1,
target: task2
});
// then
expectCanCreate([ task1, task2, sequenceFlow ], 'Process', false);
}));
});
describe('on process diagram', function() {
var testXML = require('./BpmnRules.process.bpmn');

View File

@ -3,7 +3,9 @@ import {
} from 'test/TestHelper';
import {
isString
isArray,
isString,
map
} from 'min-dash';
@ -47,10 +49,21 @@ export function expectCanDrop(element, target, expectedResult) {
}
export function expectCanCreate(element, target, expectedResult) {
export function expectCanCreate(shape, target, expectedResult) {
var result = getBpmnJS().invoke(function(bpmnRules) {
return bpmnRules.canCreate(get(element), get(target));
var result = getBpmnJS().invoke(function(rules) {
if (isArray(shape)) {
return rules.allowed('elements.create', {
elements: get(shape),
target: get(target)
});
}
return rules.allowed('shape.create', {
shape: get(shape),
target: get(target)
});
});
expect(result).to.eql(expectedResult);
@ -94,21 +107,25 @@ export function expectCanMove(elements, target, rules) {
* Retrieve element, resolving an ID with
* the actual element.
*/
function get(element) {
function get(elementId) {
var actualElement;
if (isString(element)) {
actualElement = getBpmnJS().invoke(function(elementRegistry) {
return elementRegistry.get(element);
});
if (!actualElement) {
throw new Error('element #' + element + ' not found');
}
return actualElement;
if (isArray(elementId)) {
return map(elementId, get);
}
return element;
var element;
if (isString(elementId)) {
element = getBpmnJS().invoke(function(elementRegistry) {
return elementRegistry.get(elementId);
});
if (!element) {
throw new Error('element #' + elementId + ' not found');
}
return element;
}
return elementId;
}