camunda-bpmn-js-behaviors
Version:
Camunda Platform 7 and 8 behaviors for bpmn-js
392 lines (305 loc) • 11.1 kB
JavaScript
import { isUndefined, without } from 'min-dash';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { createElement } from '../util/ElementUtil';
import { getExtensionElementsList } from '../util/ExtensionElementsUtil';
import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil';
import {
createUserTaskFormId,
formKeyToUserTaskFormId,
getFormDefinition,
getRootElement,
getUserTaskForm,
isUserTaskFormKey,
userTaskFormIdToFormKey
} from './util/FormsUtil';
/**
* Zeebe BPMN specific forms behavior.
*/
export default class FormsBehavior extends CommandInterceptor {
constructor(bpmnFactory, elementRegistry, eventBus, modeling) {
super(eventBus);
this._modeling = modeling;
function removeUserTaskForm(element, moddleElement, userTaskForm) {
const extensionElements = moddleElement.get('extensionElements');
const values = without(extensionElements.get('values'), userTaskForm);
modeling.updateModdleProperties(element, extensionElements, {
values
});
if (!values.length) {
modeling.updateModdleProperties(element, moddleElement, {
extensionElements: undefined
});
}
}
function removeFormDefinition(element, formDefinition) {
const bo = getBusinessObject(element);
const extensionElements = bo.get('extensionElements');
const values = without(extensionElements.get('values'), formDefinition);
modeling.updateModdleProperties(element, extensionElements, { values });
if (!values.length) {
modeling.updateModdleProperties(element, bo, {
extensionElements: undefined
});
}
}
/**
* Remove zeebe:UserTaskForm on user task or start event removed.
*/
this.postExecute('shape.delete', function(context) {
const {
labelTarget,
oldParent,
shape
} = context;
if (labelTarget) {
return;
}
const rootElement = getRootElement(oldParent);
const userTaskForm = getUserTaskForm(shape, { rootElement });
if ((!is(shape, 'bpmn:UserTask') && !is(shape, 'bpmn:StartEvent')) || !userTaskForm) {
return;
}
removeUserTaskForm(shape, rootElement, userTaskForm);
}, true);
/**
* Clean up form definition when a start event is moved or pasted into
* a subprocess (forms are only supported on root-level none start events).
*/
this.postExecuted([ 'shape.move', 'shape.create' ], function(event) {
const { context } = event;
const { shape } = context;
const parent = context.newParent || context.parent;
if (shape.labelTarget) {
return;
}
if (!is(shape, 'bpmn:StartEvent') || !is(parent, 'bpmn:SubProcess')) {
return;
}
const formDefinition = getFormDefinition(shape);
if (!formDefinition) {
return;
}
removeFormDefinition(shape, formDefinition);
});
/**
* Clean up form definition when a none start event is replaced with
* a typed start event (message, timer, signal, etc.).
*/
this.postExecuted('shape.replace', function(event) {
const { context } = event;
const { newShape } = context;
if (!is(newShape, 'bpmn:StartEvent')) {
return;
}
const bo = getBusinessObject(newShape);
const eventDefinitions = bo.get('eventDefinitions');
// Only clean up if the new shape has event definitions (not a none start event)
if (!eventDefinitions || !eventDefinitions.length) {
return;
}
const formDefinition = getFormDefinition(newShape);
if (!formDefinition) {
return;
}
removeFormDefinition(newShape, formDefinition);
});
/**
* Create and reference new zeebe:UserTaskForm when user task or start event
* is created that references existing zeebe:UserTaskForm that is already
* referenced by existing element.
*/
this.postExecute('shape.create', function(context) {
const { shape } = context;
if (shape.labelTarget) {
return;
}
if (!is(shape, 'bpmn:UserTask') && !is(shape, 'bpmn:StartEvent')) {
return;
}
const oldFormDefinition = getFormDefinition(shape);
if (!oldFormDefinition) {
return;
}
const oldUserTaskForm = getUserTaskForm(shape);
if (!oldUserTaskForm) {
return;
}
const isReferenced = elementRegistry.filter(element => {
if (element === shape) {
return false;
}
const formDefinition = getFormDefinition(element);
return formDefinition
&& formDefinition.get('formKey')
&& formKeyToUserTaskFormId(formDefinition.get('formKey')) === oldUserTaskForm.get('id');
});
if (!isReferenced.length) {
return;
}
const rootElement = getRootElement(shape);
let extensionElements = rootElement.get('extensionElements');
// (1) ensure extension elements exist
if (!extensionElements) {
extensionElements = createElement('bpmn:ExtensionElements', {
values: []
}, rootElement, bpmnFactory);
modeling.updateModdleProperties(shape, rootElement, {
extensionElements
});
}
// (2) create new user task form
const userTaskFormId = createUserTaskFormId();
const userTaskForm = createElement('zeebe:UserTaskForm', {
id: userTaskFormId,
body: oldUserTaskForm.get('body')
}, extensionElements, bpmnFactory);
modeling.updateModdleProperties(shape, extensionElements, {
values: [
...(extensionElements.get('values') || []),
userTaskForm
]
});
// (3) reference new user task form
modeling.updateModdleProperties(shape, oldFormDefinition, {
formKey: userTaskFormIdToFormKey(userTaskFormId)
});
}, true);
/**
* Ensure that a user task only has one of the following:
*
* 1. zeebe:FormDefinition with zeebe:formId (linked Camunda form)
* 2. zeebe:FormDefinition with zeebe:formKey in the format of camunda-forms:bpmn:UserTaskForm_1 (embedded Camunda form)
* 3. zeebe:FormDefinition with zeebe:formKey (custom form)
* 4. zeebe:FormDefinition with zeebe:externalReference (external form)
*
* Furthermore, ensure that:
*
* 1. zeebe:bindingType only exists if zeebe:formId is set (linked Camunda form)
*/
this.preExecute('element.updateModdleProperties', function(context) {
const {
moddleElement,
properties
} = context;
if (is(moddleElement, 'zeebe:FormDefinition')) {
if ('formId' in properties) {
properties.formKey = undefined;
properties.externalReference = undefined;
} else if ('formKey' in properties) {
properties.formId = undefined;
properties.externalReference = undefined;
properties.bindingType = undefined;
} else if ('externalReference' in properties) {
properties.formId = undefined;
properties.formKey = undefined;
properties.bindingType = undefined;
}
if ('bindingType' in properties && !('formId' in properties) && !moddleElement.get('formId')) {
properties.externalReference = undefined;
properties.formId = '';
properties.formKey = undefined;
}
}
}, true);
/**
* Clean up user task form after form key or definition is removed. Clean up
* empty extension elements after form definition is removed.
*/
this.postExecute('element.updateModdleProperties', function(context) {
const {
element,
moddleElement,
oldProperties
} = context;
if (is(moddleElement, 'zeebe:FormDefinition')) {
const formKey = moddleElement.get('formKey');
if (!formKey || !isUserTaskFormKey(formKey)) {
const userTaskForm = getUserTaskForm(element, { formKey: oldProperties.formKey });
if (userTaskForm) {
removeUserTaskForm(element, getRootElement(element), userTaskForm);
}
}
} else if (isExtensionElementRemoved(context, 'zeebe:FormDefinition')) {
const formDefinition = oldProperties.values.find(value => is(value, 'zeebe:FormDefinition'));
const userTaskForm = getUserTaskForm(element, { formKey: formDefinition.get('formKey') });
if (userTaskForm) {
removeUserTaskForm(element, getRootElement(element), userTaskForm);
}
if (!moddleElement.get('values').length) {
modeling.updateProperties(element, {
extensionElements: undefined
});
}
}
}, true);
this._registerZeebeUserTaskSupport();
}
_registerZeebeUserTaskSupport() {
/**
* Handle `formKey` for `zeebe:UserTask`.
* 1. Remove if embedded form is used.
* 2. Convert to externalReference if custom form key.
*/
this.postExecute('element.updateModdleProperties', ({ element }) => {
if (!is(element, 'bpmn:UserTask') || !hasZeebeUserTask(element)) {
return;
}
const formDefinition = getFormDefinition(element);
if (!formDefinition) {
return;
}
const formKey = formDefinition.get('formKey');
if (isUndefined(formKey)) {
return;
}
if (isUserTaskFormKey(formKey)) {
this._modeling.updateModdleProperties(element, formDefinition, { formKey: undefined });
} else {
this._modeling.updateModdleProperties(element, formDefinition, {
externalReference: formKey
});
}
}, true);
/**
* Replace `externalReference` with `formKey` for non-`zeebe:UserTask`.
*/
this.postExecute('element.updateModdleProperties', ({ element }) => {
if (!is(element, 'bpmn:UserTask') || hasZeebeUserTask(element)) {
return;
}
const formDefinition = getFormDefinition(element);
if (!formDefinition) {
return;
}
const externalReference = formDefinition.get('externalReference');
if (isUndefined(externalReference)) {
return;
}
this._modeling.updateModdleProperties(element, formDefinition, {
externalReference: undefined,
formKey: externalReference
});
}, true);
}
}
FormsBehavior.$inject = [
'bpmnFactory',
'elementRegistry',
'eventBus',
'modeling'
];
function isExtensionElementRemoved(context, type) {
const {
moddleElement,
oldProperties,
properties
} = context;
return is(moddleElement, 'bpmn:ExtensionElements')
&& 'values' in oldProperties
&& 'values' in properties
&& oldProperties.values.find(value => is(value, type))
&& !properties.values.find(value => is(value, type));
}
function hasZeebeUserTask(userTask) {
return getExtensionElementsList(userTask, 'zeebe:UserTask').length;
}