BpmnReplace.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import {
  2. pick,
  3. assign,
  4. filter,
  5. forEach,
  6. isArray,
  7. isUndefined,
  8. has
  9. } from 'min-dash';
  10. import {
  11. is,
  12. getBusinessObject
  13. } from '../../util/ModelUtil';
  14. import {
  15. isAny
  16. } from '../modeling/util/ModelingUtil';
  17. import {
  18. isExpanded,
  19. isEventSubProcess
  20. } from '../../util/DiUtil';
  21. import { getPropertyNames } from '../copy-paste/ModdleCopy';
  22. /**
  23. * @typedef {import('../modeling/BpmnFactory').default} BpmnFactory
  24. * @typedef {import('../modeling/ElementFactory').default} ElementFactory
  25. * @typedef {import('../copy-paste/ModdleCopy').default} ModdleCopy
  26. * @typedef {import('../modeling/Modeling').default} Modeling
  27. * @typedef {import('diagram-js/lib/features/replace/Replace').default} Replace
  28. * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules
  29. *
  30. * @typedef {import('../../model/Types').Element} Element
  31. * @typedef {import('../../model/Types').Shape} Shape
  32. * @typedef {import('../../model/Types').ModdleElement} ModdleElement
  33. *
  34. * @typedef { {
  35. * type: string;
  36. * cancelActivity: boolean;
  37. * instantiate: boolean;
  38. * eventGatewayType: string;
  39. * triggeredByEvent: boolean;
  40. * isInterrupting: boolean;
  41. * collapsed: boolean;
  42. * isExpanded: boolean;
  43. * eventDefinitionType: string;
  44. * eventDefinitionAttrs: Object;
  45. * host: Shape;
  46. * } } TargetElement
  47. *
  48. * @typedef { {
  49. * moveChildren: boolean;
  50. * } & Record<string, any> } Hints
  51. */
  52. function copyProperties(source, target, properties) {
  53. if (!isArray(properties)) {
  54. properties = [ properties ];
  55. }
  56. forEach(properties, function(property) {
  57. if (!isUndefined(source[property])) {
  58. target[property] = source[property];
  59. }
  60. });
  61. }
  62. var CUSTOM_PROPERTIES = [
  63. 'cancelActivity',
  64. 'instantiate',
  65. 'eventGatewayType',
  66. 'triggeredByEvent',
  67. 'isInterrupting'
  68. ];
  69. /**
  70. * Check if element should be collapsed or expanded.
  71. */
  72. function shouldToggleCollapsed(element, targetElement) {
  73. var oldCollapsed = (
  74. element && has(element, 'collapsed') ? element.collapsed : !isExpanded(element)
  75. );
  76. var targetCollapsed;
  77. if (targetElement && (has(targetElement, 'collapsed') || has(targetElement, 'isExpanded'))) {
  78. // property is explicitly set so use it
  79. targetCollapsed = (
  80. has(targetElement, 'collapsed') ? targetElement.collapsed : !targetElement.isExpanded
  81. );
  82. } else {
  83. // keep old state
  84. targetCollapsed = oldCollapsed;
  85. }
  86. if (oldCollapsed !== targetCollapsed) {
  87. return true;
  88. }
  89. return false;
  90. }
  91. /**
  92. * BPMN-specific replace.
  93. *
  94. * @param {BpmnFactory} bpmnFactory
  95. * @param {ElementFactory} elementFactory
  96. * @param {ModdleCopy} moddleCopy
  97. * @param {Modeling} modeling
  98. * @param {Replace} replace
  99. * @param {Rules} rules
  100. */
  101. export default function BpmnReplace(
  102. bpmnFactory,
  103. elementFactory,
  104. moddleCopy,
  105. modeling,
  106. replace,
  107. rules
  108. ) {
  109. /**
  110. * Prepares a new business object for the replacement element
  111. * and triggers the replace operation.
  112. *
  113. * @param {Element} element
  114. * @param {TargetElement} targetElement
  115. * @param {Hints} [hints]
  116. *
  117. * @return {Element}
  118. */
  119. function replaceElement(element, targetElement, hints) {
  120. hints = hints || {};
  121. var type = targetElement.type,
  122. oldBusinessObject = element.businessObject;
  123. if (isSubProcess(oldBusinessObject) && type === 'bpmn:SubProcess') {
  124. if (shouldToggleCollapsed(element, targetElement)) {
  125. // expanding or collapsing process
  126. modeling.toggleCollapse(element);
  127. return element;
  128. }
  129. }
  130. var newBusinessObject = bpmnFactory.create(type);
  131. var newElement = {
  132. type: type,
  133. businessObject: newBusinessObject,
  134. };
  135. newElement.di = {};
  136. // colors will be set to DI
  137. copyProperties(element.di, newElement.di, [
  138. 'fill',
  139. 'stroke',
  140. 'background-color',
  141. 'border-color',
  142. 'color'
  143. ]);
  144. var elementProps = getPropertyNames(oldBusinessObject.$descriptor),
  145. newElementProps = getPropertyNames(newBusinessObject.$descriptor, true),
  146. copyProps = intersection(elementProps, newElementProps);
  147. // initialize special properties defined in target definition
  148. assign(newBusinessObject, pick(targetElement, CUSTOM_PROPERTIES));
  149. var properties = filter(copyProps, function(propertyName) {
  150. // copying event definitions, unless we replace
  151. if (propertyName === 'eventDefinitions') {
  152. return hasEventDefinition(element, targetElement.eventDefinitionType);
  153. }
  154. // retain loop characteristics if the target element
  155. // is not an event sub process
  156. if (propertyName === 'loopCharacteristics') {
  157. return !isEventSubProcess(newBusinessObject);
  158. }
  159. // so the applied properties from 'target' don't get lost
  160. if (has(newBusinessObject, propertyName)) {
  161. return false;
  162. }
  163. if (propertyName === 'processRef' && targetElement.isExpanded === false) {
  164. return false;
  165. }
  166. if (propertyName === 'triggeredByEvent') {
  167. return false;
  168. }
  169. if (propertyName === 'isForCompensation') {
  170. return !isEventSubProcess(newBusinessObject);
  171. }
  172. return true;
  173. });
  174. newBusinessObject = moddleCopy.copyElement(
  175. oldBusinessObject,
  176. newBusinessObject,
  177. properties
  178. );
  179. // initialize custom BPMN extensions
  180. if (targetElement.eventDefinitionType) {
  181. // only initialize with new eventDefinition
  182. // if we did not set an event definition yet,
  183. // i.e. because we copied it
  184. if (!hasEventDefinition(newBusinessObject, targetElement.eventDefinitionType)) {
  185. newElement.eventDefinitionType = targetElement.eventDefinitionType;
  186. newElement.eventDefinitionAttrs = targetElement.eventDefinitionAttrs;
  187. }
  188. }
  189. if (is(oldBusinessObject, 'bpmn:Activity')) {
  190. if (isSubProcess(oldBusinessObject)) {
  191. // no toggeling, so keep old state
  192. newElement.isExpanded = isExpanded(element);
  193. }
  194. // else if property is explicitly set, use it
  195. else if (targetElement && has(targetElement, 'isExpanded')) {
  196. newElement.isExpanded = targetElement.isExpanded;
  197. // assign default size of new expanded element
  198. var defaultSize = elementFactory.getDefaultSize(newBusinessObject, {
  199. isExpanded: newElement.isExpanded
  200. });
  201. newElement.width = defaultSize.width;
  202. newElement.height = defaultSize.height;
  203. // keep element centered
  204. newElement.x = element.x - (newElement.width - element.width) / 2;
  205. newElement.y = element.y - (newElement.height - element.height) / 2;
  206. }
  207. // TODO: need also to respect min/max Size
  208. // copy size, from an expanded subprocess to an expanded alternative subprocess
  209. // except bpmn:Task, because Task is always expanded
  210. if ((isExpanded(element) && !is(oldBusinessObject, 'bpmn:Task')) && newElement.isExpanded) {
  211. newElement.width = element.width;
  212. newElement.height = element.height;
  213. }
  214. }
  215. // remove children if not expanding sub process
  216. if (isSubProcess(oldBusinessObject) && !isSubProcess(newBusinessObject)) {
  217. hints.moveChildren = false;
  218. }
  219. // transform collapsed/expanded pools
  220. if (is(oldBusinessObject, 'bpmn:Participant')) {
  221. // create expanded pool
  222. if (targetElement.isExpanded === true) {
  223. newBusinessObject.processRef = bpmnFactory.create('bpmn:Process');
  224. } else {
  225. // remove children when transforming to collapsed pool
  226. hints.moveChildren = false;
  227. }
  228. // apply same width and default height
  229. newElement.width = element.width;
  230. newElement.height = elementFactory.getDefaultSize(newElement).height;
  231. }
  232. if (!rules.allowed('shape.resize', { shape: newBusinessObject })) {
  233. newElement.height = elementFactory.getDefaultSize(newElement).height;
  234. newElement.width = elementFactory.getDefaultSize(newElement).width;
  235. }
  236. newBusinessObject.name = oldBusinessObject.name;
  237. // retain default flow's reference between inclusive <-> exclusive gateways and activities
  238. if (
  239. isAny(oldBusinessObject, [
  240. 'bpmn:ExclusiveGateway',
  241. 'bpmn:InclusiveGateway',
  242. 'bpmn:Activity'
  243. ]) &&
  244. isAny(newBusinessObject, [
  245. 'bpmn:ExclusiveGateway',
  246. 'bpmn:InclusiveGateway',
  247. 'bpmn:Activity'
  248. ])
  249. ) {
  250. newBusinessObject.default = oldBusinessObject.default;
  251. }
  252. if (
  253. targetElement.host &&
  254. !is(oldBusinessObject, 'bpmn:BoundaryEvent') &&
  255. is(newBusinessObject, 'bpmn:BoundaryEvent')
  256. ) {
  257. newElement.host = targetElement.host;
  258. }
  259. // The DataStoreReference element is 14px wider than the DataObjectReference element
  260. // This ensures that they stay centered on the x axis when replaced
  261. if (
  262. newElement.type === 'bpmn:DataStoreReference' ||
  263. newElement.type === 'bpmn:DataObjectReference'
  264. ) {
  265. newElement.x = element.x + (element.width - newElement.width) / 2;
  266. }
  267. return replace.replaceElement(element, newElement, { ...hints, targetElement });
  268. }
  269. this.replaceElement = replaceElement;
  270. }
  271. BpmnReplace.$inject = [
  272. 'bpmnFactory',
  273. 'elementFactory',
  274. 'moddleCopy',
  275. 'modeling',
  276. 'replace',
  277. 'rules'
  278. ];
  279. /**
  280. * @param {ModdleElement} businessObject
  281. *
  282. * @return {boolean}
  283. */
  284. function isSubProcess(businessObject) {
  285. return is(businessObject, 'bpmn:SubProcess');
  286. }
  287. /**
  288. * @param {Element|ModdleElement} element
  289. * @param {string} type
  290. *
  291. * @return {boolean}
  292. */
  293. function hasEventDefinition(element, type) {
  294. var businessObject = getBusinessObject(element);
  295. return type && businessObject.get('eventDefinitions').some(function(definition) {
  296. return is(definition, type);
  297. });
  298. }
  299. /**
  300. * Compute intersection between two arrays.
  301. *
  302. * @param {Array} a
  303. * @param {Array} b
  304. *
  305. * @return {Array}
  306. */
  307. function intersection(a, b) {
  308. return a.filter(function(item) {
  309. return b.includes(item);
  310. });
  311. }