diff --git a/lib/camunda-cloud/CreateZeebeUserTaskBehavior.js b/lib/camunda-cloud/CreateZeebeUserTaskBehavior.js
new file mode 100644
index 0000000..197abea
--- /dev/null
+++ b/lib/camunda-cloud/CreateZeebeUserTaskBehavior.js
@@ -0,0 +1,81 @@
+import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil';
+import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
+
+import { createElement } from '../util/ElementUtil';
+import { getExtensionElementsList } from '../util/ExtensionElementsUtil';
+
+const HIGH_PRIORITY = 5000;
+
+/**
+ * Zeebe BPMN specific behavior for creating user tasks.
+ */
+export default class CreateZeebeUserTaskBehavior extends CommandInterceptor {
+ constructor(bpmnFactory, eventBus, modeling) {
+ super(eventBus);
+
+ /**
+ * Add zeebe:userTask extension element when creating bpmn:UserTask.
+ */
+ this.postExecuted(
+ [ 'shape.create', 'shape.replace' ],
+ HIGH_PRIORITY,
+ function(context) {
+ const shape = context.shape || context.newShape;
+ const explicitlyDisabled = context.hints && context.hints.createElementsBehavior === false;
+
+ if (!is(shape, 'bpmn:UserTask') || explicitlyDisabled) {
+ return;
+ }
+
+ let userTaskElement = getZeebeUserTask(shape);
+ if (userTaskElement) {
+ return;
+ }
+
+ const businessObject = getBusinessObject(shape);
+ let extensionElements = businessObject.get('extensionElements');
+
+ if (!extensionElements) {
+ extensionElements = createElement(
+ 'bpmn:ExtensionElements',
+ {
+ values: [],
+ },
+ businessObject,
+ bpmnFactory
+ );
+
+ modeling.updateProperties(shape, { extensionElements });
+ }
+
+ userTaskElement = createElement(
+ 'zeebe:UserTask',
+ {},
+ extensionElements,
+ bpmnFactory
+ );
+
+ modeling.updateModdleProperties(shape, extensionElements, {
+ values: [ ...(extensionElements.values || []), userTaskElement ],
+ });
+ },
+ true
+ );
+ }
+}
+
+CreateZeebeUserTaskBehavior.$inject = [ 'bpmnFactory', 'eventBus', 'modeling' ];
+
+/**
+ * Get zeebe:userTask extension.
+ *
+ * @param {djs.model.Base|ModdleElement} element
+ *
+ * @returns {ModdleElement|null}
+ */
+function getZeebeUserTask(element) {
+ const businessObject = getBusinessObject(element);
+ const userTaskElements = getExtensionElementsList(businessObject, 'zeebe:UserTask');
+
+ return userTaskElements[0] || null;
+}
diff --git a/lib/camunda-cloud/index.js b/lib/camunda-cloud/index.js
index 515e69c..2856e27 100644
--- a/lib/camunda-cloud/index.js
+++ b/lib/camunda-cloud/index.js
@@ -6,6 +6,7 @@ import CleanUpSubscriptionBehavior from './CleanUpSubscriptionBehavior';
import CleanUpTimerExpressionBehavior from './CleanUpTimerExpressionBehavior';
import CopyPasteBehavior from './CopyPasteBehavior';
import CreateZeebeCallActivityBehavior from './CreateZeebeCallActivityBehavior';
+import CreateZeebeUserTaskBehavior from './CreateZeebeUserTaskBehavior';
import DeleteParticipantBehaviour from '../shared/DeleteParticipantBehaviour';
import FormsBehavior from './FormsBehavior';
import RemoveAssignmentDefinitionBehavior from './RemoveAssignmentDefinitionBehavior';
@@ -22,6 +23,7 @@ export default {
'cleanUpTimerExpressionBehavior',
'copyPasteBehavior',
'createZeebeCallActivityBehavior',
+ 'createZeebeUserTaskBehavior',
'deleteParticipantBehaviour',
'formsBehavior',
'removeAssignmentDefinitionBehavior',
@@ -36,6 +38,7 @@ export default {
cleanUpTimerExpressionBehavior: [ 'type', CleanUpTimerExpressionBehavior ],
copyPasteBehavior: [ 'type', CopyPasteBehavior ],
createZeebeCallActivityBehavior: [ 'type', CreateZeebeCallActivityBehavior ],
+ createZeebeUserTaskBehavior: [ 'type', CreateZeebeUserTaskBehavior ],
deleteParticipantBehaviour: [ 'type', DeleteParticipantBehaviour ],
formsBehavior: [ 'type', FormsBehavior ],
removeAssignmentDefinitionBehavior: [ 'type', RemoveAssignmentDefinitionBehavior ],
diff --git a/test/camunda-cloud/CleanUpTaskListenersBehaviorSpec.js b/test/camunda-cloud/CleanUpTaskListenersBehaviorSpec.js
index 1d0f3e4..55d6aa0 100644
--- a/test/camunda-cloud/CleanUpTaskListenersBehaviorSpec.js
+++ b/test/camunda-cloud/CleanUpTaskListenersBehaviorSpec.js
@@ -155,24 +155,6 @@ describe('camunda-cloud/features/modeling - CleanUpTaskListenersBehavior', funct
}));
- it('should remove zeebe:TaskListeners for non zeebe user task', inject(function(bpmnReplace, elementRegistry) {
-
- // given
- let el = elementRegistry.get('NonZeebeUserTask');
-
- // when
- bpmnReplace.replaceElement(el, {
- type: 'bpmn:UserTask'
- });
-
- // then
- el = elementRegistry.get('NonZeebeUserTask');
- const extensionElements = getExtensionElements(el);
-
- expect(extensionElements.get('values')).to.have.lengthOf(0);
- }));
-
-
it('should remove zeebe:TaskListeners container when empty', inject(function(elementRegistry, modeling) {
// given
diff --git a/test/camunda-cloud/CreateZeebeUserTaskBehaviorSpec.js b/test/camunda-cloud/CreateZeebeUserTaskBehaviorSpec.js
new file mode 100644
index 0000000..bd42f6f
--- /dev/null
+++ b/test/camunda-cloud/CreateZeebeUserTaskBehaviorSpec.js
@@ -0,0 +1,239 @@
+import { bootstrapCamundaCloudModeler, inject } from 'test/TestHelper';
+
+import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil';
+import { find } from 'min-dash';
+
+import { getExtensionElementsList } from 'lib/util/ExtensionElementsUtil';
+
+
+import emptyProcessDiagramXML from './process-empty.bpmn';
+import userTasksXML from './process-user-tasks.bpmn';
+
+describe('camunda-cloud/features/modeling - CreateZeebeUserTaskBehavior', function() {
+
+ describe('when a shape is created', function() {
+
+ beforeEach(bootstrapCamundaCloudModeler(emptyProcessDiagramXML));
+
+
+ it('should execute when creating bpmn:UserTask', inject(function(
+ canvas,
+ modeling
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+
+ // when
+ const newShape = modeling.createShape(
+ { type: 'bpmn:UserTask' },
+ { x: 100, y: 100 },
+ rootElement
+ );
+
+ // then
+ const businessObject = getBusinessObject(newShape),
+ zeebeUserTaskExtensions = getExtensionElementsList(businessObject, 'zeebe:UserTask');
+
+ expect(zeebeUserTaskExtensions).to.exist;
+ expect(zeebeUserTaskExtensions).to.have.lengthOf(1);
+ }));
+
+
+ it('should NOT execute when zeebe:UserTask already present', inject(function(
+ canvas,
+ bpmnFactory,
+ modeling
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement(),
+ bo = bpmnFactory.create('bpmn:UserTask', {
+ extensionElements: bpmnFactory.create('bpmn:ExtensionElements', {
+ values: [ bpmnFactory.create('zeebe:UserTask') ],
+ }),
+ });
+
+ // when
+ const newShape = modeling.createShape(
+ { type: 'bpmn:UserTask', businessObject: bo },
+ { x: 100, y: 100 },
+ rootElement
+ );
+
+ // then
+ const businessObject = getBusinessObject(newShape),
+ zeebeUserTaskExtensions = getExtensionElementsList(businessObject, 'zeebe:UserTask');
+
+ expect(zeebeUserTaskExtensions).to.exist;
+ expect(zeebeUserTaskExtensions).to.have.lengthOf(1);
+ }));
+
+
+ it('should NOT execute when creating bpmn:Task', inject(function(
+ canvas,
+ modeling
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+
+ // when
+ const newShape = modeling.createShape(
+ { type: 'bpmn:Task' },
+ { x: 100, y: 100 },
+ rootElement
+ );
+
+ // then
+ const zeebeUserTaskExtension = getZeebeUserTask(newShape);
+
+ expect(zeebeUserTaskExtension).not.to.exist;
+ }));
+ });
+
+
+ describe('when a shape is pasted', function() {
+
+ beforeEach(bootstrapCamundaCloudModeler(userTasksXML));
+
+
+ it('should NOT add zeebe:UserTask', inject(function(
+ canvas,
+ copyPaste,
+ elementRegistry
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+ const userTask = elementRegistry.get('UserTask_1');
+
+ // when
+ copyPaste.copy(userTask);
+
+ const elements = copyPaste.paste({
+ element: rootElement,
+ point: {
+ x: 1000,
+ y: 1000,
+ },
+ });
+
+ // then
+ const pastedUserTask = find(elements, (element) =>
+ is(element, 'bpmn:UserTask')
+ );
+
+ const zeebeUserTask = getZeebeUserTask(pastedUserTask);
+
+ expect(zeebeUserTask).not.to.exist;
+ }));
+
+
+ it('should keep existing zeebe:UserTask', inject(function(
+ canvas,
+ copyPaste,
+ elementRegistry
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+ const userTask = elementRegistry.get('withZeebeUserTask');
+
+ // when
+ copyPaste.copy(userTask);
+
+ const elements = copyPaste.paste({
+ element: rootElement,
+ point: {
+ x: 1000,
+ y: 1000,
+ },
+ });
+
+ // then
+ const pastedUserTask = find(elements, (element) =>
+ is(element, 'bpmn:UserTask')
+ );
+ const zeebeUserTasks = getExtensionElementsList(pastedUserTask, 'zeebe:UserTask');
+
+ expect(zeebeUserTasks).to.exist;
+ expect(zeebeUserTasks).to.have.lengthOf(1);
+ }));
+ });
+
+
+ describe('when a shape is replaced', function() {
+
+ beforeEach(bootstrapCamundaCloudModeler(userTasksXML));
+
+
+ it('should add zeebe:UserTask when target is bpmn:UserTask', inject(function(
+ elementRegistry,
+ bpmnReplace,
+ canvas,
+ modeling
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+
+ // when
+ const task = modeling.createShape(
+ { type: 'bpmn:Task', id: 'simpleTask' },
+ { x: 100, y: 100 },
+ rootElement
+ );
+ bpmnReplace.replaceElement(task, { type: 'bpmn:UserTask' });
+
+ // then
+ const updatedTask = elementRegistry.get(task.id),
+ zeebeUserTaskExtension = getZeebeUserTask(updatedTask);
+
+ expect(zeebeUserTaskExtension).to.exist;
+ }));
+
+
+ it('should NOT add zeebe:UserTask when target is bpmn:ServiceTask', inject(function(
+ elementRegistry,
+ bpmnReplace,
+ canvas,
+ modeling
+ ) {
+
+ // given
+ const rootElement = canvas.getRootElement();
+
+ // when
+ const task = modeling.createShape(
+ { type: 'bpmn:Task', id: 'simpleTask' },
+ { x: 100, y: 100 },
+ rootElement
+ );
+ bpmnReplace.replaceElement(task, { type: 'bpmn:ServiceTask' });
+
+ // then
+ const updatedTask = elementRegistry.get(task.id),
+ zeebeUserTask = getZeebeUserTask(updatedTask);
+
+ expect(zeebeUserTask).not.to.exist;
+ }));
+ });
+});
+
+
+// helpers //////////
+
+/**
+ * Get the first zeebe:userTask element of an element.
+ *
+ * @param {djs.model.Base|ModdleElement} element
+ *
+ * @returns {ModdleElement|null}
+ */
+function getZeebeUserTask(element) {
+ const businessObject = getBusinessObject(element);
+ const userTaskElements = getExtensionElementsList(businessObject, 'zeebe:UserTask');
+
+ return userTaskElements[0] || null;
+}
diff --git a/test/camunda-cloud/FormsBehaviorSpec.js b/test/camunda-cloud/FormsBehaviorSpec.js
index 4c1b83c..833cad2 100644
--- a/test/camunda-cloud/FormsBehaviorSpec.js
+++ b/test/camunda-cloud/FormsBehaviorSpec.js
@@ -181,17 +181,15 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
describe('no existing form definition or user task form', function() {
- it('should not create and reference new user task form', inject(function(canvas, elementFactory, modeling) {
+ it('should not create and reference new user task form', inject(function(canvas, elementFactory) {
// given
- const rootElement = canvas.getRootElement();
-
const element = elementFactory.createShape({
type: 'bpmn:UserTask'
});
// when
- modeling.createShape(element, { x: 100, y: 100 }, rootElement);
+ createShape(element);
// then
expect(getUserTaskForm(element)).not.to.exist;
@@ -203,11 +201,9 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
describe('existing form definition, no user task form', function() {
- it('should not create and reference new form definition', inject(function(bpmnFactory, canvas, elementFactory, modeling) {
+ it('should not create and reference new form definition', inject(function(bpmnFactory, canvas, elementFactory) {
// given
- const rootElement = canvas.getRootElement();
-
const formDefinition = bpmnFactory.create('zeebe:FormDefinition', {
formId: 'foo'
});
@@ -230,7 +226,7 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
});
// when
- modeling.createShape(element, { x: 100, y: 100 }, rootElement);
+ createShape(element);
// then
expect(getUserTaskForm(element)).not.to.exist;
@@ -242,11 +238,9 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
describe('existing form definition, user task form not referenced', function() {
- it('should not create and reference new form definition', inject(function(bpmnFactory, canvas, elementFactory, modeling) {
+ it('should not create and reference new form definition', inject(function(bpmnFactory, canvas, elementFactory) {
// given
- const rootElement = canvas.getRootElement();
-
const formDefinition = bpmnFactory.create('zeebe:FormDefinition', {
formKey: userTaskFormIdToFormKey('UserTaskForm_3')
});
@@ -269,7 +263,7 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
});
// when
- modeling.createShape(element, { x: 100, y: 100 }, rootElement);
+ createShape(element);
// then
expect(getUserTaskForm(element)).to.exist;
@@ -283,11 +277,9 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
let element;
- beforeEach(inject(function(canvas, bpmnFactory, elementFactory, modeling) {
+ beforeEach(inject(function(bpmnFactory, elementFactory) {
// given
- const rootElement = canvas.getRootElement();
-
const formDefinition = bpmnFactory.create('zeebe:FormDefinition', {
formKey: userTaskFormIdToFormKey('UserTaskForm_1')
});
@@ -310,7 +302,7 @@ describe('camunda-cloud/features/modeling - FormsBehavior', function() {
});
// when
- modeling.createShape(element, { x: 100, y: 100 }, rootElement);
+ createShape(element);
}));
@@ -852,3 +844,15 @@ function hasUsertaskForm(id, userTaskForms) {
return userTaskForm.get('zeebe:id') === id;
});
}
+
+/**
+ * Create shape without invoking create behavior.
+ * This allows to create a job-worker user task for the tests purpose.
+ * @param {ModdleElement} element
+ */
+function createShape(element) {
+ return getBpmnJS().invoke(function(canvas, modeling) {
+ const rootElement = canvas.getRootElement();
+ return modeling.createShape(element, { x: 100, y: 100 }, rootElement, { createElementsBehavior: false });
+ });
+}
diff --git a/test/camunda-cloud/process-user-tasks.bpmn b/test/camunda-cloud/process-user-tasks.bpmn
index 09a72da..bcac1dc 100644
--- a/test/camunda-cloud/process-user-tasks.bpmn
+++ b/test/camunda-cloud/process-user-tasks.bpmn
@@ -84,6 +84,11 @@
+
+
+
+
+
@@ -156,6 +161,10 @@
+
+
+
+