bpmn-js-element-templates
Version:
Element templates for bpmn-js
2,076 lines (1,692 loc) • 246 kB
JavaScript
import { isNil, flatten, values, isObject, filter, isString, isUndefined, reduce, has, find, isArray, forEach, isFunction, without, assign, keys } from 'min-dash';
import { getBusinessObject, is, isAny } from 'bpmn-js/lib/util/ModelUtil';
import { v4 } from 'uuid';
import { satisfies, coerce, valid, validRange } from 'semver';
import semverCompare from 'semver-compare';
import { getSchemaVersion as getSchemaVersion$1, validate, getZeebeSchemaVersion, getZeebeSchemaPackage, validateZeebe } from '@bpmn-io/element-templates-validator';
import Ids from 'ids';
import { isEventSubProcess } from 'bpmn-js/lib/util/DiUtil';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { getLabel, setLabel } from 'bpmn-js/lib/features/label-editing/LabelUtil';
import { isPlane, getShapeIdFromPlane } from 'bpmn-js/lib/util/DrilldownUtil';
/**
* Check if the property is cast to FEEL expression:
* - Boolean and Number properties with feel set to 'optional' or 'static'
* - Boolean and Number input/output parameters have default feel=static
*
* @returns {boolean}
*/
const shouldCastToFeel = (property) => {
const feel = getFeelValue(property);
return [ 'optional', 'static' ].includes(feel) && [ 'Boolean', 'Number' ].includes(property.type);
};
const ALWAYS_CAST_TO_FEEL = [
'zeebe:input',
'zeebe:output'
];
function getFeelValue(property) {
if (ALWAYS_CAST_TO_FEEL.includes(property.binding.type)) {
return property.feel || 'static';
}
return property.feel;
}
const toFeelExpression = (value, type) => {
if (typeof value === 'string' && value.startsWith('=')) {
return value;
}
if (type === 'Boolean') {
value = value === 'false' ? false : value;
return '=' + !!value;
}
if (typeof value === 'undefined') {
return value;
}
return '=' + value.toString();
};
/**
* The BPMN 2.0 extension attribute name under
* which the element template ID is stored.
*
* @type {String}
*/
const TEMPLATE_ID_ATTR$1 = 'zeebe:modelerTemplate';
/**
* The BPMN 2.0 extension attribute name under
* which the element template version is stored.
*
* @type {String}
*/
const TEMPLATE_VERSION_ATTR$1 = 'zeebe:modelerTemplateVersion';
/**
* The BPMN 2.0 extension attribute name under
* which the element template icon is stored.
*
* @type {String}
*/
const TEMPLATE_ICON_ATTR = 'zeebe:modelerTemplateIcon';
/**
* Get template id for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateId$1(element) {
const businessObject = getBusinessObject(element);
if (businessObject) {
return businessObject.get(TEMPLATE_ID_ATTR$1);
}
}
/**
* Get template version for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateVersion$1(element) {
const businessObject = getBusinessObject(element);
if (businessObject) {
return businessObject.get(TEMPLATE_VERSION_ATTR$1);
}
}
/**
* Get template icon for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateIcon(element) {
const businessObject = getBusinessObject(element);
if (businessObject) {
return businessObject.get(TEMPLATE_ICON_ATTR);
}
}
/**
* Find extension with given type in
* BPMN element, diagram element or ExtensionElement.
*
* @param {ModdleElement|djs.model.Base} element
* @param {String} type
*
* @return {ModdleElement} the extension
*/
function findExtension$1(element, type) {
const businessObject = getBusinessObject(element);
let extensionElements;
if (is(businessObject, 'bpmn:ExtensionElements')) {
extensionElements = businessObject;
} else {
extensionElements = businessObject.get('extensionElements');
}
if (!extensionElements) {
return;
}
return extensionElements.get('values').find((value) => {
return is(value, type);
});
}
function findZeebeProperty(zeebeProperties, binding) {
return zeebeProperties.get('properties').find((value) => {
return value.name === binding.name;
});
}
function findInputParameter(ioMapping, binding) {
const parameters = ioMapping.get('inputParameters');
return parameters.find((parameter) => {
return parameter.target === binding.name;
});
}
function findOutputParameter(ioMapping, binding) {
const parameters = ioMapping.get('outputParameters');
return parameters.find((parameter) => {
return parameter.source === binding.source;
});
}
function findTaskHeader(taskHeaders, binding) {
const headers = taskHeaders.get('values');
return headers.find((header) => {
return header.key === binding.key;
});
}
/**
* Find message referred to in an event, an event definition, or a task.
* @param {ModdleElement} businessObject
*/
function findMessage(businessObject) {
if (is(businessObject, 'bpmn:Event')) {
const eventDefinitions = businessObject.get('eventDefinitions');
if (!eventDefinitions || !eventDefinitions.length) {
return;
}
businessObject = eventDefinitions[0];
}
if (!businessObject) {
return;
}
return businessObject.get('messageRef');
}
/**
* Find signal referred to in an event or an event definition.
* @param {ModdleElement} businessObject
*/
function findSignal(businessObject) {
if (is(businessObject, 'bpmn:Event')) {
const eventDefinitions = businessObject.get('eventDefinitions');
if (!eventDefinitions || !eventDefinitions.length) {
return;
}
businessObject = eventDefinitions[0];
}
if (!businessObject) {
return;
}
return businessObject.get('signalRef');
}
/**
* Find timer event definition in an event.
* @param {ModdleElement|element} element
*/
function findTimerEventDefinition(element) {
const businessObject = getBusinessObject(element);
if (is(businessObject, 'bpmn:Event')) {
const eventDefinitions = businessObject.get('eventDefinitions');
if (!eventDefinitions.length) {
return;
}
return eventDefinitions.find(def => is(def, 'bpmn:TimerEventDefinition'));
}
}
/**
* Get the default value disregarding generated values.
*/
function getDefaultFixedValue(property) {
if (
shouldCastToFeel(property) || property.feel === 'required'
) {
return toFeelExpression(property.value, property.type);
}
return property.value;
}
function getDefaultValue(property) {
const value = getDefaultFixedValue(property);
if (value !== undefined) {
return value;
}
if (property.generatedValue) {
const { type } = property.generatedValue;
if (type === 'uuid') {
return v4();
}
}
}
/**
* The BPMN 2.0 extension attribute name under
* which the element template ID is stored.
*
* @type {String}
*/
const TEMPLATE_ID_ATTR = 'camunda:modelerTemplate';
/**
* The BPMN 2.0 extension attribute name under
* which the element template version is stored.
*
* @type {String}
*/
const TEMPLATE_VERSION_ATTR = 'camunda:modelerTemplateVersion';
/**
* Returns incompatible engines for a given template.
*
* @param {Object} template
* @param {Object} checkEngines
*
* @return {Object}
*/
function getIncompatibleEngines(template, checkEngines) {
const templateEngines = template.engines;
return reduce(templateEngines, (result, _, engine) => {
if (!has(checkEngines, engine)) {
return result;
}
if (!satisfies(checkEngines[engine], templateEngines[engine])) {
result[engine] = {
actual: checkEngines[engine],
required: templateEngines[engine]
};
}
return result;
}, {});
}
/**
* Returns whether a template is compatible with the given engines.
*
* @param {Object} template
* @param {Object} checkEngines
*
* @return {boolean}
*/
function isCompatible(template, checkEngines) {
return !Object.keys(getIncompatibleEngines(template, checkEngines)).length;
}
/**
* Build a map of templates grouped by id.
*
* @param {Array<Object>} templates
* @param {Object} engines
*
* @return {Object}
*/
function buildTemplatesById(templates, engines) {
const templatesById = {};
templates.forEach((template) => {
const id = template.id;
const version = isUndefined(template.version) ? '_' : template.version;
if (!templatesById[id]) {
templatesById[id] = {};
}
templatesById[id][version] = template;
const latest = templatesById[id].latest;
if (isCompatible(template, engines)) {
if (!latest || isUndefined(latest.version) || latest.version < version)
{
templatesById[id].latest = template;
}
}
});
return templatesById;
}
/**
* Finds the list of templates that match the given criteria within a template index.
*
* @param {string|djs.model.Base} [elementOrTemplateId]
* @param {Object} templatesIndex
* @param {Object} [options]
* @param {boolean} [options.latest]
* @param {boolean} [options.deprecated]
*
* @return {Array<Object>}
*/
function findTemplates(elementOrTemplateId, templatesIndex, options = {}) {
const {
latest: includeLatestOnly,
deprecated: includeDeprecated
} = options;
const getVersions = (template) => {
const { latest, ...versions } = template;
return includeLatestOnly ? (
!includeDeprecated && (latest && latest.deprecated) ? [] : (latest ? [ latest ] : [])
) : values(versions) ;
};
if (isNil(elementOrTemplateId)) {
return flatten(values(templatesIndex).map(getVersions));
}
if (isObject(elementOrTemplateId)) {
const element = elementOrTemplateId;
return filter(findTemplates(null, templatesIndex, options), function(template) {
return isAny(element, template.appliesTo);
}) || [];
}
if (isString(elementOrTemplateId)) {
return templatesIndex[ elementOrTemplateId ] && getVersions(templatesIndex[ elementOrTemplateId ]);
}
throw new Error('argument must be of type {string|djs.model.Base|undefined}');
}
/**
* Get template id for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateId(element) {
const businessObject = getBusinessObject(element);
if (businessObject) {
return businessObject.get(TEMPLATE_ID_ATTR);
}
}
/**
* Get template version for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateVersion(element) {
const businessObject = getBusinessObject(element);
if (businessObject) {
return businessObject.get(TEMPLATE_VERSION_ATTR);
}
}
/**
* Find extension with given type in
* BPMN element, diagram element or ExtensionElement.
*
* @param {ModdleElement|djs.model.Base} element
* @param {String} type
*
* @return {ModdleElement} the extension
*/
function findExtension(element, type) {
const businessObject = getBusinessObject(element);
let extensionElements;
if (is(businessObject, 'bpmn:ExtensionElements')) {
extensionElements = businessObject;
} else {
extensionElements = businessObject.get('extensionElements');
}
if (!extensionElements) {
return null;
}
return extensionElements.get('values').find((value) => {
return is(value, type);
});
}
function findExtensions(element, types) {
const extensionElements = getExtensionElements(element);
if (!extensionElements) {
return [];
}
return extensionElements.get('values').filter((value) => {
return isAny(value, types);
});
}
function findCamundaErrorEventDefinition(element, errorRef) {
const errorEventDefinitions = findExtensions(element, [ 'camunda:ErrorEventDefinition' ]);
let error;
// error ID has to start with <Error_${ errorRef }_>
return errorEventDefinitions.find((definition) => {
error = definition.get('bpmn:errorRef');
if (error) {
return error.get('bpmn:id').startsWith(`Error_${ errorRef }`);
}
});
}
// helpers //////////
function getExtensionElements(element) {
const businessObject = getBusinessObject(element);
if (is(businessObject, 'bpmn:ExtensionElements')) {
return businessObject;
} else {
return businessObject.get('extensionElements');
}
}
// eslint-disable-next-line no-undef
const packageVersion = "2.18.0";
/**
* @typedef {import('bpmn-js/lib/model/Types').Element} Element
*/
/**
* Registry for element templates.
*/
let ElementTemplates$1 = class ElementTemplates {
constructor(commandStack, eventBus, modeling, injector, config) {
this._commandStack = commandStack;
this._eventBus = eventBus;
this._injector = injector;
this._modeling = modeling;
this._templatesById = {};
this._templates = [];
config = config || {};
this._engines = this._coerceEngines(config.engines || {});
eventBus.on('elementTemplates.engines.changed', event => {
this.set(this._templates);
});
}
/**
* Get template with given ID and optional version or for element.
*
* @param {String|Element} elementOrTemplateId
* @param {number} [version]
*
* @return {ElementTemplate}
*/
get(elementOrTemplateId, version) {
const templates = this._templatesById;
let element;
if (isUndefined(elementOrTemplateId)) {
return null;
} else if (isString(elementOrTemplateId)) {
if (isUndefined(version)) {
version = '_';
}
if (templates[ elementOrTemplateId ] && templates[ elementOrTemplateId ][ version ]) {
return templates[ elementOrTemplateId ][ version ];
} else {
return null;
}
} else {
element = elementOrTemplateId;
return this.get(this._getTemplateId(element), this._getTemplateVersion(element));
}
}
/**
* Get default template for given element.
*
* @param {Element} element
*
* @return {ElementTemplate}
*/
getDefault(element) {
return find(this.getAll(element), function(template) {
return template.isDefault;
}) || null;
}
/**
* Get all templates (with given ID or applicable to element).
*
* @param {string|Element} [elementOrTemplateId]
* @return {Array<ElementTemplate>}
*/
getAll(elementOrTemplateId) {
return findTemplates(elementOrTemplateId, this._templatesById, { deprecated: true });
}
/**
* Get all templates (with given ID or applicable to element) with the latest
* version.
*
* @param {String|Element} [elementOrTemplateId]
* @param {{ deprecated?: boolean }} [options]
*
* @return {Array<ElementTemplate>}
*/
getLatest(elementOrTemplateId, options = {}) {
return findTemplates(elementOrTemplateId, this._templatesById, {
...options,
latest: true
});
}
/**
* Get templates compatible with a given engine configuration override.
*
* @param {string|Element} [elementOrTemplateId]
* @param {Object} enginesOverrides
* @param {Object} [options]
* @param {boolean} [options.deprecated=false]
* @param {boolean} [options.latest=true]
*
* @returns {Array<ElementTemplate>}
*/
getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) {
const overridenEngines = this._coerceEngines({ ...this._engines, ...enginesOverrides });
const templatesById = buildTemplatesById(this._templates, overridenEngines);
return findTemplates(elementOrTemplateId, templatesById, {
latest: true,
...options
});
}
/**
* Set templates.
*
* @param {Array<ElementTemplate>} templates
*/
set(templates) {
this._templates = templates;
this._templatesById = buildTemplatesById(this._templates, this._engines);
this._fire('changed');
}
getEngines() {
return this._engines;
}
setEngines(engines) {
this._engines = this._coerceEngines(engines);
this._fire('engines.changed');
}
/**
* Ensures that only valid engines are kept around
*
* @param { Record<string, string> } engines
*
* @return { Record<string, string> } filtered, valid engines
*/
_coerceEngines(engines) {
// we provide <elementTemplates> engine with the current
// package version; templates may use that engine to declare
// compatibility with this library
engines = {
elementTemplates: packageVersion,
...engines
};
return reduce(engines, (validEngines, version, engine) => {
const coercedVersion = coerce(version);
if (!valid(coercedVersion)) {
console.error(
new Error(`Engine <${ engine }> specifies unparseable version <${version}>`)
);
return validEngines;
}
return {
...validEngines,
[ engine ]: coercedVersion.raw
};
}, {});
}
/**
* Check if template is compatible with currently set engine version.
*
* @param {ElementTemplate} template
*
* @return {boolean} - true if compatible or no engine is set for elementTemplates or template.
*/
isCompatible(template) {
return isCompatible(template, this._engines);
}
/**
* Get engines that are incompatible with the template.
*
* @param {any} template
*
* @return { Record<string, { required: string, found: string } } - incompatible engines along with their template and local versions
*/
getIncompatibleEngines(template) {
return getIncompatibleEngines(template, this._engines);
}
/**
* Get template versions for a given element or template ID.
*
* @param {object|string|null} id
* @param { { latest?: boolean, deprecated?: boolean } [options]
*
* @return {Array<ElementTemplate>}
*/
_getTemplateVerions(id, options = {}) {
return findTemplates(id, this._templatesById, options);
}
_getTemplateId(element) {
return getTemplateId(element);
}
_getTemplateVersion(element) {
return getTemplateVersion(element);
}
/**
* Apply element template to a given element.
*
* @param {Element} element
* @param {ElementTemplate} newTemplate
*
* @return {Element} the updated element
*/
applyTemplate(element, newTemplate) {
const oldTemplate = this.get(element);
const context = {
element,
newTemplate,
oldTemplate
};
const event = oldTemplate?.id === newTemplate?.id ? 'update' : 'apply';
this._commandStack.execute('propertiesPanel.camunda.changeTemplate', context);
this._fire(event, {
element,
newTemplate,
oldTemplate
});
return context.element;
}
_fire(action, payload) {
return this._eventBus.fire(`elementTemplates.${action}`, payload);
}
/**
* Remove template from a given element.
*
* @param {Element} element
*
* @return {Element} the updated element
*/
removeTemplate(element) {
const oldTemplate = this.get(element);
const context = {
element,
oldTemplate
};
this._commandStack.execute('propertiesPanel.removeTemplate', context);
this._fire('remove', {
element,
oldTemplate
});
return context.newElement;
}
/**
* Unlink template from a given element.
*
* @param {Element} element
*
* @return {Element} the updated element
*/
unlinkTemplate(element) {
const oldTemplate = this.get(element);
const context = {
element,
oldTemplate
};
this._commandStack.execute('propertiesPanel.unlinkTemplate', context);
this._fire('unlink', {
element,
oldTemplate
});
return context.element;
}
};
ElementTemplates$1.$inject = [
'commandStack',
'eventBus',
'modeling',
'injector',
'config.elementTemplates'
];
const PROPERTY_TYPE = 'property';
const ZEBBE_PROPERTY_TYPE = 'zeebe:property';
const ZEBBE_INPUT_TYPE = 'zeebe:input';
const ZEEBE_OUTPUT_TYPE = 'zeebe:output';
const ZEEBE_PROPERTY_TYPE = 'zeebe:property';
const ZEEBE_TASK_DEFINITION_TYPE_TYPE = 'zeebe:taskDefinition:type';
const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition';
const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader';
const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property';
const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property';
const SIGNAL_PROPERTY_TYPE = 'bpmn:Signal#property';
const TIMER_EVENT_DEFINITION_PROPERTY_TYPE = 'bpmn:TimerEventDefinition#property';
const ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement';
const ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource';
const ZEEBE_USER_TASK = 'zeebe:userTask';
const ZEEBE_CALLED_DECISION = 'zeebe:calledDecision';
const ZEEBE_FORM_DEFINITION = 'zeebe:formDefinition';
const ZEEBE_SCRIPT_TASK = 'zeebe:script';
const ZEEBE_ASSIGNMENT_DEFINITION = 'zeebe:assignmentDefinition';
const ZEEBE_PRIORITY_DEFINITION = 'zeebe:priorityDefinition';
const ZEEBE_AD_HOC = 'zeebe:adHoc';
const ZEEBE_TASK_SCHEDULE = 'zeebe:taskSchedule';
const EXTENSION_BINDING_TYPES = [
MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE,
ZEBBE_INPUT_TYPE,
ZEEBE_OUTPUT_TYPE,
ZEEBE_PROPERTY_TYPE,
ZEEBE_TASK_DEFINITION_TYPE_TYPE,
ZEEBE_TASK_DEFINITION,
ZEEBE_TASK_HEADER_TYPE,
ZEEBE_CALLED_ELEMENT,
ZEEBE_LINKED_RESOURCE_PROPERTY,
ZEEBE_CALLED_DECISION,
ZEEBE_FORM_DEFINITION,
ZEEBE_SCRIPT_TASK,
ZEEBE_ASSIGNMENT_DEFINITION,
ZEEBE_PRIORITY_DEFINITION,
ZEEBE_AD_HOC,
ZEEBE_TASK_SCHEDULE
];
const TASK_DEFINITION_TYPES = [
ZEEBE_TASK_DEFINITION_TYPE_TYPE,
ZEEBE_TASK_DEFINITION
];
const IO_BINDING_TYPES = [
ZEBBE_INPUT_TYPE,
ZEEBE_OUTPUT_TYPE
];
const MESSAGE_BINDING_TYPES = [
MESSAGE_PROPERTY_TYPE,
MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE
];
const PROPERTY_BINDING_TYPES = [
PROPERTY_TYPE,
MESSAGE_PROPERTY_TYPE,
SIGNAL_PROPERTY_TYPE
];
/**
* Check whether a given timer expression type is supported for a given element.
*
* @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration'
* @param {Element} element
* @return {boolean}
*/
function isTimerExpressionTypeSupported(type, element) {
const businessObject = getBusinessObject(element);
switch (type) {
case 'timeDate':
return isAny(element, [
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:StartEvent'
]);
case 'timeCycle':
if (is(element, 'bpmn:StartEvent') && (!hasParentEventSubProcess(businessObject) || !isInterrupting(businessObject))) {
return true;
}
if (is(element, 'bpmn:BoundaryEvent') && !isInterrupting(businessObject)) {
return true;
}
return false;
case 'timeDuration':
if (isAny(element, [
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent'
])) {
return true;
}
if (is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) {
return true;
}
return false;
default:
return false;
}
}
/**
* Check if the template is a timer template and if so, whether it is applicable
*
* @param {Object} template
* @param {Element} element
* @return {boolean}
*/
function isTimerTemplateApplicable(template, element) {
// Find timer binding in template
const timerBinding = template.properties?.find(property => {
return property.binding?.type === TIMER_EVENT_DEFINITION_PROPERTY_TYPE;
});
// No timer binding - template is applicable
if (!timerBinding) {
return true;
}
const timerType = timerBinding.binding.name;
// Check if the timer type can be applied (possibly with auto-conversion)
return canTimerTypeBeAppliedWithConversion(timerType, element);
}
/**
* Check if a timer type can be applied to an element, possibly with auto-conversion.
*
* Only blocks truly impossible combinations:
* - timeDuration on process-level start events (can't be converted)
*
* @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration'
* @param {Element} element
* @return {boolean}
*/
function canTimerTypeBeAppliedWithConversion(type, element) {
const businessObject = getBusinessObject(element);
switch (type) {
case 'timeDate':
return isAny(element, [
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:StartEvent'
]);
case 'timeCycle':
if (isAny(element, [ 'bpmn:StartEvent', 'bpmn:BoundaryEvent' ])) {
return true;
}
return false;
case 'timeDuration':
if (isAny(element, [
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent'
])) {
return true;
}
// timeDuration can only be applied to start events in event subprocesses
// Process-level start events cannot support timeDuration (no conversion possible)
if (is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) {
return true;
}
return false;
default:
return false;
}
}
// helpers //////////
function isInterrupting(businessObject) {
if (is(businessObject, 'bpmn:BoundaryEvent')) {
return businessObject.get('cancelActivity') !== false;
}
return businessObject.get('isInterrupting') !== false;
}
function hasParentEventSubProcess(businessObject) {
const parent = businessObject.$parent;
return parent && is(parent, 'bpmn:SubProcess') && parent.get('triggeredByEvent');
}
/**
* @typedef {import('bpmn-js/lib/model/Types').Element} Element
*/
/**
* Filters to determine whether a template is applicable to a given element.
* @type {Array<(template: ElementTemplate, element: Element) => boolean>}
*/
const TEMPLATE_FILTERS = [
isTimerTemplateApplicable
];
/**
* Registry for element templates.
*/
class ElementTemplates extends ElementTemplates$1 {
constructor(templateElementFactory, commandStack, eventBus, modeling, injector, config) {
super(commandStack, eventBus, modeling, injector, config);
this._templateElementFactory = templateElementFactory;
}
_getTemplateId(element) {
return getTemplateId$1(element);
}
_getTemplateVersion(element) {
return getTemplateVersion$1(element);
}
/**
* Get all templates (with given ID or applicable to element).
*
* @param {string|djs.model.Base} [elementOrTemplateId]
* @return {Array<ElementTemplate>}
*/
getAll(elementOrTemplateId) {
const templates = super.getAll(elementOrTemplateId);
if (isObject(elementOrTemplateId)) {
return this._filterApplicableTemplates(templates, elementOrTemplateId);
}
return templates;
}
/**
* Get all templates (with given ID or applicable to element) with the latest version.
*
* @param {String|djs.model.Base} [elementOrTemplateId]
* @param {{ deprecated?: boolean }} [options]
*
* @return {Array<ElementTemplate>}
*/
getLatest(elementOrTemplateId, options = {}) {
const templates = super.getLatest(elementOrTemplateId, options);
if (isObject(elementOrTemplateId)) {
return this._filterApplicableTemplates(templates, elementOrTemplateId);
}
return templates;
}
/**
* Get compatible templates for element with optional engine overrides.
*
* @param {String|djs.model.Base} [elementOrTemplateId]
* @param {Object} [enginesOverrides]
* @param {Object} [options]
*
* @return {Array<ElementTemplate>}
*/
getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) {
const templates = super.getCompatible(elementOrTemplateId, enginesOverrides, options);
if (isObject(elementOrTemplateId)) {
return this._filterApplicableTemplates(templates, elementOrTemplateId);
}
return templates;
}
_filterApplicableTemplates(templates, element) {
return TEMPLATE_FILTERS.reduce(
(filteredTemplates, filterFn) =>
filteredTemplates.filter(template => filterFn(template, element)),
templates
);
}
/**
* Create an element based on an element template. This is, for example,
* called from the create-append anything menu.
*
* @param {ElementTemplate} template
* @returns {djs.model.Base}
*/
createElement(template) {
if (!template) {
throw new Error('template is missing');
}
const element = this._templateElementFactory.create(template);
return element;
}
/**
* Apply element template to a given element.
*
* @param {Element} element
* @param {ElementTemplate} newTemplate
*
* @return {Element} the updated element
*/
applyTemplate(element, newTemplate) {
const oldTemplate = this.get(element);
const context = {
element,
newTemplate,
oldTemplate
};
const event = oldTemplate?.id === newTemplate?.id ? 'update' : 'apply';
this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', context);
this._fire(event, {
element,
newTemplate,
oldTemplate
});
return context.element;
}
/**
* Remove template from a given element.
*
* @param {Element} element
*
* @return {Element} the updated element
*/
removeTemplate(element) {
const oldTemplate = this.get(element);
const context = {
element,
oldTemplate
};
this._commandStack.execute('propertiesPanel.removeTemplate', context);
this._fire('remove', {
element,
oldTemplate
});
return context.element;
}
}
ElementTemplates.$inject = [
'templateElementFactory',
'commandStack',
'eventBus',
'modeling',
'injector',
'config.elementTemplates',
];
const SUPPORTED_SCHEMA_VERSION$1 = getSchemaVersion$1();
const MORPHABLE_TYPES = [ 'bpmn:Activity', 'bpmn:Event', 'bpmn:Gateway' ];
/**
* A element template validator.
*/
let Validator$1 = class Validator {
constructor(moddle) {
this._templatesById = {};
this._validTemplates = [];
this._errors = [];
this._moddle = moddle;
}
/**
* Adds the templates.
*
* @param {Array<TemplateDescriptor>} templates
*
* @return {Validator}
*/
addAll(templates) {
if (!isArray(templates)) {
this._logError('templates must be []');
} else {
templates.forEach(this.add, this);
}
return this;
}
/**
* Add the given element template, if it is valid.
*
* @param {TemplateDescriptor} template
*
* @return {Validator}
*/
add(template) {
const err = this._validateTemplate(template);
let id, version;
if (!err) {
id = template.id;
version = template.version || '_';
if (!this._templatesById[ id ]) {
this._templatesById[ id ] = {};
}
this._templatesById[ id ][ version ] = template;
this._validTemplates.push(template);
}
return this;
}
/**
* Validate given template and return error (if any).
*
* @param {TemplateDescriptor} template
*
* @return {Error} validation error, if any
*/
_validateTemplate(template) {
const id = template.id,
version = template.version || '_',
schemaVersion = template.$schema && getSchemaVersion(template.$schema);
// (1) compatibility
if (schemaVersion && (semverCompare(SUPPORTED_SCHEMA_VERSION$1, schemaVersion) < 0)) {
return this._logError(
`unsupported element template schema version <${ schemaVersion }>. Your installation only supports up to version <${ SUPPORTED_SCHEMA_VERSION$1 }>. Please update your installation`,
template
);
}
// (2) versioning
if (this._templatesById[ id ] && this._templatesById[ id ][ version ]) {
if (version === '_') {
return this._logError(`template id <${ id }> already used`, template);
} else {
return this._logError(`template id <${ id }> and version <${ version }> already used`, template);
}
}
// (3) elementType validation
const elementTypeError = this._validateElementType(template);
if (elementTypeError) {
return elementTypeError;
}
// (4) JSON schema compliance
const schemaValidationResult = validate(template);
const {
errors: schemaErrors,
valid
} = schemaValidationResult;
if (!valid) {
filteredSchemaErrors(schemaErrors).forEach((error) => {
this._logError(error.message, template);
});
return new Error('invalid template');
}
// (5) engines validation
const enginesError = this._validateEngines(template);
if (enginesError) {
return enginesError;
}
return null;
}
_validateEngines(template) {
let err;
forEach(template.engines, (rangeStr, engine) => {
if (!validRange(rangeStr)) {
err = this._logError(new Error(
`Engine <${engine}> specifies invalid semver range <${rangeStr}>`
), template);
}
});
return err;
}
/**
* Validate elementType for given template and return error (if any).
*
* @param {TemplateDescriptor} template
*
* @return {Error} validation error, if any
*/
_validateElementType(template) {
if (template.elementType && template.appliesTo) {
const elementType = template.elementType.value,
appliesTo = template.appliesTo;
// (3.1) template can be applied to elementType
// prevents cases where the elementType is not part of appliesTo
if (!appliesTo.find(type => this._isType(elementType, type))) {
return this._logError(`template does not apply to requested element type <${ elementType }>`, template);
}
// (3.2) template only applies to same type of element
// prevent elementTemplates to morph into incompatible types, e.g. Task -> SequenceFlow
for (const sourceType of appliesTo) {
if (!this._canMorph(sourceType, elementType)) {
return this._logError(`can not morph <${sourceType}> into <${elementType}>`, template);
}
}
}
}
/**
* Check if given type is a subtype of given base type.
*
* @param {String} type
* @param {String} baseType
* @returns {Boolean}
*/
_isType(type, baseType) {
const moddleType = this._moddle.getType(type);
return moddleType && (baseType in this._moddle.getElementDescriptor(moddleType).allTypesByName);
}
/**
* Checks if a given type can be morphed into another type.
*
* @param {String} sourceType
* @param {String} targetType
* @returns {Boolean}
*/
_canMorph(sourceType, targetType) {
if (sourceType === targetType) {
return true;
}
const baseType = MORPHABLE_TYPES.find(type => this._isType(sourceType, type));
if (!baseType) {
return false;
}
return this._isType(targetType, baseType);
}
/**
* Log an error for the given template
*
* @param {(String|Error)} err
* @param {TemplateDescriptor} template
*
* @return {Error} logged validation errors
*/
_logError(err, template) {
if (isString(err)) {
if (template) {
const {
id,
name
} = template;
err = `template(id: <${ id }>, name: <${ name }>): ${ err }`;
}
err = new Error(err);
}
this._errors.push(err);
return err;
}
getErrors() {
return this._errors;
}
getValidTemplates() {
return this._validTemplates;
}
};
// helpers //////////
/**
* Extract schema version from schema URI
*
* @param {String} schemaUri - for example https://unpkg.com/@camunda/element-templates-json-schema@99.99.99/resources/schema.json
*
* @return {String} for example '99.99.99'
*/
function getSchemaVersion(schemaUri) {
const re = /\d+\.\d+\.\d+/g;
const match = schemaUri.match(re);
return match === null ? undefined : match[ 0 ];
}
/**
* Extract only relevant errors of the validation result.
*
* The JSON Schema we use under the hood produces more errors than we need for a
* detected schema violation (for example, unmatched sub-schemas, if-then-rules,
* `oneOf`-definitions ...).
*
* We call these errors "relevant" that have a custom error message defined by us OR
* are basic data type errors.
*
* @param {Array} schemaErrors
*
* @return {Array}
*/
function filteredSchemaErrors(schemaErrors) {
return filter(schemaErrors, (err) => {
const {
instancePath,
keyword
} = err;
// (1) regular errors are customized from the schema
if (keyword === 'errorMessage') {
return true;
}
// (2) data type errors
// ignore type errors nested in scopes
if (keyword === 'type' && instancePath && !instancePath.startsWith('/scopes/')) {
return true;
}
return false;
});
}
const SUPPORTED_SCHEMA_VERSION = getZeebeSchemaVersion();
const SUPPORTED_SCHEMA_PACKAGE = getZeebeSchemaPackage();
/**
* A Camunda Cloud element template validator.
*/
class Validator extends Validator$1 {
constructor(moddle) {
super(moddle);
}
/**
* Validate given template and return error (if any).
*
* @param {TemplateDescriptor} template
*
* @return {Error} validation error, if any
*/
_validateTemplate(template) {
const id = template.id,
version = template.version || '_',
schema = template.$schema,
schemaVersion = schema && getSchemaVersion(schema);
// (1) $schema attribute defined
if (!schema) {
return this._logError(
'missing $schema attribute.',
template
);
}
if (!this.isSchemaValid(schema)) {
return this._logError(
`unsupported $schema attribute <${ schema }>.`,
template
);
}
// (2) compatibility
if (schemaVersion && (semverCompare(SUPPORTED_SCHEMA_VERSION, schemaVersion) < 0)) {
return this._logError(
`unsupported element template schema version <${ schemaVersion }>. Your installation only supports up to version <${ SUPPORTED_SCHEMA_VERSION }>. Please update your installation`,
template
);
}
// (3) versioning
if (this._templatesById[ id ] && this._templatesById[ id ][ version ]) {
if (version === '_') {
return this._logError(`template id <${ id }> already used`, template);
} else {
return this._logError(`template id <${ id }> and version <${ version }> already used`, template);
}
}
// (4) elementType validation
const elementTypeError = this._validateElementType(template);
if (elementTypeError) {
return elementTypeError;
}
// (5) JSON schema compliance
const schemaValidationResult = validateZeebe(template);
const {
errors: schemaErrors,
valid
} = schemaValidationResult;
if (!valid) {
filteredSchemaErrors(schemaErrors).forEach((error) => {
this._logError(error.message, template);
});
return new Error('invalid template');
}
// (6) engines validation
const enginesError = this._validateEngines(template);
if (enginesError) {
return enginesError;
}
return null;
}
isSchemaValid(schema) {
return schema && schema.includes(SUPPORTED_SCHEMA_PACKAGE);
}
_validateEngines(template) {
let err;
forEach(template.engines, (rangeStr, engine) => {
if (!validRange(rangeStr)) {
err = this._logError(new Error(
`Engine <${engine}> specifies invalid semver range <${rangeStr}>`
), template);
}
});
return err;
}
}
/**
* The guy responsible for template loading.
*
* Provide the actual templates via the `config.elementTemplates`.
*
* That configuration can either be an array of template
* descriptors or a node style callback to retrieve
* the templates asynchronously.
*
* @param {Array<TemplateDescriptor>|Function} config
* @param {EventBus} eventBus
* @param {ElementTemplates} elementTemplates
* @param {Moddle} moddle
*/
let ElementTemplatesLoader$1 = class ElementTemplatesLoader {
constructor(config, eventBus, elementTemplates, moddle) {
this._loadTemplates;
this._eventBus = eventBus;
this._elementTemplates = elementTemplates;
this._moddle = moddle;
if (isArray(config) || isFunction(config)) {
this._loadTemplates = config;
}
if (config && config.loadTemplates) {
this._loadTemplates = config.loadTemplates;
}
eventBus.on('diagram.init', () => {
this.reload();
});
}
reload() {
const loadTemplates = this._loadTemplates;
// no templates specified
if (isUndefined(loadTemplates)) {
return;
}
// template loader function specified
if (isFunction(loadTemplates)) {
return loadTemplates((err, templates) => {
if (err) {
return this._templateErrors([ err ]);
}
this.setTemplates(templates);
});
}
// templates array specified
if (loadTemplates.length) {
return this.setTemplates(loadTemplates);
}
}
setTemplates(templates) {
const elementTemplates = this._elementTemplates,
moddle = this._moddle;
const validator = new Validator$1(moddle).addAll(templates);
const errors = validator.getErrors(),
validTemplates = validator.getValidTemplates();
elementTemplates.set(validTemplates);
if (errors.length) {
this._templateErrors(errors);
}
}
_templateErrors(errors) {
this._elementTemplates._fire('errors', {
errors: errors
});
}
};
ElementTemplatesLoader$1.$inject = [
'config.elementTemplates',
'eventBus',
'elementTemplates',
'moddle'
];
/**
* @param {Object|Array<TemplateDescriptor>|Function} config
* @param {EventBus} eventBus
* @param {ElementTemplates} elementTemplates
* @param {Moddle} moddle
*/
class ElementTemplatesLoader extends ElementTemplatesLoader$1 {
constructor(config, eventBus, elementTemplates, moddle) {
super(config, eventBus, elementTemplates, moddle);
this._elementTemplates = elementTemplates;
}
setTemplates(templates) {
const elementTemplates = this._elementTemplates,
moddle = this._moddle;
const validator = new Validator(moddle).addAll(templates);
const errors = validator.getErrors(),
validTemplates = validator.getValidTemplates();
elementTemplates.set(validTemplates);
if (errors.length) {
this._templateErrors(errors);
}
}
}
ElementTemplatesLoader.$inject = [
'config.elementTemplates',
'eventBus',
'elementTemplates',
'moddle'
];
/**
* Create a new element and set its parent.
*
* @param {String} elementType of the new element
* @param {Object} properties of the new element in key-value pairs
* @param {moddle.object} parent of the new element
* @param {BpmnFactory} factory which creates the new element
*
* @returns {djs.model.Base} element which is created
*/
function createElement(elementType, properties, parent, factory) {
const element = factory.create(elementType, properties);
if (parent) {
element.$parent = parent;
}
return element;
}
/**
* generate a semantic id with given prefix
*/
function nextId(prefix) {
const ids = new Ids([ 32,32,1 ]);
return ids.nextPrefixed(prefix);
}
function getRoot(businessObject) {
let parent = businessObject;
while (parent.$parent) {
parent = parent.$parent;
}
return parent;
}
/**
* Check if element type is a subprocess (either regular or AdHoc).
*
* @param {string} elementType
* @returns {boolean}
*/
function isSubprocess(elementType) {
return elementType === 'bpmn:SubProcess' || elementType === 'bpmn:AdHocSubProcess' || elementType === 'bpmn:Transaction';
}
/**
* Create an input parameter representing the given
* binding and value.
*
* @param {PropertyBinding} binding
* @param {String} value
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createInputParameter$1(binding, value, bpmnFactory) {
const {
name
} = binding;
return bpmnFactory.create('zeebe:Input', {
source: value,
target: name
});
}
/**
* Create an output parameter representing the given
* binding and value.
*
* @param {PropertyBinding} binding
* @param {String} value
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createOutputParameter$1(binding, value, bpmnFactory) {
const {
source
} = binding;
return bpmnFactory.create('zeebe:Output', {
source,
target: value
});
}
/**
* Create a task header representing the given
* binding and value.
*
* @param {PropertyBinding} binding
* @param {String} value
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createTaskHeader(binding, value, bpmnFactory) {
const {
key
} = binding;
return bpmnFactory.create('zeebe:Header', {
key,
value
});
}
/**
* Create a task definition representing the given value.
*
* @param {object} attrs
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createTaskDefinition(attrs = {}, bpmnFactory) {
return bpmnFactory.create('zeebe:TaskDefinition', attrs);
}
/**
* Create zeebe:Property from the given binding.
*
* @param {PropertyBinding} binding
* @param {String} value
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createZeebeProperty(binding, value = '', bpmnFactory) {
const { name } = binding;
return bpmnFactory.create('zeebe:Property', {
name,
value
});
}
/**
* Retrieves whether an element should be updated for a given property.
*
* That matches once
* a) the property value is not empty, or
* b) the property is not optional
*
* @param {String} value
* @param {Object} property
* @returns {Boolean}
*/
function shouldUpdate(value, property) {
const { optional } = property;
return value || !optional;
}
/**
* Gets or, in case not existent, creates extension element for given element.
*
* @param {djs.model.Base} element
* @param {String} type
* @param {BpmnFactory} bpmnFactory
* @returns {ModdleElement}
*/
function ensureExtension(element, type, bpmnFactory) {
const businessObject = getBusinessObject(element);
let extensionElements = businessObject.get('extensionElements');
if (!extensionElements) {
extensionElements = createElement('bpmn:ExtensionElements', {}, businessObject, bpmnFactory);
businessObject.set('extensionElements', extensionElements);
}
let extension = findExtension$1(extensionElements, type);
if (!extension) {
extension = bpmnFactory.create(type);
extension.$parent = extensionElements;
extensionElements.get('values').push(extension);
}
return extension;
}
function getTaskDefinitionPropertyName(binding) {
return binding.type === ZEEBE_TASK_DEFINITION_TYPE_TYPE ? 'type' : binding.property;
}
function removeRootElement(rootElement, injector) {
const modeling = injector.get('modeling'),
canvas = injector.get('canvas'),
bpmnjs = injector.get('bpmnjs');
const element = canvas.getRootElement(),
definitions = bpmnjs.getDefinitions(),
rootElements = definitions.get('rootElements');
const newRootElements = rootElements.filter(e => e !== rootElement);
// short-circuit to prevent unnecessary updates
if (newRootElements.length === rootElements.length) {
return;
}
modeling.updateModdleProperties(element, definitions, {
rootElements: newRootElements
});
}
/**
* Remove message from element and the diagram.
*
* @param {import('bpmn-js/lib/model/Types').Element} element
* @param {import('didi').Injector} injector
*/
function removeMessage(element, injector) {
const modeling = injector.get('modeling');
const bo = getReferringElement(element);
// Event does not have an event definition
if (!bo) {
return;
}
const message = findMessage(bo);
if (!message) {
return;
}
modeling.updateModdleProperties(element, bo, {
messageRef: undefined
});
removeRootElement(message, injector);
}
/**
* Remove signal from element and the diagram.
*
* @param {import('bpmn-js/lib/model/Types').Element} element
* @param {import('didi').Injector} injector
*/
function removeSignal(element, injector) {
const modeling = injector.get('modeling');
const bo = getReferringElement(element);
// Event does not have an event definition
if (!bo) {
return;
}
const signal = findSignal(bo);
if (!signal) {
return;
}
modeling.updateModdleProperties(element, bo, {
signalRef: undefined
});
removeRootElement(signal, injector);
}
function getReferringElement(element) {
const bo = getBusinessObject(element);
if (is(bo, 'bpmn:Event')) {
return bo.get('eventDefinitions')[0];
}
return bo;
}
const EXPRESSION_TYPES$1 = [
'bpmn:Expression',
'bpmn:FormalExpression'
];
function isExpression(element, propertyName) {
const bo = getBusinessObject(element);
const descriptor = bo.$descriptor.propertiesByName[propertyName];
return descriptor && EXPRESSION_TYPES$1.includes(descriptor.type);
}
function createExpression(value, parent, bpmnFactory) {
const expression = createElement('bpmn:FormalExpression', { body: value }, parent, bpmnFactory);
return expression;
}
function getExpressionValue(element, propertyName) {
const bo = getBusinessObject(element);
const expression = bo.get(propertyName);
return expression?.get('body') || undefined;
}
/**
* @typedef {import('bpmn-js/lib/model/Types').Element} Element
*/
/**
* Applies an element template to an element. Sets `zeebe:modelerTemplate` and
* `zeebe:modelerTemplateVersion`.
*/
let ChangeElementTemplateHandler$1 = class ChangeElementTemplateHandler {
constructor(bpmnFactory, bpmnReplace, commandStack, modeling, moddleCopy, injector) {
this._bpmnFactory = bpmnFactory;
this._bpmnReplace = bpmnReplace;
this._modeling = modeling;
this._moddleCopy = moddleCopy;
this._commandStack = commandStack;
this._injector = injector;
}
/**
* Change element template. If new template is specified, apply it. If old
* template is specified, too, update from old to new. If only old template is
* specified, remove it. Optionally, remove old template properties.
*
* @param {Object} context
* @param {Element} context.element
* @param {Object} [context.oldTemplate]
* @param {Object} [context.newTemplate]
* @param {boolean} [context.removeProperties=false]
*/
preExecute(context) {
const {
newTemplate,
oldTemplate,