UNPKG

bpmn-js-element-templates

Version:
1,694 lines (1,589 loc) 523 kB
import { isNil, flatten, values, isObject, filter, isString, isUndefined as isUndefined$1, reduce, find, isArray, forEach, isFunction, without, assign, groupBy, isNumber, sortBy, has, keys, bind, set, pick, findIndex } from 'min-dash'; import { getBusinessObject, is, isAny } from 'bpmn-js/lib/util/ModelUtil'; import { v4 } from 'uuid'; import { getCompatible, getCoerced, isSatisfied } from '@bpmn-io/semver-compat'; import semverCompare from 'semver-compare'; import { getSchemaVersion as getSchemaVersion$1, validate as validate$1, getZeebeSchemaVersion, getZeebeSchemaPackage, validateZeebe } from '@bpmn-io/element-templates-validator'; import { Ids } from 'ids'; import { isEventSubProcess } from 'bpmn-js/lib/util/DiUtil'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; import translateModule from 'diagram-js/lib/i18n/translate'; import { useLayoutState, HeaderButton, ArrowIcon, CreateIcon, DropdownButton, TextAreaEntry, TextFieldEntry, FeelEntry as FeelEntry$1, FeelTextAreaEntry as FeelTextAreaEntry$1, JsonEditorEntry, SelectEntry, FeelCheckboxEntry, CheckboxEntry, FeelNumberEntry, NumberFieldEntry, Group, isFeelEntryEdited, isNumberFieldEntryEdited, isCheckboxEntryEdited, isSelectEntryEdited, isTextFieldEntryEdited, isJsonEditorEntryEdited, isTextAreaEntryEdited, usePrevious, ToggleSwitchEntry, ListGroup } from '@bpmn-io/properties-panel'; import classnames from 'classnames'; import { useService, CamundaPlatformPropertiesProviderModule } from 'bpmn-js-properties-panel'; import { jsxs, jsx, Fragment } from '@bpmn-io/properties-panel/preact/jsx-runtime'; import { createElement as createElement$1, h, Component } from '@bpmn-io/properties-panel/preact'; import { useCallback, useState, useEffect, useMemo } from '@bpmn-io/properties-panel/preact/hooks'; import { getVariablesForElement } from '@bpmn-io/extract-process-variables/zeebe'; import { query } from 'min-dom'; import { getLabel, setLabel } from 'bpmn-js/lib/features/label-editing/LabelUtil'; import { isPlane, getShapeIdFromPlane } from 'bpmn-js/lib/util/DrilldownUtil'; import StaticResolver from 'bpmnlint/lib/resolver/static-resolver'; import EventBus from 'diagram-js/lib/core/EventBus'; /** * Check if the property is cast to FEEL expression: * - Any property with feel set to 'required' * - 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); if (feel === 'required') { return true; } 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(); }; const fromFeelExpression = (value, type) => { if (typeof value === 'undefined') { return value; } if (typeof value === 'string' && value.startsWith('=')) { value = value.slice(1); } if (type === 'Number') { return Number(value); } if (type === 'Boolean') { return value !== 'false'; } return value; }; function isFeel$1(value) { return typeof value === 'string' && value.trim().startsWith('='); } /** * The BPMN 2.0 extension attribute name under * which the element template ID is stored. * * @type {String} */ const TEMPLATE_ID_ATTR$1 = 'zeebe:modelerTemplate'; /** * The BPMN 2.0 extension attribute name under * which the element template version is stored. * * @type {String} */ const TEMPLATE_VERSION_ATTR$1 = 'zeebe:modelerTemplateVersion'; /** * The BPMN 2.0 extension attribute name under * which the element template icon is stored. * * @type {String} */ const TEMPLATE_ICON_ATTR = 'zeebe:modelerTemplateIcon'; /** * Get template id for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateId$1(element) { const businessObject = getBusinessObject(element); if (businessObject) { return businessObject.get(TEMPLATE_ID_ATTR$1); } } /** * Get template version for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateVersion$1(element) { const businessObject = getBusinessObject(element); if (businessObject) { return businessObject.get(TEMPLATE_VERSION_ATTR$1); } } /** * Get template icon for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateIcon(element) { const businessObject = getBusinessObject(element); if (businessObject) { return businessObject.get(TEMPLATE_ICON_ATTR); } } /** * Find extension with given type in * BPMN element, diagram element or ExtensionElement. * * @param {ModdleElement|djs.model.Base} element * @param {String} type * * @return {ModdleElement} the extension */ function findExtension$1(element, type) { const businessObject = getBusinessObject(element); let extensionElements; if (is(businessObject, 'bpmn:ExtensionElements')) { extensionElements = businessObject; } else { extensionElements = businessObject.get('extensionElements'); } if (!extensionElements) { return; } return extensionElements.get('values').find(value => { return is(value, type); }); } function findZeebeProperty(zeebeProperties, binding) { return zeebeProperties.get('properties').find(value => { return value.name === binding.name; }); } function findInputParameter$1(ioMapping, binding) { const parameters = ioMapping.get('inputParameters'); return parameters.find(parameter => { return parameter.target === binding.name; }); } function findOutputParameter$1(ioMapping, binding) { const parameters = ioMapping.get('outputParameters'); return parameters.find(parameter => { return parameter.source === binding.source; }); } function findTaskHeader(taskHeaders, binding) { const headers = taskHeaders.get('values'); return headers.find(header => { return header.key === binding.key; }); } /** * Find message referred to in an event, an event definition, or a task. * @param {ModdleElement} businessObject */ function findMessage(businessObject) { if (is(businessObject, 'bpmn:Event')) { const eventDefinitions = businessObject.get('eventDefinitions'); if (!eventDefinitions || !eventDefinitions.length) { return; } businessObject = eventDefinitions[0]; } if (!businessObject) { return; } return businessObject.get('messageRef'); } /** * Find signal referred to in an event or an event definition. * @param {ModdleElement} businessObject */ function findSignal(businessObject) { if (is(businessObject, 'bpmn:Event')) { const eventDefinitions = businessObject.get('eventDefinitions'); if (!eventDefinitions || !eventDefinitions.length) { return; } businessObject = eventDefinitions[0]; } if (!businessObject) { return; } return businessObject.get('signalRef'); } /** * Find timer event definition in an event. * @param {ModdleElement|element} element */ function findTimerEventDefinition(element) { const businessObject = getBusinessObject(element); if (is(businessObject, 'bpmn:Event')) { const eventDefinitions = businessObject.get('eventDefinitions'); if (!eventDefinitions.length) { return; } return eventDefinitions.find(def => is(def, 'bpmn:TimerEventDefinition')); } } /** * Find conditional event definition in an event. * @param {ModdleElement|element} element */ function findConditionalEventDefinition(element) { const businessObject = getBusinessObject(element); if (is(businessObject, 'bpmn:Event')) { const eventDefinitions = businessObject.get('eventDefinitions'); if (!eventDefinitions || !eventDefinitions.length) { return; } return eventDefinitions.find(def => is(def, 'bpmn:ConditionalEventDefinition')); } } /** * Get the default value disregarding generated values. */ function getDefaultFixedValue(property) { if (shouldCastToFeel(property) || property.feel === 'required') { return toFeelExpression(property.value, property.type); } return property.value; } function getDefaultValue(property) { const value = getDefaultFixedValue(property); if (value !== undefined) { return value; } if (property.generatedValue) { const { type } = property.generatedValue; if (type === 'uuid') { return v4(); } } } /** * The BPMN 2.0 extension attribute name under * which the element template ID is stored. * * @type {String} */ const TEMPLATE_ID_ATTR = 'camunda:modelerTemplate'; /** * The BPMN 2.0 extension attribute name under * which the element template version is stored. * * @type {String} */ const TEMPLATE_VERSION_ATTR = 'camunda:modelerTemplateVersion'; /** * Returns incompatible engines for a given template. * * @param {Object} template * @param {Object} checkEngines * * @return { Record<string, { required: string, actual: string }> } - incompatible engines along with their template and local versions */ function getIncompatibleEngines(template, checkEngines) { const templateEngines = template.engines || {}; const compatible = getCompatible(templateEngines, checkEngines); return reduce(templateEngines, (result, required, engine) => { if (engine in checkEngines && !(engine in compatible)) { result[engine] = { actual: checkEngines[engine], required }; } return result; }, {}); } /** * Returns whether a template is compatible with the given engines. * * @param {Object} template * @param {Object} checkEngines * * @return {boolean} */ function isCompatible(template, checkEngines) { return !Object.keys(getIncompatibleEngines(template, checkEngines)).length; } /** * Build a map of templates grouped by id. * * @param {Array<Object>} templates * @param {Object} engines * * @return {Object} */ function buildTemplatesById(templates, engines) { const templatesById = {}; templates.forEach(template => { const id = template.id; const version = isUndefined$1(template.version) ? '_' : template.version; if (!templatesById[id]) { templatesById[id] = {}; } templatesById[id][version] = template; const latest = templatesById[id].latest; if (isCompatible(template, engines)) { if (!latest || isUndefined$1(latest.version) || latest.version < version) { templatesById[id].latest = template; } } }); return templatesById; } /** * Finds the list of templates that match the given criteria within a template index. * * @param {string|djs.model.Base} [elementOrTemplateId] * @param {Object} templatesIndex * @param {Object} [options] * @param {boolean} [options.latest] * @param {boolean} [options.deprecated] * * @return {Array<Object>} */ function findTemplates(elementOrTemplateId, templatesIndex, options = {}) { const { latest: includeLatestOnly, deprecated: includeDeprecated } = options; const getVersions = template => { const { latest, ...versions } = template; return includeLatestOnly ? !includeDeprecated && latest && latest.deprecated ? [] : latest ? [latest] : [] : values(versions); }; if (isNil(elementOrTemplateId)) { return flatten(values(templatesIndex).map(getVersions)); } if (isObject(elementOrTemplateId)) { const element = elementOrTemplateId; return filter(findTemplates(null, templatesIndex, options), function (template) { return isAny(element, template.appliesTo); }) || []; } if (isString(elementOrTemplateId)) { return templatesIndex[elementOrTemplateId] && getVersions(templatesIndex[elementOrTemplateId]); } throw new Error('argument must be of type {string|djs.model.Base|undefined}'); } /** * Get template id for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateId(element) { const businessObject = getBusinessObject(element); if (businessObject) { return businessObject.get(TEMPLATE_ID_ATTR); } } /** * Get template version for a given diagram element. * * @param {djs.model.Base} element * * @return {String} */ function getTemplateVersion(element) { const businessObject = getBusinessObject(element); if (businessObject) { return businessObject.get(TEMPLATE_VERSION_ATTR); } } /** * Find extension with given type in * BPMN element, diagram element or ExtensionElement. * * @param {ModdleElement|djs.model.Base} element * @param {String} type * * @return {ModdleElement} the extension */ function findExtension(element, type) { const businessObject = getBusinessObject(element); let extensionElements; if (is(businessObject, 'bpmn:ExtensionElements')) { extensionElements = businessObject; } else { extensionElements = businessObject.get('extensionElements'); } if (!extensionElements) { return null; } return extensionElements.get('values').find(value => { return is(value, type); }); } function findExtensions(element, types) { const extensionElements = getExtensionElements(element); if (!extensionElements) { return []; } return extensionElements.get('values').filter(value => { return isAny(value, types); }); } function findCamundaInOut(element, binding) { const extensionElements = getExtensionElements(element); if (!extensionElements) { return; } const { type } = binding; let matcher; if (type === 'camunda:in') { matcher = element => { return is(element, 'camunda:In') && isInOut(element, binding); }; } else if (type === 'camunda:out') { matcher = element => { return is(element, 'camunda:Out') && isInOut(element, binding); }; } else if (type === 'camunda:in:businessKey') { matcher = element => { return is(element, 'camunda:In') && 'businessKey' in element; }; } return extensionElements.get('values').find(matcher); } function findCamundaProperty(camundaProperties, binding) { return camundaProperties.get('values').find(value => { return value.name === binding.name; }); } function findInputParameter(inputOutput, binding) { const parameters = inputOutput.get('inputParameters'); return parameters.find(parameter => { return parameter.name === binding.name; }); } function findOutputParameter(inputOutput, binding) { const parameters = inputOutput.get('outputParameters'); return parameters.find(function (parameter) { const { value } = parameter; if (!binding.scriptFormat) { return value === binding.source; } const definition = parameter.get('camunda:definition'); if (!definition || binding.scriptFormat !== definition.get('camunda:scriptFormat')) { return false; } return definition.get('camunda:value') === binding.source; }); } function findCamundaErrorEventDefinition(element, errorRef) { const errorEventDefinitions = findExtensions(element, ['camunda:ErrorEventDefinition']); let error; // error ID has to start with <Error_${ errorRef }_> return errorEventDefinitions.find(definition => { error = definition.get('bpmn:errorRef'); if (error) { return error.get('bpmn:id').startsWith(`Error_${errorRef}`); } }); } // helpers ////////// function getExtensionElements(element) { const businessObject = getBusinessObject(element); if (is(businessObject, 'bpmn:ExtensionElements')) { return businessObject; } else { return businessObject.get('extensionElements'); } } function isInOut(element, binding) { if (binding.type === 'camunda:in') { // find based on target attribute if (binding.target) { return element.target === binding.target; } } if (binding.type === 'camunda:out') { // find based on source / sourceExpression if (binding.source) { return element.source === binding.source; } if (binding.sourceExpression) { return element.sourceExpression === binding.sourceExpression; } } // find based variables / local combination if (binding.variables) { return element.variables === 'all' && (binding.variables !== 'local' || element.local); } } // eslint-disable-next-line no-undef const packageVersion = "2.25.0"; /** * @typedef {import('bpmn-js/lib/model/Types').Element} Element */ /** * Registry for element templates. */ let ElementTemplates$1 = class ElementTemplates { constructor(commandStack, eventBus, modeling, injector, config) { this._commandStack = commandStack; this._eventBus = eventBus; this._injector = injector; this._modeling = modeling; this._templatesById = {}; this._templates = []; config = config || {}; this._engines = this._coerceEngines(config.engines || {}); eventBus.on('elementTemplates.engines.changed', event => { this.set(this._templates); }); } /** * Get template with given ID and optional version or for element. * * @param {String|Element} elementOrTemplateId * @param {number} [version] * * @return {ElementTemplate} */ get(elementOrTemplateId, version) { const templates = this._templatesById; let element; if (isUndefined$1(elementOrTemplateId)) { return null; } else if (isString(elementOrTemplateId)) { if (isUndefined$1(version)) { version = '_'; } if (templates[elementOrTemplateId] && templates[elementOrTemplateId][version]) { return templates[elementOrTemplateId][version]; } else { return null; } } else { element = elementOrTemplateId; return this.get(this._getTemplateId(element), this._getTemplateVersion(element)); } } /** * Get default template for given element. * * @param {Element} element * * @return {ElementTemplate} */ getDefault(element) { return find(this.getAll(element), function (template) { return template.isDefault; }) || null; } /** * Get all templates (with given ID or applicable to element). * * @param {string|Element} [elementOrTemplateId] * @return {Array<ElementTemplate>} */ getAll(elementOrTemplateId) { return findTemplates(elementOrTemplateId, this._templatesById, { deprecated: true }); } /** * Get all templates (with given ID or applicable to element) with the latest * version. * * @param {String|Element} [elementOrTemplateId] * @param {{ deprecated?: boolean }} [options] * * @return {Array<ElementTemplate>} */ getLatest(elementOrTemplateId, options = {}) { return findTemplates(elementOrTemplateId, this._templatesById, { ...options, latest: true }); } /** * Get templates compatible with a given engine configuration override. * * @param {string|Element} [elementOrTemplateId] * @param {Object} enginesOverrides * @param {Object} [options] * @param {boolean} [options.deprecated=false] * @param {boolean} [options.latest=true] * * @returns {Array<ElementTemplate>} */ getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) { const overridenEngines = { ...this._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 return getCoerced({ elementTemplates: packageVersion, ...engines }); } /** * 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, actual: 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$1 = 'property'; const ZEBBE_PROPERTY_TYPE = 'zeebe:property'; const ZEBBE_INPUT_TYPE = 'zeebe:input'; const ZEEBE_OUTPUT_TYPE = 'zeebe:output'; const ZEEBE_PROPERTY_TYPE = 'zeebe:property'; const ZEEBE_TASK_DEFINITION_TYPE_TYPE = 'zeebe:taskDefinition:type'; const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition'; const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader'; const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property'; const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property'; const SIGNAL_PROPERTY_TYPE = 'bpmn:Signal#property'; const TIMER_EVENT_DEFINITION_PROPERTY_TYPE = 'bpmn:TimerEventDefinition#property'; const CONDITIONAL_EVENT_DEFINITION_PROPERTY = 'bpmn:ConditionalEventDefinition#property'; const CONDITIONAL_EVENT_DEFINITION_ZEEBE_CONDITIONAL_FILTER_PROPERTY = 'bpmn:ConditionalEventDefinition#zeebe:conditionalFilter#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 ZEEBE_EXECUTION_LISTENER = 'zeebe:executionListener'; const ZEEBE_TASK_LISTENER = 'zeebe:taskListener'; const EXTENSION_BINDING_TYPES$1 = [MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE, ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE, ZEEBE_PROPERTY_TYPE, ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION, ZEEBE_TASK_HEADER_TYPE, ZEEBE_CALLED_ELEMENT, ZEEBE_LINKED_RESOURCE_PROPERTY, ZEEBE_CALLED_DECISION, ZEEBE_FORM_DEFINITION, ZEEBE_SCRIPT_TASK, ZEEBE_ASSIGNMENT_DEFINITION, ZEEBE_PRIORITY_DEFINITION, ZEEBE_AD_HOC, ZEEBE_TASK_SCHEDULE, ZEEBE_EXECUTION_LISTENER, ZEEBE_TASK_LISTENER]; const TASK_DEFINITION_TYPES = [ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION]; const IO_BINDING_TYPES$1 = [ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE]; const MESSAGE_BINDING_TYPES = [MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]; const PROPERTY_BINDING_TYPES = [PROPERTY_TYPE$1, MESSAGE_PROPERTY_TYPE, SIGNAL_PROPERTY_TYPE]; /** * Check whether a given timer expression type is supported for a given element. * * @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration' * @param {Element} element * @return {boolean} */ function isTimerExpressionTypeSupported(type, element) { const businessObject = getBusinessObject(element); switch (type) { case 'timeDate': return isAny(element, ['bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:StartEvent']); case 'timeCycle': if (is(element, 'bpmn:StartEvent') && (!hasParentEventSubProcess(businessObject) || !isInterrupting(businessObject))) { return true; } if (is(element, 'bpmn:BoundaryEvent') && !isInterrupting(businessObject)) { return true; } return false; case 'timeDuration': if (isAny(element, ['bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent'])) { return true; } if (is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) { return true; } return false; default: return false; } } /** * Check if the template is a timer template and if so, whether it is applicable * * @param {Object} template * @param {Element} element * @return {boolean} */ function isTimerTemplateApplicable(template, element) { // Find timer binding in template const timerBinding = template.properties?.find(property => { return property.binding?.type === TIMER_EVENT_DEFINITION_PROPERTY_TYPE; }); // No timer binding - template is applicable if (!timerBinding) { return true; } const timerType = timerBinding.binding.name; // Check if the timer type can be applied (possibly with auto-conversion) return canTimerTypeBeAppliedWithConversion(timerType, element); } /** * Check if a timer type can be applied to an element, possibly with auto-conversion. * * Only blocks truly impossible combinations: * - timeDuration on process-level start events (can't be converted) * * @param {string} type - 'timeDate', 'timeCycle', or 'timeDuration' * @param {Element} element * @return {boolean} */ function canTimerTypeBeAppliedWithConversion(type, element) { const businessObject = getBusinessObject(element); switch (type) { case 'timeDate': return isAny(element, ['bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:StartEvent']); case 'timeCycle': if (isAny(element, ['bpmn:StartEvent', 'bpmn:BoundaryEvent'])) { return true; } return false; case 'timeDuration': if (isAny(element, ['bpmn:BoundaryEvent', 'bpmn:IntermediateCatchEvent'])) { return true; } // timeDuration can only be applied to start events in event subprocesses // Process-level start events cannot support timeDuration (no conversion possible) if (is(element, 'bpmn:StartEvent') && hasParentEventSubProcess(businessObject)) { return true; } return false; default: return false; } } // helpers ////////// function isInterrupting(businessObject) { if (is(businessObject, 'bpmn:BoundaryEvent')) { return businessObject.get('cancelActivity') !== false; } return businessObject.get('isInterrupting') !== false; } function hasParentEventSubProcess(businessObject) { const parent = businessObject.$parent; return parent && is(parent, 'bpmn:SubProcess') && parent.get('triggeredByEvent'); } /** * @typedef {import('bpmn-js/lib/model/Types').Element} Element */ /** * Filters to determine whether a template is applicable to a given element. * @type {Array<(template: ElementTemplate, element: Element) => boolean>} */ const TEMPLATE_FILTERS = [isTimerTemplateApplicable]; /** * Registry for element templates. */ class ElementTemplates extends ElementTemplates$1 { constructor(templateElementFactory, commandStack, eventBus, modeling, injector, config) { super(commandStack, eventBus, modeling, injector, config); this._templateElementFactory = templateElementFactory; } _getTemplateId(element) { return getTemplateId$1(element); } _getTemplateVersion(element) { return getTemplateVersion$1(element); } /** * Get all templates (with given ID or applicable to element). * * @param {string|djs.model.Base} [elementOrTemplateId] * @return {Array<ElementTemplate>} */ getAll(elementOrTemplateId) { const templates = super.getAll(elementOrTemplateId); if (isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } /** * Get all templates (with given ID or applicable to element) with the latest version. * * @param {String|djs.model.Base} [elementOrTemplateId] * @param {{ deprecated?: boolean }} [options] * * @return {Array<ElementTemplate>} */ getLatest(elementOrTemplateId, options = {}) { const templates = super.getLatest(elementOrTemplateId, options); if (isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } /** * Get compatible templates for element with optional engine overrides. * * @param {String|djs.model.Base} [elementOrTemplateId] * @param {Object} [enginesOverrides] * @param {Object} [options] * * @return {Array<ElementTemplate>} */ getCompatible(elementOrTemplateId, enginesOverrides = {}, options = {}) { const templates = super.getCompatible(elementOrTemplateId, enginesOverrides, options); if (isObject(elementOrTemplateId)) { return this._filterApplicableTemplates(templates, elementOrTemplateId); } return templates; } _filterApplicableTemplates(templates, element) { return TEMPLATE_FILTERS.reduce((filteredTemplates, filterFn) => filteredTemplates.filter(template => filterFn(template, element)), templates); } /** * Create an element based on an element template. This is, for example, * called from the create-append anything menu. * * @param {ElementTemplate} template * @returns {djs.model.Base} */ createElement(template) { if (!template) { throw new Error('template is missing'); } const element = this._templateElementFactory.create(template); return element; } /** * Apply element template to a given element. * * @param {Element} element * @param {ElementTemplate} newTemplate * * @return {Element} the updated element */ applyTemplate(element, newTemplate) { const oldTemplate = this.get(element); const context = { element, newTemplate, oldTemplate }; const event = oldTemplate?.id === newTemplate?.id ? 'update' : 'apply'; this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', context); this._fire(event, { element, newTemplate, oldTemplate }); return context.element; } /** * Remove template from a given element. * * @param {Element} element * * @return {Element} the updated element */ removeTemplate(element) { const oldTemplate = this.get(element); const context = { element, oldTemplate }; this._commandStack.execute('propertiesPanel.removeTemplate', context); this._fire('remove', { element, oldTemplate }); return context.element; } } ElementTemplates.$inject = ['templateElementFactory', 'commandStack', 'eventBus', 'modeling', 'injector', 'config.elementTemplates']; const SUPPORTED_SCHEMA_VERSION$1 = getSchemaVersion$1(); const MORPHABLE_TYPES = ['bpmn:Activity', 'bpmn:Event', 'bpmn:Gateway']; /** * A element template validator. */ let Validator$1 = class Validator { constructor(moddle) { this._templatesById = {}; this._validTemplates = []; this._errors = []; this._moddle = moddle; } /** * Adds the templates. * * @param {Array<TemplateDescriptor>} templates * * @return {Validator} */ addAll(templates) { if (!isArray(templates)) { this._logError('templates must be []'); } else { templates.forEach(this.add, this); } return this; } /** * Add the given element template, if it is valid. * * @param {TemplateDescriptor} template * * @return {Validator} */ add(template) { const err = this._validateTemplate(template); let id, version; if (!err) { id = template.id; version = isNil(template.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 = isNil(template.version) ? '_' : template.version, schemaVersion = template.$schema && getSchemaVersion(template.$schema); // (1) compatibility if (schemaVersion && semverCompare(SUPPORTED_SCHEMA_VERSION$1, schemaVersion) < 0) { return this._logError(`unsupported element template schema version <${schemaVersion}>. Your installation only supports up to version <${SUPPORTED_SCHEMA_VERSION$1}>. Please update your installation`, template); } // (2) versioning if (this._templatesById[id] && this._templatesById[id][version]) { if (version === '_') { return this._logError(`template id <${id}> already used`, template); } else { return this._logError(`template id <${id}> and version <${version}> already used`, template); } } // (3) elementType validation const elementTypeError = this._validateElementType(template); if (elementTypeError) { return elementTypeError; } // (4) JSON schema compliance const schemaValidationResult = validate$1(template); const { errors: schemaErrors, valid } = schemaValidationResult; if (!valid) { filteredSchemaErrors(schemaErrors).forEach(error => { this._logError(error.message, template); }); return new Error('invalid template'); } // (5) engines validation const enginesError = this._validateEngines(template); if (enginesError) { return enginesError; } return null; } _validateEngines(template) { let err; forEach(template.engines, (rangeStr, engine) => { if (isSatisfied(rangeStr, '0.0.0') === null) { err = this._logError(new Error(`Engine <${engine}> specifies invalid semver range <${rangeStr}>`), template); } }); return err; } /** * Validate elementType for given template and return error (if any). * * @param {TemplateDescriptor} template * * @return {Error} validation error, if any */ _validateElementType(template) { if (template.elementType && template.appliesTo) { const elementType = template.elementType.value, appliesTo = template.appliesTo; // (3.1) template can be applied to elementType // prevents cases where the elementType is not part of appliesTo if (!appliesTo.find(type => this._isType(elementType, type))) { return this._logError(`template does not apply to requested element type <${elementType}>`, template); } // (3.2) template only applies to same type of element // prevent elementTemplates to morph into incompatible types, e.g. Task -> SequenceFlow for (const sourceType of appliesTo) { if (!this._canMorph(sourceType, elementType)) { return this._logError(`can not morph <${sourceType}> into <${elementType}>`, template); } } } } /** * Check if given type is a subtype of given base type. * * @param {String} type * @param {String} baseType * @returns {Boolean} */ _isType(type, baseType) { const moddleType = this._moddle.getType(type); return moddleType && baseType in this._moddle.getElementDescriptor(moddleType).allTypesByName; } /** * Checks if a given type can be morphed into another type. * * @param {String} sourceType * @param {String} targetType * @returns {Boolean} */ _canMorph(sourceType, targetType) { if (sourceType === targetType) { return true; } const baseType = MORPHABLE_TYPES.find(type => this._isType(sourceType, type)); if (!baseType) { return false; } return this._isType(targetType, baseType); } /** * Log an error for the given template * * @param {(String|Error)} err * @param {TemplateDescriptor} template * * @return {Error} logged validation errors */ _logError(err, template) { if (isString(err)) { if (template) { const { id, name } = template; err = `template(id: <${id}>, name: <${name}>): ${err}`; } err = new Error(err); } this._errors.push(err); return err; } getErrors() { return this._errors; } getValidTemplates() { return this._validTemplates; } }; // helpers ////////// /** * Extract schema version from schema URI * * @param {String} schemaUri - for example https://unpkg.com/@camunda/element-templates-json-schema@99.99.99/resources/schema.json * * @return {String} for example '99.99.99' */ function getSchemaVersion(schemaUri) { const re = /\d+\.\d+\.\d+/g; const match = schemaUri.match(re); return match === null ? undefined : match[0]; } /** * Extract only relevant errors of the validation result. * * The JSON Schema we use under the hood produces more errors than we need for a * detected schema violation (for example, unmatched sub-schemas, if-then-rules, * `oneOf`-definitions ...). * * We call these errors "relevant" that have a custom error message defined by us OR * are basic data type errors. * * @param {Array} schemaErrors * * @return {Array} */ function filteredSchemaErrors(schemaErrors) { return filter(schemaErrors, err => { const { instancePath, keyword } = err; // (1) regular errors are customized from the schema if (keyword === 'errorMessage') { return true; } // (2) data type errors // ignore type errors nested in scopes if (keyword === 'type' && instancePath && !instancePath.startsWith('/scopes/')) { return true; } return false; }); } const SUPPORTED_SCHEMA_VERSION = getZeebeSchemaVersion(); const SUPPORTED_SCHEMA_PACKAGE = getZeebeSchemaPackage(); /** * A Camunda Cloud element template validator. */ class Validator extends Validator$1 { constructor(moddle) { super(moddle); } /** * Validate given template and return error (if any). * * @param {TemplateDescriptor} template * * @return {Error} validation error, if any */ _validateTemplate(template) { const id = template.id, version = isNil(template.version) ? '_' : template.version, schema = template.$schema, schemaVersion = schema && getSchemaVersion(schema); // (1) $schema attribute defined if (!schema) { return this._logError('missing $schema attribute.', template); } if (!this.isSchemaValid(schema)) { return this._logError(`unsupported $schema attribute <${schema}>.`, template); } // (2) compatibility if (schemaVersion && semverCompare(SUPPORTED_SCHEMA_VERSION, schemaVersion) < 0) { return this._logError(`unsupported element template schema version <${schemaVersion}>. Your installation only supports up to version <${SUPPORTED_SCHEMA_VERSION}>. Please update your installation`, template); } // (3) versioning if (this._templatesById[id] && this._templatesById[id][version]) { if (version === '_') { return this._logError(`template id <${id}> already used`, template); } else { return this._logError(`template id <${id}> and version <${version}> already used`, template); } } // (4) elementType validation const elementTypeError = this._validateElementType(template); if (elementTypeError) { return elementTypeError; } // (5) JSON schema compliance const schemaValidationResult = validateZeebe(template); const { errors: schemaErrors, valid } = schemaValidationResult; if (!valid) { filteredSchemaErrors(schemaErrors).forEach(error => { this._logError(error.message, template); }); return new Error('invalid template'); } // (6) engines validation const enginesError = this._validateEngines(template); if (enginesError) { return enginesError; } return null; } isSchemaValid(schema) { return schema && schema.includes(SUPPORTED_SCHEMA_PACKAGE); } } /** * The guy responsible for template loading. * * Provide the actual templates via the `config.elementTemplates`. * * That configuration can either be an array of template * descriptors or a node style callback to retrieve * the templates asynchronously. * * @param {Array<TemplateDescriptor>|Function} config * @param {EventBus} eventBus * @param {ElementTemplates} elementTemplates * @param {Moddle} moddle */ let ElementTemplatesLoader$1 = class ElementTemplatesLoader { constructor(config, eventBus, elementTemplates, moddle) { this._loadTemplates; this._eventBus = eventBus; this._elementTemplates = elementTemplates; this._moddle = moddle; if (isArray(config) || isFunction(config)) { this._loadTemplates = config; } if (config && config.loadTemplates) { this._loadTemplates = config.loadTemplates; } eventBus.on('diagram.init', () => { this.reload(); }); } reload() { const loadTemplates = this._loadTemplates; // no templates specified if (isUndefined$1(loadTemplates)) { return; } // template loader function specified if (isFunction(loadTemplates)) { return loadTemplates((err, templates) => { if (err) { return this._templateErrors([err]); } this.setTemplates(templates); }); } // templates array specified if (loadTemplates.length) { return this.setTemplates(loadTemplates); } } setTemplates(templates) { const elementTemplates = this._elementTemplates, moddle = this._moddle; const validator = new Validator$1(moddle).addAll(templates); const errors = validator.getErrors(), validTemplates = validator.getValidTemplates(); elementTemplates.set(validTemplates); if (errors.length) { this._templateErrors(errors); } } _templateErrors(errors) { this._elementTemplates._fire('errors', { errors: errors }); } }; ElementTemplatesLoader$1.$inject = ['config.elementTemplates', 'eventBus', 'elementTemplates', 'moddle']; /** * @param {Object|Array<TemplateDescriptor>|Function} config * @param {EventBus} eventBus * @param {ElementTemplates} elementTemplates * @param {Moddle} moddle */ class ElementTemplatesLoader extends ElementTemplatesLoader$1 { constructor(config, eventBus, elementTemplates, moddle) { super(config, eventBus, elementTemplates, moddle); this._elementTemplates = elementTemplates; } setTemplates(templates) { const elementTemplates = this._elementTemplates, moddle = this._moddle; const validator = new Validator(moddle).addAll(templates); const errors = validator.getErrors(), validTemplates = validator.getValidTemplates(); elementTemplates.set(validTemplates); if (errors.length) { this._templateErrors(errors); } } } ElementTemplatesLoader.$inject = ['config.elementTemplates', 'eventBus', 'elementTemplates', 'moddle']; /** * Create a new element and set its parent. * * @param {String} elementType of the new element * @param {Object} properties of the new element in key-value pairs * @param {moddle.object} parent of the new element * @param {BpmnFactory} factory which creates the new element * * @returns {djs.model.Base} element which is created */ function createElement(elementType, properties, parent, factory) { const element = factory.create(elementType, properties); if (parent) { element.$parent = parent; } return element; } /** * generate a semantic id with given prefix */ function nextId(prefix) { const ids = new Ids([32, 32, 1]); return ids.nextPrefixed(prefix); } function getRoot(businessObject) { let parent = businessObject; while (parent.$parent) { parent = parent.$parent; } return parent; } function filterElementsByType(objectList, type) { const list = objectList || []; return list.filter(element => is(element, type)); } function findRootElementsByType(businessObject, referencedType) { const root = getRoot(businessObject); return filterElementsByType(root.get('rootElements'), referencedType); } function findRootElementById(businessObject, type, id) { const elements = findRootElementsByType(businessObject, type); return elements.find(element => element.id === id); } /** * Check if element type is a subprocess (either regular or AdHoc). * * @param {string} elementType * @returns {boolean} */ function isSubprocess(elementType) { return elementType === 'bpmn:SubProcess' || elementType === 'bpmn:AdHocSubProcess' || elementType === 'bpmn:Transaction'; } /** * Create an input parameter representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createInputParameter$1(binding, value, bpmnFactory) { const { name } = binding; return bpmnFactory.create('zeebe:Input', { source: value, target: name }); } /** * Create an output parameter representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createOutputParameter$1(binding, value, bpmnFactory) { const { source } = binding; return bpmnFactory.create('zeebe:Output', { source, target: value }); } /** * Create a task header representing the given * binding and value. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createTaskHeader(binding, value, bpmnFactory) { const { key } = binding; return bpmnFactory.create('zeebe:Header', { key, value }); } /** * Create a task definition representing the given value. * * @param {object} attrs * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createTaskDefinition(attrs = {}, bpmnFactory) { return bpmnFactory.create('zeebe:TaskDefinition', attrs); } /** * Create zeebe:Property from the given binding. * * @param {PropertyBinding} binding * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} */ function createZeebeProperty(binding, value = '', bpmnFactory) { const { name } = binding; return bpmnFactory.create('zeebe:Property', { name, value }); } /** * Retrieves whether an element should be updated for a given property. * * That matches once * a) the property value is not empty, or * b) the property is not optional * * @param {String} value * @param {Object} property * @returns {Boolean} */ function shouldUpdate(value, property) { const { optional } = property; return value || !optional; } /** * Gets or, in case not existent, creates extension element for given element. * * @param {djs.model.Base} element * @param {String} type * @param {BpmnFactory} bpmnFactory * @returns {ModdleElement} */ function ensureExtension(element, type, bpmnFactory) { const businessObject = getBusinessObject(element); let extensionElements = b