UNPKG

bpmn-js-element-templates

Version:
1,991 lines (1,598 loc) 201 kB
'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)