UNPKG

bpmn-js-element-templates

Version:
1,642 lines (1,541 loc) 407 kB
import { getBusinessObject, is, isAny } from 'bpmn-js/lib/util/ModelUtil'; import { v4 } from 'uuid'; import { isUndefined as isUndefined$1, isString, find, isNil, flatten, values, isObject, filter, isArray, isFunction, without, forEach, groupBy, sortBy, keys, assign, bind, set, pick, findIndex, has } from 'min-dash'; import semverCompare from 'semver-compare'; import { getSchemaVersion as getSchemaVersion$1, validate, getZeebeSchemaVersion, getZeebeSchemaPackage, validateZeebe } from '@bpmn-io/element-templates-validator'; import Ids from 'ids'; import { getLabel, setLabel } from 'bpmn-js/lib/features/label-editing/LabelUtil'; import { isPlane, getShapeIdFromPlane } from 'bpmn-js/lib/util/DrilldownUtil'; import { isEventSubProcess } from 'bpmn-js/lib/util/DiUtil'; import defaultTranslate from 'diagram-js/lib/i18n/translate/translate'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; import translateModule from 'diagram-js/lib/i18n/translate'; import { useLayoutState, HeaderButton, ArrowIcon, CreateIcon, DropdownButton, FeelEntry as FeelEntry$1, FeelTextAreaEntry as FeelTextAreaEntry$1, Group, isCheckboxEntryEdited, isSelectEntryEdited, isFeelEntryEdited, isTextFieldEntryEdited, isTextAreaEntryEdited, CheckboxEntry, SelectEntry, TextFieldEntry, TextAreaEntry, 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 { getVariablesForElement } from '@bpmn-io/extract-process-variables/zeebe'; import { useState, useEffect, useMemo } from '@bpmn-io/properties-panel/preact/hooks'; import { query } from 'min-dom'; import StaticResolver from 'bpmnlint/lib/resolver/static-resolver'; /** * 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'; /** * 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); } } /** * 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; }); } 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'); } function getDefaultValue(property) { if (property.value !== undefined) { return property.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'; /** * 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); } } /** * Registry for element templates. */ let ElementTemplates$1 = class ElementTemplates { constructor(commandStack, eventBus, modeling, injector) { this._commandStack = commandStack; this._eventBus = eventBus; this._injector = injector; this._modeling = modeling; 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._templates; let element; if (isUndefined$1(id)) { return null; } else if (isString(id)) { if (isUndefined$1(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 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._templates = {}; templates.forEach(template => { const id = template.id, version = isUndefined$1(template.version) ? '_' : template.version; if (!this._templates[id]) { this._templates[id] = { latest: template }; } this._templates[id][version] = template; const latestVerions = this._templates[id].latest.version; if (isUndefined$1(latestVerions) || template.version > latestVerions) { this._templates[id].latest = template; } }); } /** * @param {object|string|null} id * @param { { latest?: boolean, deprecated?: boolean } [options] * * @return {Array<ElementTemplate>} */ _getTemplateVerions(id, options = {}) { const { latest: latestOnly, deprecated: includeDeprecated } = options; const templates = this._templates; const getVersions = template => { const { latest, ...versions } = template; return latestOnly ? !includeDeprecated && latest.deprecated ? [] : [latest] : values(versions); }; if (isNil(id)) { return flatten(values(templates).map(getVersions)); } if (isObject(id)) { const element = id; return filter(this._getTemplateVerions(null, options), function (template) { return isAny(element, template.appliesTo); }) || []; } if (isString(id)) { return templates[id] && getVersions(templates[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._eventBus.fire(`elementTemplates.${action}`, payload); return context.element; } /** * Remove template from a given element. * * @param {djs.model.Base} element * * @return {djs.model.Base} the updated element */ removeTemplate(element) { const eventBus = this._injector.get('eventBus'); eventBus.fire('elementTemplates.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']; /** * Registry for element templates. */ class ElementTemplates extends ElementTemplates$1 { constructor(templateElementFactory, commandStack, eventBus, modeling, injector) { super(commandStack, eventBus, modeling, injector); 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']; 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 = 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) { let err; 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 validationResult = validate(template); const { errors, valid } = validationResult; if (!valid) { err = new Error('invalid template'); filteredSchemaErrors(errors).forEach(error => { this._logError(error.message, 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 { dataPath, 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' && dataPath && !dataPath.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) { let err; 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 validationResult = validateZeebe(template); const { errors, valid } = validationResult; if (!valid) { err = new Error('invalid template'); filteredSchemaErrors(errors).forEach(error => { this._logError(error.message, template); }); } return err; } 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} loadTemplates * @param {EventBus} eventBus * @param {ElementTemplates} elementTemplates * @param {Moddle} moddle */ let ElementTemplatesLoader$1 = class ElementTemplatesLoader { constructor(loadTemplates, eventBus, elementTemplates, moddle) { this._loadTemplates = loadTemplates; this._eventBus = eventBus; this._elementTemplates = elementTemplates; this._moddle = moddle; 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); } this.templatesChanged(); } templatesChanged() { this._eventBus.fire('elementTemplates.changed'); } templateErrors(errors) { this._eventBus.fire('elementTemplates.errors', { errors: errors }); } }; ElementTemplatesLoader$1.$inject = ['config.elementTemplates', 'eventBus', 'elementTemplates', 'moddle']; class ElementTemplatesLoader extends ElementTemplatesLoader$1 { constructor(loadTemplates, eventBus, elementTemplates, moddle) { super(loadTemplates, 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); } this.templatesChanged(); } } 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); } /** * 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 = getBusinessObject(element); let extensionElements = businessObject.get('extensionElements'); if (!extensionElements) { extensionElements = createElement('bpmn:ExtensionElements', {}, businessObject, bpmnFactory); businessObject.set('extensionElements', extensionElements); } let extension = findExtension$1(extensionElements, type); if (!extension) { extension = bpmnFactory.create(type); extension.$parent = extensionElements; extensionElements.get('values').push(extension); } return extension; } const PROPERTY_TYPE$1 = 'property'; const ZEBBE_PROPERTY_TYPE = 'zeebe:property'; const ZEBBE_INPUT_TYPE = 'zeebe:input'; const ZEEBE_OUTPUT_TYPE = 'zeebe:output'; const ZEEBE_PROPERTY_TYPE = 'zeebe:property'; const ZEEBE_TASK_DEFINITION_TYPE_TYPE = 'zeebe:taskDefinition:type'; const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition'; const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader'; const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property'; const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property'; const ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement'; const 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]; const TASK_DEFINITION_TYPES = [ZEEBE_TASK_DEFINITION_TYPE_TYPE, ZEEBE_TASK_DEFINITION]; const IO_BINDING_TYPES$1 = [ZEBBE_INPUT_TYPE, ZEEBE_OUTPUT_TYPE]; const MESSAGE_BINDING_TYPES = [MESSAGE_PROPERTY_TYPE, MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]; const PROPERTY_BINDING_TYPES = [PROPERTY_TYPE$1, MESSAGE_PROPERTY_TYPE]; function getTaskDefinitionPropertyName(binding) { return binding.type === ZEEBE_TASK_DEFINITION_TYPE_TYPE ? 'type' : binding.property; } function removeRootElement(rootElement, injector) { const modeling = injector.get('modeling'), canvas = injector.get('canvas'), bpmnjs = injector.get('bpmnjs'); const element = canvas.getRootElement(), definitions = bpmnjs.getDefinitions(), rootElements = definitions.get('rootElements'); const newRootElements = rootElements.filter(e => e !== rootElement); // short-circuit to prevent unnecessary updates if (newRootElements.length === rootElements.length) { return; } modeling.updateModdleProperties(element, definitions, { rootElements: newRootElements }); } /** * Remove message from element and the diagram. * * @param {import('bpmn-js/lib/model/Types').Element} element * @param {import('didi').Injector} injector */ function removeMessage(element, injector) { const modeling = injector.get('modeling'); const bo = getReferringElement(element); // Event does not have an event definition if (!bo) { return; } const message = findMessage(bo); if (!message) { return; } modeling.updateModdleProperties(element, bo, { messageRef: undefined }); removeRootElement(message, injector); } function getReferringElement(element) { const bo = getBusinessObject(element); if (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, injector) { this._bpmnFactory = bpmnFactory; this._bpmnReplace = bpmnReplace; // Wrap commandStack and modeling to add hints to all commands this._commandStackWrapper = { execute: (event, context, ...rest) => { commandStack.execute(event, { hints: { skipConditionUpdate: true }, ...context }, ...rest); } }; this._modelingWrapper = { updateModdleProperties: (element, moddleElement, properties) => this._commandStackWrapper.execute('element.updateModdleProperties', { element, moddleElement, properties }), updateProperties: (element, properties) => this._commandStackWrapper.execute('element.updateProperties', { element, properties }) }; 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); } } _getOrCreateExtensionElements(element, businessObject = getBusinessObject(element)) { const bpmnFactory = this._bpmnFactory, modeling = this._modelingWrapper; 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._modelingWrapper; modeling.updateProperties(element, { 'zeebe:modelerTemplate': newTemplate && newTemplate.id, 'zeebe:modelerTemplateVersion': newTemplate && newTemplate.version }); } _updateZeebeModelerTemplateIcon(element, newTemplate) { const modeling = this._modelingWrapper; const icon = newTemplate && newTemplate.icon; modeling.updateProperties(element, { 'zeebe:modelerTemplateIcon': icon && icon.contents }); } _updateProperties(element, oldTemplate, newTemplate) { const commandStack = this._commandStackWrapper; const businessObject = 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._commandStackWrapper; 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: 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._commandStackWrapper; 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: 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 (is(inputOrOutput, 'zeebe:Input')) { remove$1(oldInputs, inputOrOutput); } else { remove$1(oldOutputs, inputOrOutput); } } // (2a) do updates (unless changed) if (!shouldKeepValue(inputOrOutput, oldProperty, newProperty)) { if (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) else if (shouldUpdate(newPropertyValue, newProperty)) { if (newBindingType === 'zeebe:input') { propertyName = 'inputParameters'; newInputOrOutput = createInputParameter$1(newBinding, newPropertyValue, bpmnFactory); } else { propertyName = 'outputParameters'; newInputOrOutput = createOutputParameter$1(newBinding, newPropertyValue, bpmnFactory); } newInputOrOutput.$parent = ioMapping; commandStack.execute('element.updateModdleProperties', { element, moddleElement: ioMapping, properties: { [propertyName]: [...ioMapping.get(propertyName), newInputOrOutput] } }); } }); // (4) remove old inputs and outputs if (oldInputs.length) { commandStack.execute('element.updateModdleProperties', { element, moddleElement: ioMapping, properties: { inputParameters: without(ioMapping.get('inputParameters'), inputParameter => oldInputs.includes(inputParameter)) } }); } if (oldOutputs.length) { commandStack.execute('element.updateModdleProperties', { element, moddleElement: ioMapping, properties: { outputParameters: without(ioMapping.get('outputParameters'), outputParameter => oldOutputs.includes(outputParameter)) } }); } } /** * Update `zeebe:Header` properties of specified business * object. Those can only exist in `zeebe:taskHeaders` which can exist in `bpmn:ExtensionElements`. * * @param {djs.model.Base} element * @param {Object} oldTemplate * @param {Object} newTemplate */ _updateZeebeTaskHeaderProperties(element, oldTemplate, newTemplate) { const bpmnFactory = this._bpmnFactory, commandStack = this._commandStackWrapper; const newProperties = newTemplate.properties.filter(newProperty => { const newBinding = newProperty.binding, newBindingType = newBinding.type; return newBindingType === 'zeebe:taskHeader'; }); const businessObject = this._getOrCreateExtensionElements(element); let taskHeaders = findExtension$1(businessObject, 'zeebe:TaskHeaders'); // (1) remove old headers if no new specified if (!newProperties.length) { if (!taskHeaders) { return; } commandStack.execute('element.updateModdleProperties', { element,