UNPKG

bpmn-js-element-templates

Version:
2,064 lines (1,680 loc) 248 kB
'use strict'; var minDash = require('min-dash'); var ModelUtil = require('bpmn-js/lib/util/ModelUtil'); var uuid = require('uuid'); var semver = require('semver'); var semverCompare = require('semver-compare'); var elementTemplatesValidator = require('@bpmn-io/element-templates-validator'); var Ids = require('ids'); var DiUtil = require('bpmn-js/lib/util/DiUtil'); var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor'); var LabelUtil = require('bpmn-js/lib/features/label-editing/LabelUtil'); var DrilldownUtil = require('bpmn-js/lib/util/DrilldownUtil'); /** * Check if the property is cast to FEEL expression: * - Boolean and Number properties with feel set to 'optional' or 'static' * - Boolean and Number input/output parameters have default feel=static * * @returns {boolean} */ const shouldCastToFeel = (property) => { const feel = getFeelValue(property); return [ 'optional', 'static' ].includes(feel) && [ 'Boolean', 'Number' ].includes(property.type); }; const ALWAYS_CAST_TO_FEEL = [ 'zeebe:input', 'zeebe:output' ]; function getFeelValue(property) { if (ALWAYS_CAST_TO_FEEL.includes(property.binding.type)) { return property.feel || 'static'; } return property.feel; } const toFeelExpression = (value, type) => { if (typeof value === 'string' && value.startsWith('=')) { return value; } if (type === 'Boolean') { value = value === 'false' ? false : value; return '=' + !!value; } if (typeof value === 'undefined') { return value; } return '=' + value.toString(); }; /** * The BPMN 2.0 extension attribute name under * which the element template ID is stored. * * @type {String} */ const TEMPLATE_ID_ATTR$1 = 'zeebe:modelerTemplate'; /** * The BPMN 2.0 extension attribute name under * which the element template version is stored. * * @type {String} */ const TEMPLATE_VERSION_ATTR$1 = 'zeebe:modelerTemplateVersion'; /** * The BPMN 2.0 extension attribute name under * which the element template icon is stored. * * @type {String} */ const TEMPLATE_ICON_ATTR = 'zeebe:modelerTemplateIcon'; /** * Get template id for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateId$1(element) { const businessObject = 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; }); } /** * Find message referred to in an event, an event definition, or a task. * @param {ModdleElement} businessObject */ 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'); } /** * Find signal referred to in an event or an event definition. * @param {ModdleElement} businessObject */ function findSignal(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('signalRef'); } /** * Find timer event definition in an event. * @param {ModdleElement|element} element */ function findTimerEventDefinition(element) { const businessObject = ModelUtil.getBusinessObject(element); if (ModelUtil.is(businessObject, 'bpmn:Event')) { const eventDefinitions = businessObject.get('eventDefinitions'); if (!eventDefinitions.length) { return; } return eventDefinitions.find(def => ModelUtil.is(def, 'bpmn:TimerEventDefinition')); } } /** * Get the default value disregarding generated values. */ function getDefaultFixedValue(property) { if ( shouldCastToFeel(property) || property.feel === 'required' ) { return toFeelExpression(property.value, property.type); } return property.value; } function getDefaultValue(property) { const value = getDefaultFixedValue(property); if (value !== undefined) { return value; } if (property.generatedValue) { const { type } = property.generatedValue; if (type === 'uuid') { return 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'; /** * Returns incompatible engines for a given template. * * @param {Object} template * @param {Object} checkEngines * * @return {Object} */ function getIncompatibleEngines(template, checkEngines) { const templateEngines = template.engines; return minDash.reduce(templateEngines, (result, _, engine) => { if (!minDash.has(checkEngines, engine)) { return result; } if (!semver.satisfies(checkEngines[engine], templateEngines[engine])) { result[engine] = { actual: checkEngines[engine], required: templateEngines[engine] }; } return result; }, {}); } /** * Returns whether a template is compatible with the given engines. * * @param {Object} template * @param {Object} checkEngines * * @return {boolean} */ function isCompatible(template, checkEngines) { return !Object.keys(getIncompatibleEngines(template, checkEngines)).length; } /** * Build a map of templates grouped by id. * * @param {Array<Object>} templates * @param {Object} engines * * @return {Object} */ function buildTemplatesById(templates, engines) { const templatesById = {}; templates.forEach((template) => { const id = template.id; const version = minDash.isUndefined(template.version) ? '_' : template.version; if (!templatesById[id]) { templatesById[id] = {}; } templatesById[id][version] = template; const latest = templatesById[id].latest; if (isCompatible(template, engines)) { if (!latest || minDash.isUndefined(latest.version) || latest.version < version) { templatesById[id].latest = template; } } }); return templatesById; } /** * Finds the list of templates that match the given criteria within a template index. * * @param {string|djs.model.Base} [elementOrTemplateId] * @param {Object} templatesIndex * @param {Object} [options] * @param {boolean} [options.latest] * @param {boolean} [options.deprecated] * * @return {Array<Object>} */ function findTemplates(elementOrTemplateId, templatesIndex, options = {}) { const { latest: includeLatestOnly, deprecated: includeDeprecated } = options; const getVersions = (template) => { const { latest, ...versions } = template; return includeLatestOnly ? ( !includeDeprecated && (latest && latest.deprecated) ? [] : (latest ? [ latest ] : []) ) : minDash.values(versions) ; }; if (minDash.isNil(elementOrTemplateId)) { return minDash.flatten(minDash.values(templatesIndex).map(getVersions)); } if (minDash.isObject(elementOrTemplateId)) { const element = elementOrTemplateId; return minDash.filter(findTemplates(null, templatesIndex, options), function(template) { return ModelUtil.isAny(element, template.appliesTo); }) || []; } if (minDash.isString(elementOrTemplateId)) { return templatesIndex[ elementOrTemplateId ] && getVersions(templatesIndex[ elementOrTemplateId ]); } throw new Error('argument must be of type {string|djs.model.Base|undefined}'); } /** * Get template id for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateId(element) { const businessObject = 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.18.0"; /** * @typedef {import('bpmn-js/lib/model/Types').Element} Element */ /** * Registry for element templates. */ let ElementTemplates$1 = class ElementTemplates { constructor(commandStack, eventBus, modeling, injector, config) { this._commandStack = commandStack; this._eventBus = eventBus; this._injector = injector; this._modeling = modeling; this._templatesById = {}; this._templates = []; config = config || {}; this._engines = this._coerceEngines(config.engines || {}); eventBus.on('elementTemplates.engines.changed', event => { this.set(this._templates); }); } /** * Get template with given ID and optional version or for element. * * @param {String|Element} elementOrTemplateId * @param {number} [version] * * @return {ElementTemplate} */ get(elementOrTemplateId, version) { const templates = this._templatesById; let element; if (minDash.isUndefined(elementOrTemplateId)) { return null; } else if (minDash.isString(elementOrTemplateId)) { if (minDash.isUndefined(version)) { version = '_'; } if (templates[ elementOrTemplateId ] && templates[ elementOrTemplateId ][ version ]) { return templates[ elementOrTemplateId ][ version ]; } else { return null; } } else { element = elementOrTemplateId; return this.get(this._getTemplateId(element), this._getTemplateVersion(element)); } } /** * Get default template for given element. * * @param {Element} element * * @return {ElementTemplate} */ getDefault(element) { return minDash.find(this.getAll(element), function(template) { return template.isDefault; }) || null; } /** * Get all templates (with given ID or applicable to element). * * @param {string|Element} [elementOrTemplateId] * @return {Array<ElementTemplate>} */ getAll(elementOrTemplateId) { return findTemplates(elementOrTemplateId, this._templatesById, { deprecated: true }); } /** * Get all templates (with given ID or applicable to element) with the latest * version. * * @param {String|Element} [elementOrTemplateId] * @param {{ deprecated?: boolean }} [options] * * @return {Array<ElementTemplate>} */ getLatest(elementOrTemplateId, options = {}) { return findTemplates(elementOrTemplateId, this._templatesById, { ...options, latest: true }); } /** * Get templates compatible with a given engine configuration override. * * @param {string|Element} [elementOrTemplateId] * @param {Object} enginesOverrides * @param {Object} [options] * @param {boolean} [options.deprecated=false] * @param {boolean} [options.latest=true] * * @returns {Array<ElementTemplate>} */ getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) { const overridenEngines = this._coerceEngines({ ...this._engines, ...enginesOverrides }); const templatesById = buildTemplatesById(this._templates, overridenEngines); return findTemplates(elementOrTemplateId, templatesById, { latest: true, ...options }); } /** * Set templates. * * @param {Array<ElementTemplate>} templates */ set(templates) { this._templates = templates; this._templatesById = buildTemplatesById(this._templates, this._engines); this._fire('changed'); } getEngines() { return this._engines; } setEngines(engines) { this._engines = this._coerceEngines(engines); this._fire('engines.changed'); } /** * Ensures that only valid engines are kept around * * @param { Record<string, string> } engines * * @return { Record<string, string> } filtered, valid engines */ _coerceEngines(engines) { // we provide <elementTemplates> engine with the current // package version; templates may use that engine to declare // compatibility with this library engines = { elementTemplates: packageVersion, ...engines }; return 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 isCompatible(template, this._engines); } /** * Get engines that are incompatible with the template. * * @param {any} template * * @return { Record<string, { required: string, found: string } } - incompatible engines along with their template and local versions */ getIncompatibleEngines(template) { return getIncompatibleEngines(template, this._engines); } /** * Get template versions for a given element or template ID. * * @param {object|string|null} id * @param { { latest?: boolean, deprecated?: boolean } [options] * * @return {Array<ElementTemplate>} */ _getTemplateVerions(id, options = {}) { return findTemplates(id, this._templatesById, options); } _getTemplateId(element) { return getTemplateId(element); } _getTemplateVersion(element) { return getTemplateVersion(element); } /** * Apply element template to a given element. * * @param {Element} element * @param {ElementTemplate} newTemplate * * @return {Element} the updated element */ applyTemplate(element, newTemplate) { const oldTemplate = this.get(element); const context = { element, newTemplate, oldTemplate }; const event = oldTemplate?.id === newTemplate?.id ? 'update' : 'apply'; this._commandStack.execute('propertiesPanel.camunda.changeTemplate', context); this._fire(event, { element, newTemplate, oldTemplate }); return context.element; } _fire(action, payload) { return this._eventBus.fire(`elementTemplates.${action}`, payload); } /** * Remove template from a given element. * * @param {Element} element * * @return {Element} the updated element */ removeTemplate(element) { const oldTemplate = this.get(element); const context = { element, oldTemplate }; this._commandStack.execute('propertiesPanel.removeTemplate', context); this._fire('remove', { element, oldTemplate }); return context.newElement; } /** * Unlink template from a given element. * * @param {Element} element * * @return {Element} the updated element */ unlinkTemplate(element) { const oldTemplate = this.get(element); const context = { element, oldTemplate }; this._commandStack.execute('propertiesPanel.unlinkTemplate', context); this._fire('unlink', { element, oldTemplate }); return context.element; } }; ElementTemplates$1.$inject = [ 'commandStack', 'eventBus', 'modeling', 'injector', 'config.elementTemplates' ]; const PROPERTY_TYPE = 'property'; const ZEBBE_PROPERTY_TYPE = 'zeebe:property'; const ZEBBE_INPUT_TYPE = 'zeebe:input'; const ZEEBE_OUTPUT_TYPE = 'zeebe:output'; const ZEEBE_PROPERTY_TYPE = 'zeebe:property'; const ZEEBE_TASK_DEFINITION_TYPE_TYPE = 'zeebe:taskDefinition:type'; const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition'; const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader'; const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property'; const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property'; const SIGNAL_PROPERTY_TYPE = 'bpmn:Signal#property'; const TIMER_EVENT_DEFINITION_PROPERTY_TYPE = 'bpmn:TimerEventDefinition#property'; const ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement'; const ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource'; const ZEEBE_USER_TASK = 'zeebe:userTask'; const ZEEBE_CALLED_DECISION = 'zeebe:calledDecision'; const ZEEBE_FORM_DEFINITION = 'zeebe:formDefinition'; const ZEEBE_SCRIPT_TASK = 'zeebe:script'; const ZEEBE_ASSIGNMENT_DEFINITION = 'zeebe:assignmentDefinition'; const ZEEBE_PRIORITY_DEFINITION = 'zeebe:priorityDefinition'; const ZEEBE_AD_HOC = 'zeebe:adHoc'; const ZEEBE_TASK_SCHEDULE = 'zeebe:taskSchedule'; const EXTENSION_BINDING_TYPES = [ MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE, ZEEBE_PROPERTY_TYPE, ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION, ZEEBE_TASK_HEADER_TYPE, ZEEBE_CALLED_ELEMENT, ZEEBE_LINKED_RESOURCE_PROPERTY, ZEEBE_CALLED_DECISION, ZEEBE_FORM_DEFINITION, ZEEBE_SCRIPT_TASK, ZEEBE_ASSIGNMENT_DEFINITION, ZEEBE_PRIORITY_DEFINITION, ZEEBE_AD_HOC, ZEEBE_TASK_SCHEDULE ]; const TASK_DEFINITION_TYPES = [ ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION ]; const IO_BINDING_TYPES = [ ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE ]; const MESSAGE_BINDING_TYPES = [ MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE ]; const PROPERTY_BINDING_TYPES = [ PROPERTY_TYPE, MESSAGE_PROPERTY_TYPE, SIGNAL_PROPERTY_TYPE ]; /** * Check whether a given timer expression type is supported for a given element. * * @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration' * @param {Element} element * @return {boolean} */ function isTimerExpressionTypeSupported(type, element) { const businessObject = ModelUtil.getBusinessObject(element); switch (type) { case 'timeDate': return ModelUtil.isAny(element, [ 'bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:StartEvent' ]); case 'timeCycle': if (ModelUtil.is(element, 'bpmn:StartEvent') && (!hasParentEventSubProcess(businessObject) || !isInterrupting(businessObject))) { return true; } if (ModelUtil.is(element, 'bpmn:BoundaryEvent') && !isInterrupting(businessObject)) { return true; } return false; case 'timeDuration': if (ModelUtil.isAny(element, [ 'bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent' ])) { return true; } if (ModelUtil.is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) { return true; } return false; default: return false; } } /** * Check if the template is a timer template and if so, whether it is applicable * * @param {Object} template * @param {Element} element * @return {boolean} */ function isTimerTemplateApplicable(template, element) { // Find timer binding in template const timerBinding = template.properties?.find(property => { return property.binding?.type === TIMER_EVENT_DEFINITION_PROPERTY_TYPE; }); // No timer binding - template is applicable if (!timerBinding) { return true; } const timerType = timerBinding.binding.name; // Check if the timer type can be applied (possibly with auto-conversion) return canTimerTypeBeAppliedWithConversion(timerType, element); } /** * Check if a timer type can be applied to an element, possibly with auto-conversion. * * Only blocks truly impossible combinations: * - timeDuration on process-level start events (can't be converted) * * @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration' * @param {Element} element * @return {boolean} */ function canTimerTypeBeAppliedWithConversion(type, element) { const businessObject = ModelUtil.getBusinessObject(element); switch (type) { case 'timeDate': return ModelUtil.isAny(element, [ 'bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:StartEvent' ]); case 'timeCycle': if (ModelUtil.isAny(element, [ 'bpmn:StartEvent', 'bpmn:BoundaryEvent' ])) { return true; } return false; case 'timeDuration': if (ModelUtil.isAny(element, [ 'bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent' ])) { return true; } // timeDuration can only be applied to start events in event subprocesses // Process-level start events cannot support timeDuration (no conversion possible) if (ModelUtil.is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) { return true; } return false; default: return false; } } // helpers ////////// function isInterrupting(businessObject) { if (ModelUtil.is(businessObject, 'bpmn:BoundaryEvent')) { return businessObject.get('cancelActivity') !== false; } return businessObject.get('isInterrupting') !== false; } function hasParentEventSubProcess(businessObject) { const parent = businessObject.$parent; return parent && ModelUtil.is(parent, 'bpmn:SubProcess') && parent.get('triggeredByEvent'); } /** * @typedef {import('bpmn-js/lib/model/Types').Element} Element */ /** * Filters to determine whether a template is applicable to a given element. * @type {Array<(template: ElementTemplate, element: Element) => boolean>} */ const TEMPLATE_FILTERS = [ isTimerTemplateApplicable ]; /** * Registry for element templates. */ class ElementTemplates extends ElementTemplates$1 { constructor(templateElementFactory, commandStack, eventBus, modeling, injector, config) { super(commandStack, eventBus, modeling, injector, config); this._templateElementFactory = templateElementFactory; } _getTemplateId(element) { return getTemplateId$1(element); } _getTemplateVersion(element) { return getTemplateVersion$1(element); } /** * Get all templates (with given ID or applicable to element). * * @param {string|djs.model.Base} [elementOrTemplateId] * @return {Array<ElementTemplate>} */ getAll(elementOrTemplateId) { const templates = super.getAll(elementOrTemplateId); if (minDash.isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } /** * Get all templates (with given ID or applicable to element) with the latest version. * * @param {String|djs.model.Base} [elementOrTemplateId] * @param {{ deprecated?: boolean }} [options] * * @return {Array<ElementTemplate>} */ getLatest(elementOrTemplateId, options = {}) { const templates = super.getLatest(elementOrTemplateId, options); if (minDash.isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } /** * Get compatible templates for element with optional engine overrides. * * @param {String|djs.model.Base} [elementOrTemplateId] * @param {Object} [enginesOverrides] * @param {Object} [options] * * @return {Array<ElementTemplate>} */ getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) { const templates = super.getCompatible(elementOrTemplateId, enginesOverrides, options); if (minDash.isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } _filterApplicableTemplates(templates, element) { return TEMPLATE_FILTERS.reduce( (filteredTemplates, filterFn) => filteredTemplates.filter(template => filterFn(template, element)), templates ); } /** * Create an element based on an element template. This is, for example, * called from the create-append anything menu. * * @param {ElementTemplate} template * @returns {djs.model.Base} */ createElement(template) { if (!template) { throw new Error('template is missing'); } const element = this._templateElementFactory.create(template); return element; } /** * Apply element template to a given element. * * @param {Element} element * @param {ElementTemplate} newTemplate * * @return {Element} the updated element */ applyTemplate(element, newTemplate) { const oldTemplate = this.get(element); const context = { element, newTemplate, oldTemplate }; const event = oldTemplate?.id === newTemplate?.id ? 'update' : 'apply'; this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', context); this._fire(event, { element, newTemplate, oldTemplate }); return context.element; } /** * Remove template from a given element. * * @param {Element} element * * @return {Element} the updated element */ removeTemplate(element) { const oldTemplate = this.get(element); const context = { element, oldTemplate }; this._commandStack.execute('propertiesPanel.removeTemplate', context); this._fire('remove', { element, oldTemplate }); return context.element; } } ElementTemplates.$inject = [ 'templateElementFactory', 'commandStack', 'eventBus', 'modeling', 'injector', 'config.elementTemplates', ]; const SUPPORTED_SCHEMA_VERSION$1 = 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; } /** * Check if element type is a subprocess (either regular or AdHoc). * * @param {string} elementType * @returns {boolean} */ function isSubprocess(elementType) { return elementType === 'bpmn:SubProcess' || elementType === 'bpmn:AdHocSubProcess' || elementType === 'bpmn:Transaction'; } /** * Create an input parameter representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createInputParameter$1(binding, value, bpmnFactory) { const { name } = binding; return bpmnFactory.create('zeebe:Input', { source: value, target: name }); } /** * Create an output parameter representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createOutputParameter$1(binding, value, bpmnFactory) { const { source } = binding; return bpmnFactory.create('zeebe:Output', { source, target: value }); } /** * Create a task header representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createTaskHeader(binding, value, bpmnFactory) { const { key } = binding; return bpmnFactory.create('zeebe:Header', { key, value }); } /** * Create a task definition representing the given value. * * @param {object} attrs * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createTaskDefinition(attrs = {}, bpmnFactory) { return bpmnFactory.create('zeebe:TaskDefinition', attrs); } /** * Create zeebe:Property from the given binding. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createZeebeProperty(binding, value = '', bpmnFactory) { const { name } = binding; return bpmnFactory.create('zeebe:Property', { name, value }); } /** * Retrieves whether an element should be updated for a given property. * * That matches once * a) the property value is not empty, or * b) the property is not optional * * @param {String} value * @param {Object} property * @returns {Boolean} */ function shouldUpdate(value, property) { const { optional } = property; return value || !optional; } /** * Gets or, in case not existent, creates extension element for given element. * * @param {djs.model.Base} element * @param {String} type * @param {BpmnFactory} bpmnFactory * @returns {ModdleElement} */ function ensureExtension(element, type, bpmnFactory) { const businessObject = 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; } function getTaskDefinitionPropertyName(binding) { return binding.type === ZEEBE_TASK_DEFINITION_TYPE_TYPE ? 'type' : binding.property; } function removeRootElement(rootElement, injector) { const modeling = injector.get('modeling'), canvas = injector.get('canvas'), bpmnjs = injector.get('bpmnjs'); const element = canvas.getRootElement(), definitions = bpmnjs.getDefinitions(), rootElements = definitions.get('rootElements'); const newRootElements = rootElements.filter(e => e !== rootElement); // short-circuit to prevent unnecessary updates if (newRootElements.length === rootElements.length) { return; } modeling.updateModdleProperties(element, definitions, { rootElements: newRootElements }); } /** * Remove message from element and the diagram. * * @param {import('bpmn-js/lib/model/Types').Element} element * @param {import('didi').Injector} injector */ function removeMessage(element, injector) { const modeling = injector.get('modeling'); const bo = getReferringElement(element); // Event does not have an event definition if (!bo) { return; } const message = findMessage(bo); if (!message) { return; } modeling.updateModdleProperties(element, bo, { messageRef: undefined }); removeRootElement(message, injector); } /** * Remove signal from element and the diagram. * * @param {import('bpmn-js/lib/model/Types').Element} element * @param {import('didi').Injector} injector */ function removeSignal(element, injector) { const modeling = injector.get('modeling'); const bo = getReferringElement(element); // Event does not have an event definition if (!bo) { return; } const signal = findSignal(bo); if (!signal) { return; } modeling.updateModdleProperties(element, bo, { signalRef: undefined }); removeRootElement(signal, injector); } function getReferringElement(element) { const bo = ModelUtil.getBusinessObject(element); if (ModelUtil.is(bo, 'bpmn:Event')) { return bo.get('eventDefinitions')[0]; } return bo; } const EXPRESSION_TYPES$1 = [ 'bpmn:Expression', 'bpmn:FormalExpression' ]; function isExpression(element, propertyName) { const bo = ModelUtil.getBusinessObject(element); const descriptor = bo.$descriptor.propertiesByName[propertyName]; return descriptor && EXPRESSION_TYPES$1.includes(descriptor.type); } function createExpression(value, parent, bpmnFactory) { const expression = createElement('bpmn:FormalExpression', { body: value }, parent, bpmnFactory); return expression; } function getExpressionValue(element, propertyName) { const bo = ModelUtil.getBusinessObject(element); const expression = bo.get(propertyName); return expression?.get('body') || undefined; } /** * @typedef {import('bpmn-js/lib/model/Types').Element} Element */ /** * Applies an element template to an element. Sets `zeebe:modelerTemplate` and * `zeebe:modelerTemplateVersion`. */ let ChangeElementTemplateHandler$1 = class ChangeElementTemplateHandler { constructor(bpmnFactory, bpmnReplace, commandStack, modeling, moddleCopy, injector) { this._bpmnFactory = bpmnFactory; this._bpmnReplace = bpmnReplace; this._modeling = modeling; this._moddleCopy = moddleCopy; this._commandStack = commandStack; this._injector = injector; } /** * Change element template.