diff --git a/lib/features/modeling/BpmnUpdater.js b/lib/features/modeling/BpmnUpdater.js
index f41e2ca2af..fc12068c8b 100644
--- a/lib/features/modeling/BpmnUpdater.js
+++ b/lib/features/modeling/BpmnUpdater.js
@@ -191,6 +191,7 @@ export default function BpmnUpdater(
}
});
+
// attach / detach connection
function updateConnection(e) {
self.updateConnection(e.context);
diff --git a/lib/features/modeling/behavior/ArtifactBehavior.js b/lib/features/modeling/behavior/ArtifactBehavior.js
new file mode 100644
index 0000000000..0526f88ffb
--- /dev/null
+++ b/lib/features/modeling/behavior/ArtifactBehavior.js
@@ -0,0 +1,194 @@
+import inherits from 'inherits-browser';
+
+import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
+
+import {
+ is
+} from '../../../util/ModelUtil';
+import { forEach } from 'min-dash';
+
+/**
+ * @typedef {import('../BpmnFactory').default} BpmnFactory
+ * @typedef {import('../../../Modeler').default} Modeler
+ * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry
+ * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
+ * @typedef {import('didi').Injector} Injector
+ *
+ * @typedef {import('../../../model/Types').Element} Element
+ * @typedef {import('../../../model/Types').Shape} Shape
+ *
+ * @typedef {import('diagram-js/lib/util/Types').DirectionTRBL} DirectionTRBL
+ * @typedef {import('../Modeling').default} Modeling
+ */
+
+var HIGH_PRIORITY = 1500;
+
+/**
+ * BPMN specific artifact behavior.
+ *
+ * @param {BpmnFactory} bpmnFactory
+ * @param {Modeler} bpmnjs
+ * @param {ElementRegistry} elementRegistry
+ * @param {EventBus} eventBus
+ * @param {Injector} injector
+ * @param {Modeling} modeling
+ */
+export default function ArtifactBehavior(
+ bpmnFactory,
+ bpmnjs,
+ elementRegistry,
+ eventBus,
+ injector,
+ modeling
+) {
+ injector.invoke(CommandInterceptor, this);
+
+ this.preExecute('shape.delete', HIGH_PRIORITY, function(event) {
+ var context = event.context,
+ shape = context.shape;
+
+ if (!is(shape, 'bpmn:Participant') && !is(shape, 'bpmn:SubProcess')) {
+ return;
+ }
+
+ modeling.removeElements(getRelevantArtifacts(shape));
+ });
+
+ // Handles artifacts on participants
+ eventBus.on('shape.move.start', function(event) {
+ let shape = event.shape;
+
+ if (!is(shape, 'bpmn:Participant') && !is(shape, 'bpmn:SubProcess')) {
+ return;
+ }
+
+ event.context.shapes.push(...getRelevantArtifacts(shape));
+ });
+
+ function calculateOverlappingArea(child, possibleParent) {
+ const overlapWidth = Math.max(0, Math.min((child.x + child.width), (possibleParent.x + possibleParent.width)) - Math.max(child.x, possibleParent.x));
+ const overlapHeight = Math.max(0, Math.min((child.y + child.height), (possibleParent.y + possibleParent.height)) - Math.max(child.y, possibleParent.y));
+
+ return overlapWidth * overlapHeight;
+ }
+
+ function isAssociatedTextAnnotation(shape, possibleParticipants) {
+ if (shape.type !== 'bpmn:TextAnnotation') {
+ return false;
+ }
+ let possibleAssociatedElements = possibleParticipants.flatMap((participant) => participant.children).flatMap((element) => element.id),
+ incomingElements = shape.incoming.flatMap((shape) => shape.businessObject.sourceRef.id);
+
+
+ return incomingElements.some(el =>
+ possibleAssociatedElements.includes(el)
+ );
+ }
+
+ function getRelevantArtifacts(shape) {
+
+ // when swimminglanes / participants were placed shape.businessObject.processRef has no mor artifacts because the bpmn:Collaboration has it
+
+ let relevantArtifacts = [],
+ parent = shape.parent,
+ allPossibleElements = getAllPossibleElements(parent),
+ possibleParticipants = getPossibleContainer(parent, allPossibleElements),
+ possibleArtifacts = getPossibleArtifacts(parent, allPossibleElements);
+
+ forEach(possibleArtifacts, function(currentShape) {
+
+ let participantCounter = 0,
+ curOverlap = calculateOverlappingArea(shape, currentShape.bounds ?? shape);
+
+ if (isAssociatedTextAnnotation (currentShape, possibleParticipants)) {
+ relevantArtifacts.push(currentShape);
+ return;
+ }
+
+ // should not be moved
+ if (isAssociatedAssociation(currentShape)) {
+ return;
+ }
+
+ if (curOverlap <= 0) {
+ return;
+ }
+ possibleParticipants.forEach(function(participant) {
+
+ if (participantCounter >= 2) {
+ return;
+ }
+ participantCounter = calculateOverlappingArea(currentShape.bounds, participant) === (currentShape.bounds?.height * currentShape.bounds?.width) ? participantCounter + 1 : participantCounter;
+ });
+
+ if (participantCounter === 1) {
+ relevantArtifacts.push(getElementFromChildren(parent, currentShape.id));
+ }
+ });
+
+ return relevantArtifacts;
+ }
+
+ function isAssociatedAssociation(shape) {
+ return shape.type === 'bpmn:Association' || shape.$type === 'bpmndi:BPMNEdge';
+ }
+
+ function getPossibleContainer(parent) {
+ return parent.children.filter(isContainer);
+ }
+
+ function getPossibleArtifacts(parent, possibleElements) {
+
+ let artifacts = parent.businessObject?.artifacts ?? parent.di.$parent?.bpmnElement?.artifacts ?? [];
+ artifacts = artifacts.flatMap((artifact) => `${artifact.id}_di`);
+ return possibleElements.filter((element) => artifacts.includes(element.id));
+ }
+
+ function getAllPossibleElements(parent) {
+ let diagram = getDiagram(parent);
+
+ // return parent.businessObject.$parent.diagrams[0].plane;
+ return diagram.diagrams[0].plane.planeElement;
+ }
+
+ function isContainer(shape) {
+ return is(shape, 'bpmn:Participant') || is(shape, 'bpmn:SubProcess');
+ }
+
+ function getDiagram(parent) {
+ let next = parent?.businessObject?.$parent ?? parent.$parent;
+ if (next.$type === 'bpmn:Definitions' || (next.$type === 'bpmn:Collaboration' && !next.$parent)) {
+ return next;
+ } else {
+ return getDiagram(next);
+ }
+ }
+
+ function getElementFromChildren(parent, shapeId) {
+ let filteredChildren = parent.children.filter(child => child.di.id === shapeId),
+ result =
+ filteredChildren.length > 0
+ ? filteredChildren
+ : getRootImpl(parent).children.filter(child => child.di.id === shapeId);
+
+ return result.at(0);
+ }
+
+ function getRootImpl(parent) {
+ while (parent.parent && parent.type !== 'bpmn:Collaboration') {
+ parent = parent.parent;
+ }
+ return parent;
+ }
+}
+
+inherits(ArtifactBehavior, CommandInterceptor);
+
+ArtifactBehavior.$inject = [
+ 'bpmnFactory',
+ 'bpmnjs',
+ 'elementRegistry',
+ 'eventBus',
+ 'injector',
+ 'modeling'
+];
\ No newline at end of file
diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js
index a7e0d93d1f..5f858f7433 100644
--- a/lib/features/modeling/behavior/index.js
+++ b/lib/features/modeling/behavior/index.js
@@ -2,6 +2,7 @@ import AdaptiveLabelPositioningBehavior from './AdaptiveLabelPositioningBehavior
import AppendBehavior from './AppendBehavior';
import AssociationBehavior from './AssociationBehavior';
import AttachEventBehavior from './AttachEventBehavior';
+import ArtifactBehavior from './ArtifactBehavior';
import BoundaryEventBehavior from './BoundaryEventBehavior';
import CompensateBoundaryEventBehavior from './CompensateBoundaryEventBehavior';
import CreateBehavior from './CreateBehavior';
@@ -49,6 +50,7 @@ export default {
'appendBehavior',
'associationBehavior',
'attachEventBehavior',
+ 'artifactBehavior',
'boundaryEventBehavior',
'compensateBoundaryEventBehaviour',
'createBehavior',
@@ -91,6 +93,7 @@ export default {
appendBehavior: [ 'type', AppendBehavior ],
associationBehavior: [ 'type', AssociationBehavior ],
attachEventBehavior: [ 'type', AttachEventBehavior ],
+ artifactBehavior: [ 'type', ArtifactBehavior ],
boundaryEventBehavior: [ 'type', BoundaryEventBehavior ],
compensateBoundaryEventBehaviour: [ 'type', CompensateBoundaryEventBehavior ],
createBehavior: [ 'type', CreateBehavior ],
diff --git a/test/fixtures/bpmn/participant-with-one-artifact.bpmn b/test/fixtures/bpmn/participant-with-one-artifact.bpmn
new file mode 100644
index 0000000000..1ce8e0e5f5
--- /dev/null
+++ b/test/fixtures/bpmn/participant-with-one-artifact.bpmn
@@ -0,0 +1,31 @@
+
+
+
+
+
+ test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/bpmn/participant-with-one-group.bpmn b/test/fixtures/bpmn/participant-with-one-group.bpmn
new file mode 100644
index 0000000000..0c611b13b7
--- /dev/null
+++ b/test/fixtures/bpmn/participant-with-one-group.bpmn
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+ Flow_0vdmipo
+
+
+ Flow_0vdmipo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/bpmn/participants-with-artifact.bpmn b/test/fixtures/bpmn/participants-with-artifact.bpmn
new file mode 100644
index 0000000000..366a0cdc50
--- /dev/null
+++ b/test/fixtures/bpmn/participants-with-artifact.bpmn
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/spec/ModelerSpec.js b/test/spec/ModelerSpec.js
index 200d2febe9..b988620ddf 100644
--- a/test/spec/ModelerSpec.js
+++ b/test/spec/ModelerSpec.js
@@ -18,7 +18,7 @@ import {
setBpmnJS,
clearBpmnJS,
collectTranslations,
- enableLogging
+ enableLogging, bootstrapModeler
} from 'test/TestHelper';
import {
@@ -27,6 +27,12 @@ import {
} from 'min-dash';
import { getDi } from 'lib/util/ModelUtil';
+import diagramXML from './features/grid-snapping/basic.bpmn';
+import coreModule from '../../lib/core';
+import createModule from 'diagram-js/lib/features/create';
+import gridSnappingModule from '../../lib/features/grid-snapping';
+import modelingModule from '../../lib/features/modeling';
+import moveModule from 'diagram-js/lib/features/move';
var singleStart = window.__env__ && window.__env__.SINGLE_START === 'modeler';
@@ -947,6 +953,150 @@ describe('Modeler', function() {
});
+ describe('artifacts move with their participants', function() {
+
+ beforeEach(bootstrapModeler(diagramXML, {
+ modules: [
+ coreModule,
+ createModule,
+ gridSnappingModule,
+ modelingModule,
+ moveModule
+ ]
+ }));
+
+
+ it('should drag participant and annotation should follow', async function() {
+ var xml = require('../fixtures/bpmn/participant-with-one-artifact.bpmn');
+
+ const result = await createModeler(xml);
+ expect(result.error).not.to.exist;
+
+ var modeler = result.modeler;
+ var elementRegistry = modeler.get('elementRegistry');
+ var dragging = modeler.get('dragging');
+ var move = modeler.get('move');
+
+ var participant = elementRegistry.get('Participant_1axg5jz');
+ var annotation = elementRegistry.get('TextAnnotation_0hsrpnp');
+
+ expect(participant).to.exist;
+ expect(annotation).to.exist;
+
+ const oldX = annotation.x;
+ const oldY = annotation.y;
+
+ move.start(
+ createCanvasEvent({ x: participant.x + 10, y: participant.y + 10 }),
+ participant
+ );
+
+ dragging.move(createCanvasEvent({ x: participant.x + 110, y: participant.y + 60 }));
+
+ dragging.end();
+
+ const newAnnotation = elementRegistry.get('TextAnnotation_0hsrpnp');
+
+ expect(newAnnotation.x).to.equal(oldX + 100);
+ expect(newAnnotation.y).to.equal(oldY + 50);
+ });
+
+ it('should drag participant and group should follow', async function() {
+ var xml = require('../fixtures/bpmn/participant-with-one-group.bpmn');
+
+ const result = await createModeler(xml);
+ expect(result.error).not.to.exist;
+
+ var modeler = result.modeler;
+ var elementRegistry = modeler.get('elementRegistry');
+ var move = modeler.get('move');
+ var dragging = modeler.get('dragging');
+
+ var participant = elementRegistry.get('Participant_1axg5jz');
+ var group = elementRegistry.get('Group_0rfu791');
+
+ expect(participant).to.exist;
+ expect(group).to.exist;
+
+ const oldX = group.x;
+ const oldY = group.y;
+
+ move.start(
+ createCanvasEvent({ x: participant.x + 20, y: participant.y + 20 }),
+ participant
+ );
+
+ dragging.move(createCanvasEvent({ x: participant.x + 140, y: participant.y + 80 }));
+
+ dragging.end();
+
+ const newGroup = elementRegistry.get('Group_0rfu791');
+
+ expect(newGroup.x).to.equal(oldX + 120);
+ expect(newGroup.y).to.equal(oldY + 60);
+ });
+
+ it('should NOT move group when it overlaps two participants and one participant is dragged', async function() {
+ var xml = require('../fixtures/bpmn/participants-with-artifact.bpmn');
+
+ const result = await createModeler(xml);
+ expect(result.error).not.to.exist;
+
+ var modeler = result.modeler;
+ var elementRegistry = modeler.get('elementRegistry');
+ var move = modeler.get('move');
+ var dragging = modeler.get('dragging');
+
+ var participant1 = elementRegistry.get('Participant_09y906w');
+ var participant2 = elementRegistry.get('Participant_1svjgx6');
+ var group = elementRegistry.get('Group_0007vsj');
+
+ expect(participant1).to.exist;
+ expect(participant2).to.exist;
+ expect(group).to.exist;
+
+ const oldX = group.x;
+ const oldY = group.y;
+
+ move.start(
+ createCanvasEvent({ x: participant1.x + 20, y: participant1.y + 20 }),
+ participant1
+ );
+
+ dragging.move(createCanvasEvent({ x: participant1.x + 120, y: participant1.y + 80 }));
+ dragging.end();
+
+ const newGroup = elementRegistry.get('Group_0007vsj');
+
+ expect(newGroup.x).to.equal(oldX);
+ expect(newGroup.y).to.equal(oldY);
+ });
+
+ it('should delete when participant is deleted', async function() {
+ var xml = require('../fixtures/bpmn/participant-with-one-group.bpmn');
+
+ const result = await createModeler(xml);
+ expect(result.error).not.to.exist;
+
+ var modeler = result.modeler;
+ var modeling = modeler.get('modeling');
+ var elementRegistry = modeler.get('elementRegistry');
+
+ var participant = elementRegistry.get('Participant_1axg5jz');
+ var group = elementRegistry.get('Group_0rfu791');
+
+ expect(participant).to.exist;
+ expect(group).to.exist;
+
+ modeling.removeShape(participant);
+
+ var participantFound = elementRegistry.get('Participant_1axg5jz');
+ var groupFound = elementRegistry.get('Group_0rfu791');
+
+ expect(participantFound).to.not.exist;
+ expect(groupFound).to.not.exist;
+ });
+ });
});