CustomProps.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. 'use strict';
  2. var assign = require('lodash/assign');
  3. var entryFactory = require('../../../../factory/EntryFactory'),
  4. getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject,
  5. getTemplate = require('../Helper').getTemplate,
  6. cmdHelper = require('../../../../helper/CmdHelper'),
  7. elementHelper = require('../../../../helper/ElementHelper');
  8. var findExtension = require('../Helper').findExtension,
  9. findExtensions = require('../Helper').findExtensions,
  10. findInputParameter = require('../Helper').findInputParameter,
  11. findOutputParameter = require('../Helper').findOutputParameter,
  12. findActivitiProperty = require('../Helper').findActivitiProperty,
  13. findActivitiInOut = require('../Helper').findActivitiInOut;
  14. var createActivitiProperty = require('../CreateHelper').createActivitiProperty,
  15. createInputParameter = require('../CreateHelper').createInputParameter,
  16. createOutputParameter = require('../CreateHelper').createOutputParameter,
  17. createActivitiIn = require('../CreateHelper').createActivitiIn,
  18. createActivitiOut = require('../CreateHelper').createActivitiOut,
  19. createActivitiInWithBusinessKey = require('../CreateHelper').createActivitiInWithBusinessKey,
  20. createActivitiFieldInjection = require('../CreateHelper').createActivitiFieldInjection;
  21. var ACTIVITI_PROPERTY_TYPE = 'activiti:property',
  22. ACTIVITI_INPUT_PARAMETER_TYPE = 'activiti:inputParameter',
  23. ACTIVITI_OUTPUT_PARAMETER_TYPE = 'activiti:outputParameter',
  24. ACTIVITI_IN_TYPE = 'activiti:in',
  25. ACTIVITI_OUT_TYPE = 'activiti:out',
  26. ACTIVITI_IN_BUSINESS_KEY_TYPE = 'activiti:in:businessKey',
  27. ACTIVITI_EXECUTION_LISTENER_TYPE = 'activiti:executionListener',
  28. ACTIVITI_FIELD = 'activiti:field';
  29. var BASIC_MODDLE_TYPES = [
  30. 'Boolean',
  31. 'Integer',
  32. 'String'
  33. ];
  34. var EXTENSION_BINDING_TYPES = [
  35. ACTIVITI_PROPERTY_TYPE,
  36. ACTIVITI_INPUT_PARAMETER_TYPE,
  37. ACTIVITI_OUTPUT_PARAMETER_TYPE,
  38. ACTIVITI_IN_TYPE,
  39. ACTIVITI_OUT_TYPE,
  40. ACTIVITI_IN_BUSINESS_KEY_TYPE,
  41. ACTIVITI_FIELD
  42. ];
  43. var IO_BINDING_TYPES = [
  44. ACTIVITI_INPUT_PARAMETER_TYPE,
  45. ACTIVITI_OUTPUT_PARAMETER_TYPE
  46. ];
  47. var IN_OUT_BINDING_TYPES = [
  48. ACTIVITI_IN_TYPE,
  49. ACTIVITI_OUT_TYPE,
  50. ACTIVITI_IN_BUSINESS_KEY_TYPE
  51. ];
  52. /**
  53. * Injects custom properties into the given group.
  54. *
  55. * @param {djs.model.Base} element
  56. * @param {ElementTemplates} elementTemplates
  57. * @param {BpmnFactory} bpmnFactory
  58. * @param {Function} translate
  59. */
  60. module.exports = function(element, elementTemplates, bpmnFactory, translate) {
  61. var template = getTemplate(element, elementTemplates);
  62. if (!template) {
  63. return [];
  64. }
  65. var renderCustomField = function(id, p, idx) {
  66. var propertyType = p.type;
  67. var entryOptions = {
  68. id: id,
  69. description: p.description,
  70. label: p.label ? translate(p.label) : p.label,
  71. modelProperty: id,
  72. get: propertyGetter(id, p),
  73. set: propertySetter(id, p, bpmnFactory),
  74. validate: propertyValidator(id, p, translate)
  75. };
  76. var entry;
  77. if (propertyType === 'Boolean') {
  78. entry = entryFactory.checkbox(entryOptions);
  79. }
  80. if (propertyType === 'String') {
  81. entry = entryFactory.textField(entryOptions);
  82. }
  83. if (propertyType === 'Text') {
  84. entry = entryFactory.textBox(entryOptions);
  85. }
  86. if (propertyType === 'Dropdown') {
  87. entryOptions.selectOptions = p.choices;
  88. entry = entryFactory.selectBox(entryOptions);
  89. }
  90. return entry;
  91. };
  92. var groups = [];
  93. var id, entry;
  94. var customFieldsGroup = {
  95. id: 'customField',
  96. label: translate('Custom Fields'),
  97. entries: []
  98. };
  99. template.properties.forEach(function(p, idx) {
  100. id = 'custom-' + template.id + '-' + idx;
  101. entry = renderCustomField(id, p, idx);
  102. if (entry) {
  103. customFieldsGroup.entries.push(entry);
  104. }
  105. });
  106. if (customFieldsGroup.entries.length > 0) {
  107. groups.push(customFieldsGroup);
  108. }
  109. if (template.scopes) {
  110. for (var scopeName in template.scopes) {
  111. var scope = template.scopes[scopeName];
  112. var idScopeName = scopeName.replace(/:/g, '_');
  113. var customScopeFieldsGroup = {
  114. id: 'customField-' + idScopeName,
  115. label: translate('Custom Fields for scope: ') + scopeName,
  116. entries: []
  117. };
  118. scope.properties.forEach(function(p, idx) {
  119. var propertyId = 'custom-' + template.id + '-' + idScopeName + '-' + idx;
  120. var scopedProperty = propertyWithScope(p, scopeName);
  121. entry = renderCustomField(propertyId, scopedProperty, idx);
  122. if (entry) {
  123. customScopeFieldsGroup.entries.push(entry);
  124. }
  125. });
  126. if (customScopeFieldsGroup.entries.length > 0) {
  127. groups.push(customScopeFieldsGroup);
  128. }
  129. }
  130. }
  131. return groups;
  132. };
  133. // getters, setters and validators ///////////////
  134. /**
  135. * Return a getter that retrieves the given property.
  136. *
  137. * @param {String} name
  138. * @param {PropertyDescriptor} property
  139. *
  140. * @return {Function}
  141. */
  142. function propertyGetter(name, property) {
  143. /* getter */
  144. return function get(element) {
  145. var value = getPropertyValue(element, property);
  146. return objectWithKey(name, value);
  147. };
  148. }
  149. /**
  150. * Return a setter that updates the given property.
  151. *
  152. * @param {String} name
  153. * @param {PropertyDescriptor} property
  154. * @param {BpmnFactory} bpmnFactory
  155. *
  156. * @return {Function}
  157. */
  158. function propertySetter(name, property, bpmnFactory) {
  159. /* setter */
  160. return function set(element, values) {
  161. var value = values[name];
  162. return setPropertyValue(element, property, value, bpmnFactory);
  163. };
  164. }
  165. /**
  166. * Return a validator that ensures the property is ok.
  167. *
  168. * @param {String} name
  169. * @param {PropertyDescriptor} property
  170. * @param {Function} translate
  171. *
  172. * @return {Function}
  173. */
  174. function propertyValidator(name, property, translate) {
  175. /* validator */
  176. return function validate(element, values) {
  177. var value = values[name];
  178. var error = validateValue(value, property, translate);
  179. if (error) {
  180. return objectWithKey(name, error);
  181. }
  182. };
  183. }
  184. // get, set and validate helpers ///////////////////
  185. /**
  186. * Return the value of the specified property descriptor,
  187. * on the passed diagram element.
  188. *
  189. * @param {djs.model.Base} element
  190. * @param {PropertyDescriptor} property
  191. *
  192. * @return {Any}
  193. */
  194. function getPropertyValue(element, property) {
  195. var bo = getBusinessObject(element);
  196. var binding = property.binding,
  197. scope = property.scope;
  198. var bindingType = binding.type,
  199. bindingName = binding.name;
  200. var propertyValue = property.value || '';
  201. if (scope) {
  202. bo = findExtension(bo, scope.name);
  203. if (!bo) {
  204. return propertyValue;
  205. }
  206. }
  207. // property
  208. if (bindingType === 'property') {
  209. var value = bo.get(bindingName);
  210. if (bindingName === 'conditionExpression') {
  211. if (value) {
  212. return value.body;
  213. } else {
  214. // return defined default
  215. return propertyValue;
  216. }
  217. } else {
  218. // return value; default to defined default
  219. return typeof value !== 'undefined' ? value : propertyValue;
  220. }
  221. }
  222. var activitiProperties,
  223. activitiProperty;
  224. if (bindingType === ACTIVITI_PROPERTY_TYPE) {
  225. if (scope) {
  226. activitiProperties = bo.get('properties');
  227. } else {
  228. activitiProperties = findExtension(bo, 'activiti:Properties');
  229. }
  230. if (activitiProperties) {
  231. activitiProperty = findActivitiProperty(activitiProperties, binding);
  232. if (activitiProperty) {
  233. return activitiProperty.value;
  234. }
  235. }
  236. return propertyValue;
  237. }
  238. var inputOutput,
  239. ioParameter;
  240. if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) {
  241. if (scope) {
  242. inputOutput = bo.get('inputOutput');
  243. } else {
  244. inputOutput = findExtension(bo, 'activiti:InputOutput');
  245. }
  246. if (!inputOutput) {
  247. // ioParameter cannot exist yet, return property value
  248. return propertyValue;
  249. }
  250. }
  251. // activiti input parameter
  252. if (bindingType === ACTIVITI_INPUT_PARAMETER_TYPE) {
  253. ioParameter = findInputParameter(inputOutput, binding);
  254. if (ioParameter) {
  255. if (binding.scriptFormat) {
  256. if (ioParameter.definition) {
  257. return ioParameter.definition.value;
  258. }
  259. } else {
  260. return ioParameter.value || '';
  261. }
  262. }
  263. return propertyValue;
  264. }
  265. // activiti output parameter
  266. if (binding.type === ACTIVITI_OUTPUT_PARAMETER_TYPE) {
  267. ioParameter = findOutputParameter(inputOutput, binding);
  268. if (ioParameter) {
  269. return ioParameter.name;
  270. }
  271. return propertyValue;
  272. }
  273. var ioElement;
  274. if (IN_OUT_BINDING_TYPES.indexOf(bindingType) != -1) {
  275. ioElement = findActivitiInOut(bo, binding);
  276. if (ioElement) {
  277. if (bindingType === ACTIVITI_IN_BUSINESS_KEY_TYPE) {
  278. return ioElement.businessKey;
  279. } else
  280. if (bindingType === ACTIVITI_OUT_TYPE) {
  281. return ioElement.target;
  282. } else
  283. if (bindingType === ACTIVITI_IN_TYPE) {
  284. return ioElement[binding.expression ? 'sourceExpression' : 'source'];
  285. }
  286. }
  287. return propertyValue;
  288. }
  289. if (bindingType === ACTIVITI_EXECUTION_LISTENER_TYPE) {
  290. var executionListener;
  291. if (scope) {
  292. executionListener = bo.get('executionListener');
  293. } else {
  294. executionListener = findExtension(bo, 'activiti:ExecutionListener');
  295. }
  296. return executionListener.script.value;
  297. }
  298. var fieldInjection;
  299. if (ACTIVITI_FIELD === bindingType) {
  300. var fieldInjections = findExtensions(bo, [ 'activiti:Field' ]);
  301. fieldInjections.forEach(function(item) {
  302. if (item.name === binding.name) {
  303. fieldInjection = item;
  304. }
  305. });
  306. if (fieldInjection) {
  307. return fieldInjection.string || fieldInjection.expression;
  308. } else {
  309. return '';
  310. }
  311. }
  312. throw unknownPropertyBinding(property);
  313. }
  314. module.exports.getPropertyValue = getPropertyValue;
  315. /**
  316. * Return an update operation that changes the diagram
  317. * element's custom property to the given value.
  318. *
  319. * The response of this method will be processed via
  320. * {@link PropertiesPanel#applyChanges}.
  321. *
  322. * @param {djs.model.Base} element
  323. * @param {PropertyDescriptor} property
  324. * @param {String} value
  325. * @param {BpmnFactory} bpmnFactory
  326. *
  327. * @return {Object|Array<Object>} results to be processed
  328. */
  329. function setPropertyValue(element, property, value, bpmnFactory) {
  330. var bo = getBusinessObject(element);
  331. var binding = property.binding,
  332. scope = property.scope;
  333. var bindingType = binding.type,
  334. bindingName = binding.name;
  335. var propertyValue;
  336. var updates = [];
  337. var extensionElements;
  338. if (EXTENSION_BINDING_TYPES.indexOf(bindingType) !== -1) {
  339. extensionElements = bo.get('extensionElements');
  340. // create extension elements, if they do not exist (yet)
  341. if (!extensionElements) {
  342. extensionElements = elementHelper.createElement('bpmn:ExtensionElements', null, element, bpmnFactory);
  343. updates.push(cmdHelper.updateBusinessObject(
  344. element, bo, objectWithKey('extensionElements', extensionElements)
  345. ));
  346. }
  347. }
  348. if (scope) {
  349. bo = findExtension(bo, scope.name);
  350. if (!bo) {
  351. bo = elementHelper.createElement(scope.name, null, element, bpmnFactory);
  352. updates.push(cmdHelper.addElementsTolist(
  353. bo, extensionElements, 'values', [ bo ]
  354. ));
  355. }
  356. }
  357. // property
  358. if (bindingType === 'property') {
  359. if (bindingName === 'conditionExpression') {
  360. propertyValue = elementHelper.createElement('bpmn:FormalExpression', {
  361. body: value,
  362. language: binding.scriptFormat
  363. }, bo, bpmnFactory);
  364. } else {
  365. var moddlePropertyDescriptor = bo.$descriptor.propertiesByName[bindingName];
  366. var moddleType = moddlePropertyDescriptor.type;
  367. // make sure we only update String, Integer, Real and
  368. // Boolean properties (do not accidentally override complex objects...)
  369. if (BASIC_MODDLE_TYPES.indexOf(moddleType) === -1) {
  370. throw new Error('cannot set moddle type <' + moddleType + '>');
  371. }
  372. if (moddleType === 'Boolean') {
  373. propertyValue = !!value;
  374. } else
  375. if (moddleType === 'Integer') {
  376. propertyValue = parseInt(value, 10);
  377. if (isNaN(propertyValue)) {
  378. // do not write NaN value
  379. propertyValue = undefined;
  380. }
  381. } else {
  382. propertyValue = value;
  383. }
  384. }
  385. if (propertyValue !== undefined) {
  386. updates.push(cmdHelper.updateBusinessObject(
  387. element, bo, objectWithKey(bindingName, propertyValue)
  388. ));
  389. }
  390. }
  391. // activiti:property
  392. var activitiProperties,
  393. existingActivitiProperty,
  394. newActivitiProperty;
  395. if (bindingType === ACTIVITI_PROPERTY_TYPE) {
  396. if (scope) {
  397. activitiProperties = bo.get('properties');
  398. } else {
  399. activitiProperties = findExtension(extensionElements, 'activiti:Properties');
  400. }
  401. if (!activitiProperties) {
  402. activitiProperties = elementHelper.createElement('activiti:Properties', null, bo, bpmnFactory);
  403. if (scope) {
  404. updates.push(cmdHelper.updateBusinessObject(
  405. element, bo, { properties: activitiProperties }
  406. ));
  407. }
  408. else {
  409. updates.push(cmdHelper.addElementsTolist(
  410. element, extensionElements, 'values', [ activitiProperties ]
  411. ));
  412. }
  413. }
  414. existingActivitiProperty = findActivitiProperty(activitiProperties, binding);
  415. newActivitiProperty = createActivitiProperty(binding, value, bpmnFactory);
  416. updates.push(cmdHelper.addAndRemoveElementsFromList(
  417. element,
  418. activitiProperties,
  419. 'values',
  420. null,
  421. [ newActivitiProperty ],
  422. existingActivitiProperty ? [ existingActivitiProperty ] : []
  423. ));
  424. }
  425. // activiti:inputParameter
  426. // activiti:outputParameter
  427. var inputOutput,
  428. existingIoParameter,
  429. newIoParameter;
  430. if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) {
  431. if (scope) {
  432. inputOutput = bo.get('inputOutput');
  433. } else {
  434. inputOutput = findExtension(extensionElements, 'activiti:InputOutput');
  435. }
  436. // create inputOutput element, if it do not exist (yet)
  437. if (!inputOutput) {
  438. inputOutput = elementHelper.createElement('activiti:InputOutput', null, bo, bpmnFactory);
  439. if (scope) {
  440. updates.push(cmdHelper.updateBusinessObject(
  441. element, bo, { inputOutput: inputOutput }
  442. ));
  443. }
  444. else {
  445. updates.push(cmdHelper.addElementsTolist(
  446. element, extensionElements, 'values', inputOutput
  447. ));
  448. }
  449. }
  450. }
  451. if (bindingType === ACTIVITI_INPUT_PARAMETER_TYPE) {
  452. existingIoParameter = findInputParameter(inputOutput, binding);
  453. newIoParameter = createInputParameter(binding, value, bpmnFactory);
  454. updates.push(cmdHelper.addAndRemoveElementsFromList(
  455. element,
  456. inputOutput,
  457. 'inputParameters',
  458. null,
  459. [ newIoParameter ],
  460. existingIoParameter ? [ existingIoParameter ] : []
  461. ));
  462. }
  463. if (bindingType === ACTIVITI_OUTPUT_PARAMETER_TYPE) {
  464. existingIoParameter = findOutputParameter(inputOutput, binding);
  465. newIoParameter = createOutputParameter(binding, value, bpmnFactory);
  466. updates.push(cmdHelper.addAndRemoveElementsFromList(
  467. element,
  468. inputOutput,
  469. 'outputParameters',
  470. null,
  471. [ newIoParameter ],
  472. existingIoParameter ? [ existingIoParameter ] : []
  473. ));
  474. }
  475. // activiti:in
  476. // activiti:out
  477. // activiti:in:businessKey
  478. var existingInOut,
  479. newInOut;
  480. if (IN_OUT_BINDING_TYPES.indexOf(bindingType) !== -1) {
  481. existingInOut = findActivitiInOut(bo, binding);
  482. if (bindingType === ACTIVITI_IN_TYPE) {
  483. newInOut = createActivitiIn(binding, value, bpmnFactory);
  484. } else
  485. if (bindingType === ACTIVITI_OUT_TYPE) {
  486. newInOut = createActivitiOut(binding, value, bpmnFactory);
  487. } else {
  488. newInOut = createActivitiInWithBusinessKey(binding, value, bpmnFactory);
  489. }
  490. updates.push(cmdHelper.addAndRemoveElementsFromList(
  491. element,
  492. extensionElements,
  493. 'values',
  494. null,
  495. [ newInOut ],
  496. existingInOut ? [ existingInOut ] : []
  497. ));
  498. }
  499. if (bindingType === ACTIVITI_FIELD) {
  500. var existingFieldInjections = findExtensions(bo, [ 'activiti:Field' ]);
  501. var newFieldInjections = [];
  502. if (existingFieldInjections.length > 0) {
  503. existingFieldInjections.forEach(function(item) {
  504. if (item.name === binding.name) {
  505. newFieldInjections.push(createActivitiFieldInjection(binding, value, bpmnFactory));
  506. } else {
  507. newFieldInjections.push(item);
  508. }
  509. });
  510. } else {
  511. newFieldInjections.push(createActivitiFieldInjection(binding, value, bpmnFactory));
  512. }
  513. updates.push(cmdHelper.addAndRemoveElementsFromList(
  514. element,
  515. extensionElements,
  516. 'values',
  517. null,
  518. newFieldInjections,
  519. existingFieldInjections ? existingFieldInjections : []
  520. ));
  521. }
  522. if (updates.length) {
  523. return updates;
  524. }
  525. // quick warning for better debugging
  526. console.warn('no update', element, property, value);
  527. }
  528. module.exports.setPropertyValue = setPropertyValue;
  529. /**
  530. * Validate value of a given property.
  531. *
  532. * @param {String} value
  533. * @param {PropertyDescriptor} property
  534. * @param {Function} translate
  535. *
  536. * @return {Object} with validation errors
  537. */
  538. function validateValue(value, property, translate) {
  539. var constraints = property.constraints || {};
  540. if (constraints.notEmpty && isEmpty(value)) {
  541. return translate('Must not be empty');
  542. }
  543. if (constraints.maxLength && value.length > constraints.maxLength) {
  544. return translate('Must have max length {length}', { length: constraints.maxLength });
  545. }
  546. if (constraints.minLength && value.length < constraints.minLength) {
  547. return translate('Must have min length {length}', { length: constraints.minLength });
  548. }
  549. var pattern = constraints.pattern,
  550. message;
  551. if (pattern) {
  552. if (typeof pattern !== 'string') {
  553. message = pattern.message;
  554. pattern = pattern.value;
  555. }
  556. if (!matchesPattern(value, pattern)) {
  557. return message || translate('Must match pattern {pattern}', { pattern: pattern });
  558. }
  559. }
  560. }
  561. // misc helpers ///////////////////////////////
  562. function propertyWithScope(property, scopeName) {
  563. if (!scopeName) {
  564. return property;
  565. }
  566. return assign({}, property, {
  567. scope: {
  568. name: scopeName
  569. }
  570. });
  571. }
  572. /**
  573. * Return an object with a single key -> value association.
  574. *
  575. * @param {String} key
  576. * @param {Any} value
  577. *
  578. * @return {Object}
  579. */
  580. function objectWithKey(key, value) {
  581. var obj = {};
  582. obj[key] = value;
  583. return obj;
  584. }
  585. /**
  586. * Does the given string match the specified pattern?
  587. *
  588. * @param {String} str
  589. * @param {String} pattern
  590. *
  591. * @return {Boolean}
  592. */
  593. function matchesPattern(str, pattern) {
  594. var regexp = new RegExp(pattern);
  595. return regexp.test(str);
  596. }
  597. function isEmpty(str) {
  598. return !str || /^\s*$/.test(str);
  599. }
  600. /**
  601. * Create a new {@link Error} indicating an unknown
  602. * property binding.
  603. *
  604. * @param {PropertyDescriptor} property
  605. *
  606. * @return {Error}
  607. */
  608. function unknownPropertyBinding(property) {
  609. var binding = property.binding;
  610. return new Error('unknown binding: <' + binding.type + '>');
  611. }