'use strict'; var assign = require('lodash/assign'); var entryFactory = require('../../../../factory/EntryFactory'), getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject, getTemplate = require('../Helper').getTemplate, cmdHelper = require('../../../../helper/CmdHelper'), elementHelper = require('../../../../helper/ElementHelper'); var findExtension = require('../Helper').findExtension, findExtensions = require('../Helper').findExtensions, findInputParameter = require('../Helper').findInputParameter, findOutputParameter = require('../Helper').findOutputParameter, findActivitiProperty = require('../Helper').findActivitiProperty, findActivitiInOut = require('../Helper').findActivitiInOut; var createActivitiProperty = require('../CreateHelper').createActivitiProperty, createInputParameter = require('../CreateHelper').createInputParameter, createOutputParameter = require('../CreateHelper').createOutputParameter, createActivitiIn = require('../CreateHelper').createActivitiIn, createActivitiOut = require('../CreateHelper').createActivitiOut, createActivitiInWithBusinessKey = require('../CreateHelper').createActivitiInWithBusinessKey, createActivitiFieldInjection = require('../CreateHelper').createActivitiFieldInjection; var ACTIVITI_PROPERTY_TYPE = 'activiti:property', ACTIVITI_INPUT_PARAMETER_TYPE = 'activiti:inputParameter', ACTIVITI_OUTPUT_PARAMETER_TYPE = 'activiti:outputParameter', ACTIVITI_IN_TYPE = 'activiti:in', ACTIVITI_OUT_TYPE = 'activiti:out', ACTIVITI_IN_BUSINESS_KEY_TYPE = 'activiti:in:businessKey', ACTIVITI_EXECUTION_LISTENER_TYPE = 'activiti:executionListener', ACTIVITI_FIELD = 'activiti:field'; var BASIC_MODDLE_TYPES = [ 'Boolean', 'Integer', 'String' ]; var EXTENSION_BINDING_TYPES = [ ACTIVITI_PROPERTY_TYPE, ACTIVITI_INPUT_PARAMETER_TYPE, ACTIVITI_OUTPUT_PARAMETER_TYPE, ACTIVITI_IN_TYPE, ACTIVITI_OUT_TYPE, ACTIVITI_IN_BUSINESS_KEY_TYPE, ACTIVITI_FIELD ]; var IO_BINDING_TYPES = [ ACTIVITI_INPUT_PARAMETER_TYPE, ACTIVITI_OUTPUT_PARAMETER_TYPE ]; var IN_OUT_BINDING_TYPES = [ ACTIVITI_IN_TYPE, ACTIVITI_OUT_TYPE, ACTIVITI_IN_BUSINESS_KEY_TYPE ]; /** * Injects custom properties into the given group. * * @param {djs.model.Base} element * @param {ElementTemplates} elementTemplates * @param {BpmnFactory} bpmnFactory * @param {Function} translate */ module.exports = function(element, elementTemplates, bpmnFactory, translate) { var template = getTemplate(element, elementTemplates); if (!template) { return []; } var renderCustomField = function(id, p, idx) { var propertyType = p.type; var entryOptions = { id: id, description: p.description, label: p.label ? translate(p.label) : p.label, modelProperty: id, get: propertyGetter(id, p), set: propertySetter(id, p, bpmnFactory), validate: propertyValidator(id, p, translate) }; var entry; if (propertyType === 'Boolean') { entry = entryFactory.checkbox(entryOptions); } if (propertyType === 'String') { entry = entryFactory.textField(entryOptions); } if (propertyType === 'Text') { entry = entryFactory.textBox(entryOptions); } if (propertyType === 'Dropdown') { entryOptions.selectOptions = p.choices; entry = entryFactory.selectBox(entryOptions); } return entry; }; var groups = []; var id, entry; var customFieldsGroup = { id: 'customField', label: translate('Custom Fields'), entries: [] }; template.properties.forEach(function(p, idx) { id = 'custom-' + template.id + '-' + idx; entry = renderCustomField(id, p, idx); if (entry) { customFieldsGroup.entries.push(entry); } }); if (customFieldsGroup.entries.length > 0) { groups.push(customFieldsGroup); } if (template.scopes) { for (var scopeName in template.scopes) { var scope = template.scopes[scopeName]; var idScopeName = scopeName.replace(/:/g, '_'); var customScopeFieldsGroup = { id: 'customField-' + idScopeName, label: translate('Custom Fields for scope: ') + scopeName, entries: [] }; scope.properties.forEach(function(p, idx) { var propertyId = 'custom-' + template.id + '-' + idScopeName + '-' + idx; var scopedProperty = propertyWithScope(p, scopeName); entry = renderCustomField(propertyId, scopedProperty, idx); if (entry) { customScopeFieldsGroup.entries.push(entry); } }); if (customScopeFieldsGroup.entries.length > 0) { groups.push(customScopeFieldsGroup); } } } return groups; }; // getters, setters and validators /////////////// /** * Return a getter that retrieves the given property. * * @param {String} name * @param {PropertyDescriptor} property * * @return {Function} */ function propertyGetter(name, property) { /* getter */ return function get(element) { var value = getPropertyValue(element, property); return objectWithKey(name, value); }; } /** * Return a setter that updates the given property. * * @param {String} name * @param {PropertyDescriptor} property * @param {BpmnFactory} bpmnFactory * * @return {Function} */ function propertySetter(name, property, bpmnFactory) { /* setter */ return function set(element, values) { var value = values[name]; return setPropertyValue(element, property, value, bpmnFactory); }; } /** * Return a validator that ensures the property is ok. * * @param {String} name * @param {PropertyDescriptor} property * @param {Function} translate * * @return {Function} */ function propertyValidator(name, property, translate) { /* validator */ return function validate(element, values) { var value = values[name]; var error = validateValue(value, property, translate); if (error) { return objectWithKey(name, error); } }; } // get, set and validate helpers /////////////////// /** * Return the value of the specified property descriptor, * on the passed diagram element. * * @param {djs.model.Base} element * @param {PropertyDescriptor} property * * @return {Any} */ function getPropertyValue(element, property) { var bo = getBusinessObject(element); var binding = property.binding, scope = property.scope; var bindingType = binding.type, bindingName = binding.name; var propertyValue = property.value || ''; if (scope) { bo = findExtension(bo, scope.name); if (!bo) { return propertyValue; } } // property if (bindingType === 'property') { var value = bo.get(bindingName); if (bindingName === 'conditionExpression') { if (value) { return value.body; } else { // return defined default return propertyValue; } } else { // return value; default to defined default return typeof value !== 'undefined' ? value : propertyValue; } } var activitiProperties, activitiProperty; if (bindingType === ACTIVITI_PROPERTY_TYPE) { if (scope) { activitiProperties = bo.get('properties'); } else { activitiProperties = findExtension(bo, 'activiti:Properties'); } if (activitiProperties) { activitiProperty = findActivitiProperty(activitiProperties, binding); if (activitiProperty) { return activitiProperty.value; } } return propertyValue; } var inputOutput, ioParameter; if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) { if (scope) { inputOutput = bo.get('inputOutput'); } else { inputOutput = findExtension(bo, 'activiti:InputOutput'); } if (!inputOutput) { // ioParameter cannot exist yet, return property value return propertyValue; } } // activiti input parameter if (bindingType === ACTIVITI_INPUT_PARAMETER_TYPE) { ioParameter = findInputParameter(inputOutput, binding); if (ioParameter) { if (binding.scriptFormat) { if (ioParameter.definition) { return ioParameter.definition.value; } } else { return ioParameter.value || ''; } } return propertyValue; } // activiti output parameter if (binding.type === ACTIVITI_OUTPUT_PARAMETER_TYPE) { ioParameter = findOutputParameter(inputOutput, binding); if (ioParameter) { return ioParameter.name; } return propertyValue; } var ioElement; if (IN_OUT_BINDING_TYPES.indexOf(bindingType) != -1) { ioElement = findActivitiInOut(bo, binding); if (ioElement) { if (bindingType === ACTIVITI_IN_BUSINESS_KEY_TYPE) { return ioElement.businessKey; } else if (bindingType === ACTIVITI_OUT_TYPE) { return ioElement.target; } else if (bindingType === ACTIVITI_IN_TYPE) { return ioElement[binding.expression ? 'sourceExpression' : 'source']; } } return propertyValue; } if (bindingType === ACTIVITI_EXECUTION_LISTENER_TYPE) { var executionListener; if (scope) { executionListener = bo.get('executionListener'); } else { executionListener = findExtension(bo, 'activiti:ExecutionListener'); } return executionListener.script.value; } var fieldInjection; if (ACTIVITI_FIELD === bindingType) { var fieldInjections = findExtensions(bo, [ 'activiti:Field' ]); fieldInjections.forEach(function(item) { if (item.name === binding.name) { fieldInjection = item; } }); if (fieldInjection) { return fieldInjection.string || fieldInjection.expression; } else { return ''; } } throw unknownPropertyBinding(property); } module.exports.getPropertyValue = getPropertyValue; /** * Return an update operation that changes the diagram * element's custom property to the given value. * * The response of this method will be processed via * {@link PropertiesPanel#applyChanges}. * * @param {djs.model.Base} element * @param {PropertyDescriptor} property * @param {String} value * @param {BpmnFactory} bpmnFactory * * @return {Object|Array} results to be processed */ function setPropertyValue(element, property, value, bpmnFactory) { var bo = getBusinessObject(element); var binding = property.binding, scope = property.scope; var bindingType = binding.type, bindingName = binding.name; var propertyValue; var updates = []; var extensionElements; if (EXTENSION_BINDING_TYPES.indexOf(bindingType) !== -1) { extensionElements = bo.get('extensionElements'); // create extension elements, if they do not exist (yet) if (!extensionElements) { extensionElements = elementHelper.createElement('bpmn:ExtensionElements', null, element, bpmnFactory); updates.push(cmdHelper.updateBusinessObject( element, bo, objectWithKey('extensionElements', extensionElements) )); } } if (scope) { bo = findExtension(bo, scope.name); if (!bo) { bo = elementHelper.createElement(scope.name, null, element, bpmnFactory); updates.push(cmdHelper.addElementsTolist( bo, extensionElements, 'values', [ bo ] )); } } // property if (bindingType === 'property') { if (bindingName === 'conditionExpression') { propertyValue = elementHelper.createElement('bpmn:FormalExpression', { body: value, language: binding.scriptFormat }, bo, bpmnFactory); } else { var moddlePropertyDescriptor = bo.$descriptor.propertiesByName[bindingName]; var moddleType = moddlePropertyDescriptor.type; // make sure we only update String, Integer, Real and // Boolean properties (do not accidentally override complex objects...) if (BASIC_MODDLE_TYPES.indexOf(moddleType) === -1) { throw new Error('cannot set moddle type <' + moddleType + '>'); } if (moddleType === 'Boolean') { propertyValue = !!value; } else if (moddleType === 'Integer') { propertyValue = parseInt(value, 10); if (isNaN(propertyValue)) { // do not write NaN value propertyValue = undefined; } } else { propertyValue = value; } } if (propertyValue !== undefined) { updates.push(cmdHelper.updateBusinessObject( element, bo, objectWithKey(bindingName, propertyValue) )); } } // activiti:property var activitiProperties, existingActivitiProperty, newActivitiProperty; if (bindingType === ACTIVITI_PROPERTY_TYPE) { if (scope) { activitiProperties = bo.get('properties'); } else { activitiProperties = findExtension(extensionElements, 'activiti:Properties'); } if (!activitiProperties) { activitiProperties = elementHelper.createElement('activiti:Properties', null, bo, bpmnFactory); if (scope) { updates.push(cmdHelper.updateBusinessObject( element, bo, { properties: activitiProperties } )); } else { updates.push(cmdHelper.addElementsTolist( element, extensionElements, 'values', [ activitiProperties ] )); } } existingActivitiProperty = findActivitiProperty(activitiProperties, binding); newActivitiProperty = createActivitiProperty(binding, value, bpmnFactory); updates.push(cmdHelper.addAndRemoveElementsFromList( element, activitiProperties, 'values', null, [ newActivitiProperty ], existingActivitiProperty ? [ existingActivitiProperty ] : [] )); } // activiti:inputParameter // activiti:outputParameter var inputOutput, existingIoParameter, newIoParameter; if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) { if (scope) { inputOutput = bo.get('inputOutput'); } else { inputOutput = findExtension(extensionElements, 'activiti:InputOutput'); } // create inputOutput element, if it do not exist (yet) if (!inputOutput) { inputOutput = elementHelper.createElement('activiti:InputOutput', null, bo, bpmnFactory); if (scope) { updates.push(cmdHelper.updateBusinessObject( element, bo, { inputOutput: inputOutput } )); } else { updates.push(cmdHelper.addElementsTolist( element, extensionElements, 'values', inputOutput )); } } } if (bindingType === ACTIVITI_INPUT_PARAMETER_TYPE) { existingIoParameter = findInputParameter(inputOutput, binding); newIoParameter = createInputParameter(binding, value, bpmnFactory); updates.push(cmdHelper.addAndRemoveElementsFromList( element, inputOutput, 'inputParameters', null, [ newIoParameter ], existingIoParameter ? [ existingIoParameter ] : [] )); } if (bindingType === ACTIVITI_OUTPUT_PARAMETER_TYPE) { existingIoParameter = findOutputParameter(inputOutput, binding); newIoParameter = createOutputParameter(binding, value, bpmnFactory); updates.push(cmdHelper.addAndRemoveElementsFromList( element, inputOutput, 'outputParameters', null, [ newIoParameter ], existingIoParameter ? [ existingIoParameter ] : [] )); } // activiti:in // activiti:out // activiti:in:businessKey var existingInOut, newInOut; if (IN_OUT_BINDING_TYPES.indexOf(bindingType) !== -1) { existingInOut = findActivitiInOut(bo, binding); if (bindingType === ACTIVITI_IN_TYPE) { newInOut = createActivitiIn(binding, value, bpmnFactory); } else if (bindingType === ACTIVITI_OUT_TYPE) { newInOut = createActivitiOut(binding, value, bpmnFactory); } else { newInOut = createActivitiInWithBusinessKey(binding, value, bpmnFactory); } updates.push(cmdHelper.addAndRemoveElementsFromList( element, extensionElements, 'values', null, [ newInOut ], existingInOut ? [ existingInOut ] : [] )); } if (bindingType === ACTIVITI_FIELD) { var existingFieldInjections = findExtensions(bo, [ 'activiti:Field' ]); var newFieldInjections = []; if (existingFieldInjections.length > 0) { existingFieldInjections.forEach(function(item) { if (item.name === binding.name) { newFieldInjections.push(createActivitiFieldInjection(binding, value, bpmnFactory)); } else { newFieldInjections.push(item); } }); } else { newFieldInjections.push(createActivitiFieldInjection(binding, value, bpmnFactory)); } updates.push(cmdHelper.addAndRemoveElementsFromList( element, extensionElements, 'values', null, newFieldInjections, existingFieldInjections ? existingFieldInjections : [] )); } if (updates.length) { return updates; } // quick warning for better debugging console.warn('no update', element, property, value); } module.exports.setPropertyValue = setPropertyValue; /** * Validate value of a given property. * * @param {String} value * @param {PropertyDescriptor} property * @param {Function} translate * * @return {Object} with validation errors */ function validateValue(value, property, translate) { var constraints = property.constraints || {}; if (constraints.notEmpty && isEmpty(value)) { return translate('Must not be empty'); } if (constraints.maxLength && value.length > constraints.maxLength) { return translate('Must have max length {length}', { length: constraints.maxLength }); } if (constraints.minLength && value.length < constraints.minLength) { return translate('Must have min length {length}', { length: constraints.minLength }); } var pattern = constraints.pattern, message; if (pattern) { if (typeof pattern !== 'string') { message = pattern.message; pattern = pattern.value; } if (!matchesPattern(value, pattern)) { return message || translate('Must match pattern {pattern}', { pattern: pattern }); } } } // misc helpers /////////////////////////////// function propertyWithScope(property, scopeName) { if (!scopeName) { return property; } return assign({}, property, { scope: { name: scopeName } }); } /** * Return an object with a single key -> value association. * * @param {String} key * @param {Any} value * * @return {Object} */ function objectWithKey(key, value) { var obj = {}; obj[key] = value; return obj; } /** * Does the given string match the specified pattern? * * @param {String} str * @param {String} pattern * * @return {Boolean} */ function matchesPattern(str, pattern) { var regexp = new RegExp(pattern); return regexp.test(str); } function isEmpty(str) { return !str || /^\s*$/.test(str); } /** * Create a new {@link Error} indicating an unknown * property binding. * * @param {PropertyDescriptor} property * * @return {Error} */ function unknownPropertyBinding(property) { var binding = property.binding; return new Error('unknown binding: <' + binding.type + '>'); }