bpmn-js-element-templates
Version:
Element templates for bpmn-js
1,991 lines (1,598 loc) • 201 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');
/**
* 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(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;
});
}
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 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');
}
}
// 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;
}
/**
* 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 = '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 = [
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 = [
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
];
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 businessObject = this._getOrCreateExtensionElements(element);
let ioMapping = findExtension$1(businessObject, 'zeebe:IoMapping');
// (1) remove old mappings if no new specified
if (!newProperties.length) {
if (!ioMapping) {
return;
}
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties: {
values: minDash.without(businessObject.get('values'), ioMapping)
}
});
}
if (!ioMapping) {
ioMapping = bpmnFactory.create('zeebe:IoMapping');
ioMapping.$parent = businessObject;
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: businessObject,
properties: {
values: [ ...businessObject.get('values'), ioMapping ]
}
});
}
const oldInputs = ioMapping.get('zeebe:inputParameters')
? ioMapping.get('zeebe:inputParameters').slice()
: [];
const oldOutputs = ioMapping.get('zeebe:outputParameters')
? ioMapping.get('zeebe:outputParameters').slice()
: [];
let propertyName;
newProperties.forEach((newProperty) => {
const oldProperty = findOldProperty$1(oldTemplate, newProperty),
inputOrOutput = findBusinessObject(businessObject, newProperty),
newPropertyValue = getDefaultValue(newProperty),
newBinding = newProperty.binding,
newBindingType = newBinding.type;
let newInputOrOutput,
properties;
// (2) update old inputs and outputs
if (inputOrOutput) {
// (2a) exclude old inputs and outputs from cleanup, unless
// a) optional and has empty value, and
// b) not changed
if (
shouldUpdate(newPropertyValue, newProperty) ||
shouldKeepValue(inputOrOutput, oldProperty, newProperty)
) {
if (ModelUtil.is(inputOrOutput, 'zeebe:Input')) {
remove$1(oldInputs, inputOrOutput);
} else {
remove$1(oldOutputs, inputOrOutput);
}
}
// (2a) do updates (unless changed)
if (!shouldKeepValue(inputOrOutput, oldProperty, newProperty)) {
if (ModelUtil.is(inputOrOutput, 'zeebe:Input')) {
properties = {
source: newPropertyValue
};
} else {
properties = {
target: newPropertyValue
};
}
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: inputOrOutput,
properties
});
}
}
// (3) add new inputs and outputs (unless optional)