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 @@ + + + +