bpmn-js-element-templates
Version:
Element templates for bpmn-js
1,675 lines (1,572 loc) • 451 kB
JavaScript
'use strict';
var ModelUtil = require('bpmn-js/lib/util/ModelUtil');
var uuid = require('uuid');
var minDash = require('min-dash');
var semver = require('semver');
var semverCompare = require('semver-compare');
var elementTemplatesValidator = require('@bpmn-io/element-templates-validator');
var Ids = require('ids');
var LabelUtil = require('bpmn-js/lib/features/label-editing/LabelUtil');
var DrilldownUtil = require('bpmn-js/lib/util/DrilldownUtil');
var DiUtil = require('bpmn-js/lib/util/DiUtil');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var translateModule = require('diagram-js/lib/i18n/translate');
var propertiesPanel = require('@bpmn-io/properties-panel');
var classnames = require('classnames');
var bpmnJsPropertiesPanel = require('bpmn-js-properties-panel');
var jsxRuntime = require('@bpmn-io/properties-panel/preact/jsx-runtime');
var preact = require('@bpmn-io/properties-panel/preact');
var hooks = require('@bpmn-io/properties-panel/preact/hooks');
var zeebe = require('@bpmn-io/extract-process-variables/zeebe');
var minDom = require('min-dom');
var StaticResolver = require('bpmnlint/lib/resolver/static-resolver');
var EventBus = require('diagram-js/lib/core/EventBus');
/**
* 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 = ModelUtil.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 = ModelUtil.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 = ModelUtil.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 = ModelUtil.getBusinessObject(element);
let extensionElements;
if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
extensionElements = businessObject;
} else {
extensionElements = businessObject.get('extensionElements');
}
if (!extensionElements) {
return;
}
return extensionElements.get('values').find(value => {
return ModelUtil.is(value, type);
});
}
function findZeebeProperty(zeebeProperties, binding) {
return zeebeProperties.get('properties').find(value => {
return value.name === binding.name;
});
}
function findInputParameter$1(ioMapping, binding) {
const parameters = ioMapping.get('inputParameters');
return parameters.find(parameter => {
return parameter.target === binding.name;
});
}
function findOutputParameter$1(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;
});
}
function findMessage(businessObject) {
if (ModelUtil.is(businessObject, 'bpmn:Event')) {
const eventDefinitions = businessObject.get('eventDefinitions');
if (!eventDefinitions || !eventDefinitions.length) {
return;
}
businessObject = eventDefinitions[0];
}
if (!businessObject) {
return;
}
return businessObject.get('messageRef');
}
function getDefaultValue(property) {
if (shouldCastToFeel(property)) {
return toFeelExpression(property.value, property.type);
}
if (property.value !== undefined) {
return property.value;
}
if (property.generatedValue) {
const {
type
} = property.generatedValue;
if (type === 'uuid') {
return uuid.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';
/**
* Get template id for a given diagram element.
*
* @param {djs.model.Base} element
*
* @return {String}
*/
function getTemplateId(element) {
const businessObject = ModelUtil.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 = ModelUtil.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 = ModelUtil.getBusinessObject(element);
let extensionElements;
if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
extensionElements = businessObject;
} else {
extensionElements = businessObject.get('extensionElements');
}
if (!extensionElements) {
return null;
}
return extensionElements.get('values').find(value => {
return ModelUtil.is(value, type);
});
}
function findExtensions(element, types) {
const extensionElements = getExtensionElements(element);
if (!extensionElements) {
return [];
}
return extensionElements.get('values').filter(value => {
return ModelUtil.isAny(value, types);
});
}
function findCamundaInOut(element, binding) {
const extensionElements = getExtensionElements(element);
if (!extensionElements) {
return;
}
const {
type
} = binding;
let matcher;
if (type === 'camunda:in') {
matcher = element => {
return ModelUtil.is(element, 'camunda:In') && isInOut(element, binding);
};
} else if (type === 'camunda:out') {
matcher = element => {
return ModelUtil.is(element, 'camunda:Out') && isInOut(element, binding);
};
} else if (type === 'camunda:in:businessKey') {
matcher = element => {
return ModelUtil.is(element, 'camunda:In') && 'businessKey' in element;
};
}
return extensionElements.get('values').find(matcher);
}
function findCamundaProperty(camundaProperties, binding) {
return camundaProperties.get('values').find(value => {
return value.name === binding.name;
});
}
function findInputParameter(inputOutput, binding) {
const parameters = inputOutput.get('inputParameters');
return parameters.find(parameter => {
return parameter.name === binding.name;
});
}
function findOutputParameter(inputOutput, binding) {
const parameters = inputOutput.get('outputParameters');
return parameters.find(function (parameter) {
const {
value
} = parameter;
if (!binding.scriptFormat) {
return value === binding.source;
}
const definition = parameter.get('camunda:definition');
if (!definition || binding.scriptFormat !== definition.get('camunda:scriptFormat')) {
return false;
}
return definition.get('camunda:value') === binding.source;
});
}
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 = ModelUtil.getBusinessObject(element);
if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
return businessObject;
} else {
return businessObject.get('extensionElements');
}
}
function isInOut(element, binding) {
if (binding.type === 'camunda:in') {
// find based on target attribute
if (binding.target) {
return element.target === binding.target;
}
}
if (binding.type === 'camunda:out') {
// find based on source / sourceExpression
if (binding.source) {
return element.source === binding.source;
}
if (binding.sourceExpression) {
return element.sourceExpression === binding.sourceExpression;
}
}
// find based variables / local combination
if (binding.variables) {
return element.variables === 'all' && (binding.variables !== 'local' || element.local);
}
}
// eslint-disable-next-line no-undef
const packageVersion = "2.6.0";
/**
* 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|djs.model.Base} id
* @param {number} [version]
*
* @return {ElementTemplate}
*/
get(id, version) {
const templates = this._templatesById;
let element;
if (minDash.isUndefined(id)) {
return null;
} else if (minDash.isString(id)) {
if (minDash.isUndefined(version)) {
version = '_';
}
if (templates[id] && templates[id][version]) {
return templates[id][version];
} else {
return null;
}
} else {
element = id;
return this.get(this._getTemplateId(element), this._getTemplateVersion(element));
}
}
/**
* Get default template for given element.
*
* @param {djs.model.Base} element
*
* @return {ElementTemplate}
*/
getDefault(element) {
return minDash.find(this.getAll(element), function (template) {
return template.isDefault;
}) || null;
}
/**
* Get all templates (with given ID or applicable to element).
*
* @param {string|djs.model.Base} [id]
* @return {Array<ElementTemplate>}
*/
getAll(id) {
return this._getTemplateVerions(id, {
includeDeprecated: true
});
}
/**
* Get all templates (with given ID or applicable to element) with the latest
* version.
*
* @param {String|djs.model.Base} [id]
* @param {{ deprecated?: boolean }} [options]
*
* @return {Array<ElementTemplate>}
*/
getLatest(id, options = {}) {
return this._getTemplateVerions(id, {
...options,
latest: true
});
}
/**
* Set templates.
*
* @param {Array<ElementTemplate>} templates
*/
set(templates) {
this._templatesById = {};
this._templates = templates;
templates.forEach(template => {
const id = template.id;
const version = minDash.isUndefined(template.version) ? '_' : template.version;
if (!this._templatesById[id]) {
this._templatesById[id] = {};
}
this._templatesById[id][version] = template;
const latest = this._templatesById[id].latest;
if (this.isCompatible(template)) {
if (!latest || minDash.isUndefined(latest.version) || latest.version < version) {
this._templatesById[id].latest = template;
}
}
});
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 minDash.reduce(engines, (validEngines, version, engine) => {
const coercedVersion = semver.coerce(version);
if (!semver.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 !Object.keys(this.getIncompatibleEngines(template)).length;
}
/**
* 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) {
const localEngines = this._engines;
const templateEngines = template.engines;
return minDash.reduce(templateEngines, (result, _, engine) => {
if (!minDash.has(localEngines, engine)) {
return result;
}
if (!semver.satisfies(localEngines[engine], templateEngines[engine])) {
result[engine] = {
actual: localEngines[engine],
required: templateEngines[engine]
};
}
return result;
}, {});
}
/**
* @param {object|string|null} id
* @param { { latest?: boolean, deprecated?: boolean } [options]
*
* @return {Array<ElementTemplate>}
*/
_getTemplateVerions(id, options = {}) {
const {
latest: includeLatestOnly,
deprecated: includeDeprecated
} = options;
const templatesById = this._templatesById;
const getVersions = template => {
const {
latest,
...versions
} = template;
return includeLatestOnly ? !includeDeprecated && latest && latest.deprecated ? [] : latest ? [latest] : [] : minDash.values(versions);
};
if (minDash.isNil(id)) {
return minDash.flatten(minDash.values(templatesById).map(getVersions));
}
if (minDash.isObject(id)) {
const element = id;
return minDash.filter(this._getTemplateVerions(null, options), function (template) {
return ModelUtil.isAny(element, template.appliesTo);
}) || [];
}
if (minDash.isString(id)) {
return templatesById[id] && getVersions(templatesById[id]);
}
throw new Error('argument must be of type {string|djs.model.Base|undefined}');
}
_getTemplateId(element) {
return getTemplateId(element);
}
_getTemplateVersion(element) {
return getTemplateVersion(element);
}
/**
* Apply element template to a given element.
*
* @param {djs.model.Base} element
* @param {ElementTemplate} newTemplate
*
* @return {djs.model.Base} the updated element
*/
applyTemplate(element, newTemplate) {
let action = 'apply';
let payload = {
element,
newTemplate
};
const oldTemplate = this.get(element);
if (oldTemplate && !newTemplate) {
action = 'unlink';
payload = {
element
};
}
if (newTemplate && oldTemplate && newTemplate.id === oldTemplate.id) {
action = 'update';
}
const context = {
element,
newTemplate,
oldTemplate
};
this._commandStack.execute('propertiesPanel.camunda.changeTemplate', context);
this._fire(action, payload);
return context.element;
}
_fire(action, payload) {
return this._eventBus.fire(`elementTemplates.${action}`, payload);
}
/**
* Remove template from a given element.
*
* @param {djs.model.Base} element
*
* @return {djs.model.Base} the updated element
*/
removeTemplate(element) {
this._fire('remove', {
element
});
const context = {
element
};
this._commandStack.execute('propertiesPanel.removeTemplate', context);
return context.newElement;
}
/**
* Unlink template from a given element.
*
* @param {djs.model.Base} element
*
* @return {djs.model.Base} the updated element
*/
unlinkTemplate(element) {
return this.applyTemplate(element, null);
}
};
ElementTemplates$1.$inject = ['commandStack', 'eventBus', 'modeling', 'injector', 'config.elementTemplates'];
/**
* 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);
}
/**
* Create an element based on an element template.
*
* @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 {djs.model.Base} element
* @param {ElementTemplate} newTemplate
*
* @return {djs.model.Base} the updated element
*/
applyTemplate(element, newTemplate) {
let action = 'apply';
let payload = {
element,
newTemplate
};
const oldTemplate = this.get(element);
if (oldTemplate && !newTemplate) {
action = 'unlink';
payload = {
element
};
}
if (newTemplate && oldTemplate && newTemplate.id === oldTemplate.id) {
action = 'update';
}
const context = {
element,
newTemplate,
oldTemplate
};
this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', context);
this._eventBus.fire(`elementTemplates.${action}`, payload);
return context.element;
}
}
ElementTemplates.$inject = ['templateElementFactory', 'commandStack', 'eventBus', 'modeling', 'injector', 'config.elementTemplates'];
const SUPPORTED_SCHEMA_VERSION$1 = elementTemplatesValidator.getSchemaVersion();
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 (!minDash.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 = elementTemplatesValidator.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;
minDash.forEach(template.engines, (rangeStr, engine) => {
if (!semver.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 (minDash.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 minDash.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 = elementTemplatesValidator.getZeebeSchemaVersion();
const SUPPORTED_SCHEMA_PACKAGE = elementTemplatesValidator.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 = elementTemplatesValidator.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;
minDash.forEach(template.engines, (rangeStr, engine) => {
if (!semver.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 (minDash.isArray(config) || minDash.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 (minDash.isUndefined(loadTemplates)) {
return;
}
// template loader function specified
if (minDash.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;
}
function filterElementsByType(objectList, type) {
const list = objectList || [];
return list.filter(element => ModelUtil.is(element, type));
}
function findRootElementsByType(businessObject, referencedType) {
const root = getRoot(businessObject);
return filterElementsByType(root.get('rootElements'), referencedType);
}
function findRootElementById(businessObject, type, id) {
const elements = findRootElementsByType(businessObject, type);
return elements.find(element => element.id === id);
}
/**
* 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
});
}
/**
* Create a called element representing the given value.
*
* @param {object} attrs
* @param {BpmnFactory} bpmnFactory
*
* @return {ModdleElement}
*/
function createCalledElement(attrs = {}, bpmnFactory) {
return bpmnFactory.create('zeebe:CalledElement', attrs);
}
/**
* 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 = ModelUtil.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;
}
const PROPERTY_TYPE$1 = '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 ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement';
const ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource';
const ZEEBE_USER_TASK = 'zeebe:userTask';
const EXTENSION_BINDING_TYPES$1 = [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];
const TASK_DEFINITION_TYPES = [ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION];
const IO_BINDING_TYPES$1 = [ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE];
const MESSAGE_BINDING_TYPES = [MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE];
const PROPERTY_BINDING_TYPES = [PROPERTY_TYPE$1, MESSAGE_PROPERTY_TYPE];
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);
}
function getReferringElement(element) {
const bo = ModelUtil.getBusinessObject(element);
if (ModelUtil.is(bo, 'bpmn:Event')) {
return bo.get('eventDefinitions')[0];
}
return bo;
}
/**
* 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 an element's template and update its properties as specified in `newTemplate`. Specify
* `oldTemplate` to update from one template to another. If `newTemplate` isn't specified the
* `zeebe:modelerTemplate` and `zeebe:modelerTemplateVersion` properties will be removed from
* the element.
*
* @param {Object} context
* @param {Object} context.element
* @param {Object} [context.oldTemplate]
* @param {Object} [context.newTemplate]
*/
preExecute(context) {
let newTemplate = context.newTemplate,
oldTemplate = context.oldTemplate;
let element = context.element;
// update zeebe:modelerTemplate attribute
this._updateZeebeModelerTemplate(element, newTemplate);
// update zeebe:modelerTemplateIcon
this._updateZeebeModelerTemplateIcon(element, newTemplate);
if (newTemplate) {
// update element type
element = context.element = this._updateElementType(element, oldTemplate, newTemplate);
// update properties
this._updateProperties(element, oldTemplate, newTemplate);
// update zeebe:TaskDefinition
this._updateZeebeTaskDefinition(element, oldTemplate, newTemplate);
// update zeebe:Input and zeebe:Output properties
this._updateZeebeInputOutputParameterProperties(element, oldTemplate, newTemplate);
// update zeebe:Header properties
this._updateZeebeTaskHeaderProperties(element, oldTemplate, newTemplate);
// update zeebe:Property properties
this._updateZeebePropertyProperties(element, oldTemplate, newTemplate);
this._updateMessage(element, oldTemplate, newTemplate);
this._updateCalledElement(element, oldTemplate, newTemplate);
this._updateLinkedResources(element, oldTemplate, newTemplate);
this._updateZeebeUserTask(element, newTemplate);
}
}
_getOrCreateExtensionElements(element, businessObject = ModelUtil.getBusinessObject(element)) {
const bpmnFactory = this._bpmnFactory,
modeling = this._modeling;
let extensionElements = businessObject.get('extensionElements');
if (!extensionElements) {
extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
values: []
});
extensionElements.$parent = businessObject;
modeling.updateModdleProperties(element, businessObject, {
extensionElements: extensionElements
});
}
return extensionElements;
}
_updateZeebeModelerTemplate(element, newTemplate) {
const modeling = this._modeling;
const newId = newTemplate && newTemplate.id;
const newVersion = newTemplate && newTemplate.version;
if (getTemplateId$1(element) !== newId || getTemplateVersion$1(element) !== newVersion) {
modeling.updateProperties(element, {
'zeebe:modelerTemplate': newId,
'zeebe:modelerTemplateVersion': newVersion
});
}
}
_updateZeebeModelerTemplateIcon(element, newTemplate) {
const modeling = this._modeling;
const newIcon = newTemplate && newTemplate.icon;
const newIconContents = newIcon && newIcon.contents;
if (getTemplateIcon(element) !== newIconContents) {
modeling.updateProperties(element, {
'zeebe:modelerTemplateIcon': newIconContents
});
}
}
_updateProperties(element, oldTemplate, newTemplate) {
const commandStack = this._commandStack;
const businessObject = ModelUtil.getBusinessObject(element);
const newProperties = newTemplate.properties.filter(newProperty => {
const newBinding = newProperty.binding,
newBindingType = newBinding.type;
return newBindingType === 'property';
});
// Remove old Properties if no new Properties specified
const propertiesToRemove = oldTemplate && oldTemplate.properties.filter(oldProperty => {
const oldBinding = oldProperty.binding,
oldBindingType = oldBinding.type;
return oldBindingType === 'property' && !newProperties.find(newProperty => newProperty.binding.name === oldProperty.binding.name);
}) || [];
if (propertiesToRemove.length) {
const payload = propertiesToRemove.reduce((properties, property) => {
properties[property.binding.name] = undefined;
return properties;
}, {});
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties: payload
});
}
if (!newProperties.length) {
return;
}
newProperties.forEach(newProperty => {
const oldProperty = findOldProperty$1(oldTemplate, newProperty),
newBinding = newProperty.binding,
newBindingName = newBinding.name,
newPropertyValue = getDefaultValue(newProperty),
changedElement = businessObject;
let properties = {};
if (shouldKeepValue(changedElement, oldProperty, newProperty)) {
return;
}
properties[newBindingName] = newPropertyValue;
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties
});
});
}
/**
* Update `zeebe:TaskDefinition` properties of specified business object. This
* can only exist in `bpmn:ExtensionElements`.
*
* @param {djs.model.Base} element
* @param {Object} oldTemplate
* @param {Object} newTemplate
*/
_updateZeebeTaskDefinition(element, oldTemplate, newTemplate) {
const bpmnFactory = this._bpmnFactory,
commandStack = this._commandStack;
const newProperties = newTemplate.properties.filter(newProperty => {
const newBinding = newProperty.binding,
newBindingType = newBinding.type;
return TASK_DEFINITION_TYPES.includes(newBindingType);
});
const businessObject = this._getOrCreateExtensionElements(element);
let taskDefinition = findExtension$1(businessObject, 'zeebe:TaskDefinition');
// (1) remove old task definition if no new properties specified
if (!newProperties.length) {
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties: {
values: minDash.without(businessObject.get('values'), taskDefinition)
}
});
return;
}
newProperties.forEach(newProperty => {
const oldProperty = findOldProperty$1(oldTemplate, newProperty),
newPropertyValue = getDefaultValue(newProperty),
newBinding = newProperty.binding,
propertyName = getTaskDefinitionPropertyName(newBinding);
// (2) update old task definition
if (taskDefinition) {
if (!shouldKeepValue(taskDefinition, oldProperty, newProperty)) {
const properties = {
[propertyName]: newPropertyValue
};
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: taskDefinition,
properties
});
}
}
// (3) add new task definition
else {
const properties = {
[propertyName]: newPropertyValue
};
taskDefinition = createTaskDefinition(properties, bpmnFactory);
taskDefinition.$parent = businessObject;
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties: {
values: [...businessObject.get('values'), taskDefinition]
}
});
}
});
// (4) remove properties no longer templated
const oldProperties = oldTemplate && oldTemplate.properties.filter(oldProperty => {
const oldBinding = oldProperty.binding,
oldBindingType = oldBinding.type;
return TASK_DEFINITION_TYPES.includes(oldBindingType) && !newProperties.find(newProperty => newProperty.binding.property === oldProperty.binding.property);
}) || [];
oldProperties.forEach(oldProperty => {
const properties = {
[oldProperty.binding.property]: undefined
};
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: taskDefinition,
properties
});
});
}
/**
* Update `zeebe:Input` and `zeebe:Output` properties of specified business
* object. Both can only exist in `zeebe:ioMapping` which can exist in `bpmn:ExtensionElements`.
*
* @param {djs.model.Base} element
* @param {Object} oldTemplate
* @param {Object} newTemplate
*/
_updateZeebeInputOutputParameterProperties(element, oldTemplate, newTemplate) {
const bpmnFactory = this._bpmnFactory,
commandStack = this._commandStack;
const newProperties = newTemplate.properties.filter(newProperty => {
const newBinding = newProperty.binding,
newBindingType = newBinding.type;
return newBindingType === 'zeebe:input' || newBindingType === 'zeebe:output';
});
const businessObj