@processmaker/modeler 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/img/clipboard.bcc7796a.svg +1 -0
  2. package/dist/modeler.common.js +1790 -740
  3. package/dist/modeler.common.js.map +1 -1
  4. package/dist/modeler.umd.js +1790 -740
  5. package/dist/modeler.umd.js.map +1 -1
  6. package/dist/modeler.umd.min.js +3 -3
  7. package/dist/modeler.umd.min.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/NodeIdGenerator.js +55 -22
  10. package/src/NodeInspector.js +2 -2
  11. package/src/assets/clipboard.svg +1 -0
  12. package/src/components/crown/crownButtons/copyButton.vue +3 -3
  13. package/src/components/crown/crownButtons/duplicateButton.vue +40 -0
  14. package/src/components/crown/crownConfig/crownConfig.vue +7 -1
  15. package/src/components/crown/crownMultiselect/crownMultiselect.vue +15 -6
  16. package/src/components/crown/utils.js +12 -1
  17. package/src/components/hotkeys/copyPaste.js +26 -0
  18. package/src/components/hotkeys/main.js +9 -2
  19. package/src/components/inspectors/InspectorPanel.vue +1 -0
  20. package/src/components/inspectors/LoopCharacteristics.vue +5 -2
  21. package/src/components/inspectors/process.js +5 -1
  22. package/src/components/modeler/Modeler.vue +104 -4
  23. package/src/components/modeler/Selection.vue +18 -4
  24. package/src/components/nodes/association/index.js +3 -0
  25. package/src/components/nodes/dataInputAssociation/dataInputAssociation.vue +36 -26
  26. package/src/components/nodes/genericFlow/DataOutputAssociation.js +54 -2
  27. package/src/components/nodes/genericFlow/genericFlow.vue +0 -17
  28. package/src/components/nodes/node.js +106 -2
  29. package/src/components/toolbar/ToolBar.vue +17 -3
  30. package/src/components/toolbar/breadcrumb/Breadcrumb.vue +7 -0
  31. package/src/components/toolbar/toolbar.scss +11 -0
  32. package/src/mixins/cloneSelection.js +145 -0
  33. package/src/mixins/linkConfig.js +4 -1
  34. package/src/store.js +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@processmaker/modeler",
3
- "version": "1.26.0",
3
+ "version": "1.28.0",
4
4
  "scripts": {
5
5
  "serve": "vue-cli-service serve",
6
6
  "open-cypress": "TZ=UTC cypress open",
@@ -1,11 +1,13 @@
1
1
  export default class NodeIdGenerator {
2
2
  static prefix = 'node_';
3
3
 
4
- #counter = 1;
5
- #diagramCounter = 1;
4
+ static #counter = 1;
5
+ static #diagramCounter = 1;
6
6
 
7
7
  constructor(definitions) {
8
8
  this.definitions = definitions;
9
+ this.refreshLastIdCounter();
10
+ this.refreshLastDiagramIdCounter();
9
11
  }
10
12
 
11
13
  findById(id, root = this.definitions.rootElements, walked = []) {
@@ -17,45 +19,76 @@ export default class NodeIdGenerator {
17
19
  } else if (root instanceof Object && root.$type) {
18
20
  walked.push(root);
19
21
  if (root.id === id) return root;
20
- Object.getOwnPropertyNames(root).find(key => found = !(root[key] instanceof Function) && this.findById(id, root[key], walked));
22
+ Object.getOwnPropertyNames(root).find(key => found = !(root[key] instanceof Function) && (key.substring(0, 1) !== '$') && this.findById(id, root[key], walked));
21
23
  }
22
24
  return found;
23
25
  }
24
26
 
25
- generate() {
26
- let definitionId = this.#generateDefinitionId();
27
- let diagramId = this.#generateDiagramId();
28
-
29
- while (!this.#isDefinitionIdUnique(definitionId)) {
30
- definitionId = this.#generateDefinitionId();
27
+ matchIds(idRegex, root, walked = [], lastIdCounter = 0) {
28
+ if (walked.indexOf(root) > -1) return lastIdCounter;
29
+ if (root instanceof Array) {
30
+ walked.push(root);
31
+ root.forEach(item => lastIdCounter = this.matchIds(idRegex, item, walked, lastIdCounter));
32
+ } else if (root instanceof Object) {
33
+ walked.push(root);
34
+ if (root.id) {
35
+ const match = String(root.id).match(idRegex);
36
+ const idCounter = match ? parseInt(match[1]) : 0;
37
+ if (idCounter > lastIdCounter) {
38
+ lastIdCounter = idCounter;
39
+ }
40
+ }
41
+ Object.getOwnPropertyNames(root).forEach(key => {
42
+ if (!(root[key] instanceof Function) && (key.substring(0, 1) !== '$')) {
43
+ lastIdCounter = this.matchIds(idRegex, root[key], walked, lastIdCounter);
44
+ }
45
+ });
31
46
  }
47
+ return lastIdCounter;
48
+ }
32
49
 
33
- while (!this.#isDiagramIdUnique(diagramId)) {
34
- diagramId = this.#generateDiagramId();
35
- }
50
+ refreshLastIdCounter() {
51
+ let lastIdCounter = this.matchIds(new RegExp(`^${NodeIdGenerator.prefix}(\\d+)$`), this.definitions.rootElements);
52
+ NodeIdGenerator.#counter = lastIdCounter + 1;
53
+ }
36
54
 
55
+ getCounter() {
56
+ this.refreshLastIdCounter();
57
+ return NodeIdGenerator.#counter;
58
+ }
59
+
60
+ refreshLastDiagramIdCounter() {
61
+ let lastIdCounter = this.matchIds(new RegExp(`^${NodeIdGenerator.prefix}(\\d+)_di$`), this.definitions.diagrams);
62
+ NodeIdGenerator.#diagramCounter = lastIdCounter + 1;
63
+ }
64
+
65
+ generate() {
66
+ let definitionId = this.#generateDefinitionId();
67
+ let diagramId = this.#generateDiagramId();
37
68
  return [definitionId, diagramId];
38
69
  }
39
70
 
40
71
  #generateDefinitionId = () => {
41
- const id = NodeIdGenerator.prefix + this.#counter;
42
- this.#counter++;
72
+ const id = NodeIdGenerator.prefix + NodeIdGenerator.#counter;
73
+ NodeIdGenerator.#counter++;
43
74
 
44
75
  return id;
45
76
  };
46
77
 
47
78
  #generateDiagramId = () => {
48
- const id = NodeIdGenerator.prefix + this.#diagramCounter + '_di';
49
- this.#diagramCounter++;
79
+ const id = NodeIdGenerator.prefix + NodeIdGenerator.#diagramCounter + '_di';
80
+ NodeIdGenerator.#diagramCounter++;
50
81
 
51
82
  return id;
52
83
  };
84
+ }
53
85
 
54
- #isDefinitionIdUnique = id => {
55
- return !this.findById(id) && !this.findById(id, this.definitions.diagrams);
56
- };
86
+ // Singleton instance
87
+ let singleton = null;
57
88
 
58
- #isDiagramIdUnique = id => {
59
- return !this.findById(id) && !this.findById(id, this.definitions.diagrams);
60
- };
89
+ export function getNodeIdGenerator(definitions) {
90
+ if (!singleton) {
91
+ singleton = new NodeIdGenerator(definitions);
92
+ }
93
+ return singleton;
61
94
  }
@@ -1,4 +1,4 @@
1
- import NodeIdGenerator from './NodeIdGenerator';
1
+ import { getNodeIdGenerator } from './NodeIdGenerator';
2
2
  import omit from 'lodash/omit';
3
3
 
4
4
  export default class NodeInspector {
@@ -6,7 +6,7 @@ export default class NodeInspector {
6
6
  constructor(definitions, options = {}) {
7
7
  this.index = window.NODE_INSPECTOR_FIRST_INDEX || new Date().getTime();
8
8
  this.definitions = definitions;
9
- this.nodeIdGenerator = new NodeIdGenerator(this.definitions);
9
+ this.nodeIdGenerator = getNodeIdGenerator(this.definitions);
10
10
  this.options = Object.assign({
11
11
  prefix: this.nodeIdGenerator.generate()[0],
12
12
  }, options);
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="#fff" d="M280 64h40c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128C0 92.7 28.7 64 64 64h40 9.6C121 27.5 153.3 0 192 0s71 27.5 78.4 64H280zM64 112c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320c8.8 0 16-7.2 16-16V128c0-8.8-7.2-16-16-16H304v24c0 13.3-10.7 24-24 24H192 104c-13.3 0-24-10.7-24-24V112H64zm128-8a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
@@ -13,12 +13,12 @@
13
13
  aria-hidden="true"
14
14
  >
15
15
  </crown-button>
16
-
16
+
17
17
  </template>
18
18
 
19
19
  <script>
20
20
  import CrownButton from '@/components/crown/crownButtons/crownButton';
21
- import copyIcon from '@/assets/copy-regular.svg';
21
+ import copyIcon from '@/assets/clipboard.svg';
22
22
  import validCopyElements from '@/components/crown/crownButtons/validCopyElements';
23
23
 
24
24
  export default {
@@ -33,7 +33,7 @@ export default {
33
33
  },
34
34
  methods: {
35
35
  copyElement() {
36
- this.$emit('copy-element', this.node, ++this.copyCount);
36
+ this.$emit('copy-element');
37
37
  },
38
38
  },
39
39
  };
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <crown-button
3
+ v-if="node.isBpmnType(...validCopyElements)"
4
+ :title="$t('Duplicate Element')"
5
+ v-b-tooltip.hover.viewport.d50="{ customClass: 'no-pointer-events' }"
6
+ aria-label="Duplicate Element"
7
+ data-test="duplicate-button"
8
+ role="menuitem"
9
+ @click="duplicateElement"
10
+ >
11
+ <img
12
+ :src="duplicateIcon"
13
+ aria-hidden="true"
14
+ >
15
+ </crown-button>
16
+
17
+ </template>
18
+
19
+ <script>
20
+ import CrownButton from '@/components/crown/crownButtons/crownButton';
21
+ import duplicateIcon from '@/assets/copy-regular.svg';
22
+ import validCopyElements from '@/components/crown/crownButtons/validCopyElements';
23
+
24
+ export default {
25
+ components: { CrownButton },
26
+ props: ['node'],
27
+ data() {
28
+ return {
29
+ copyCount: 0,
30
+ duplicateIcon,
31
+ validCopyElements,
32
+ };
33
+ },
34
+ methods: {
35
+ duplicateElement() {
36
+ this.$emit('duplicate-element', this.node, ++this.copyCount);
37
+ },
38
+ },
39
+ };
40
+ </script>
@@ -50,6 +50,11 @@
50
50
  v-on="$listeners"
51
51
  />
52
52
 
53
+ <duplicate-button
54
+ :node="node"
55
+ v-on="$listeners"
56
+ />
57
+
53
58
  <delete-button
54
59
  :graph="graph"
55
60
  :shape="shape"
@@ -79,6 +84,7 @@ import GenericFlowButton from '@/components/crown/crownButtons/genericFlowButton
79
84
  import AssociationFlowButton from '@/components/crown/crownButtons/associationFlowButton';
80
85
  import DataAssociationFlowButton from '@/components/crown/crownButtons/dataAssociationFlowButton';
81
86
  import CopyButton from '@/components/crown/crownButtons/copyButton.vue';
87
+ import DuplicateButton from '@/components/crown/crownButtons/duplicateButton.vue';
82
88
  import CrownDropdowns from '@/components/crown/crownButtons/crownDropdowns';
83
89
  import DefaultFlow from '@/components/crown/crownButtons/defaultFlowButton.vue';
84
90
  import poolLaneCrownConfig from '@/mixins/poolLaneCrownConfig';
@@ -95,6 +101,7 @@ export default {
95
101
  GenericFlowButton,
96
102
  AssociationFlowButton,
97
103
  CopyButton,
104
+ DuplicateButton,
98
105
  DefaultFlow,
99
106
  DataAssociationFlowButton,
100
107
  },
@@ -133,7 +140,6 @@ export default {
133
140
  handler() {
134
141
  this.setNodeColor();
135
142
  },
136
- deep: true,
137
143
  },
138
144
  highlightedShapes(shapes, prevShapes) {
139
145
  if (isEqual(shapes, prevShapes)) {
@@ -42,11 +42,18 @@ export default {
42
42
  nodeToReplace: null,
43
43
  buttons: [
44
44
  {
45
- label: 'Copy Element',
46
- icon: 'copy',
45
+ label: 'Copy Seletion',
46
+ icon: 'clipboard',
47
47
  testId: 'copy-button',
48
48
  role: 'menuitem',
49
- action: this.copyElement,
49
+ action: this.copySelection,
50
+ },
51
+ {
52
+ label: 'Duplicate Selection',
53
+ icon: 'copy',
54
+ testId: 'duplicate-button',
55
+ role: 'menuitem',
56
+ action: this.duplicateSelection,
50
57
  },
51
58
  {
52
59
  label: 'Delete Element',
@@ -70,9 +77,11 @@ export default {
70
77
  highlightedShapes: () => store.getters.highlightedShapes,
71
78
  },
72
79
  methods: {
73
- copyElement() {
74
- // @todo: Implement copyElement
75
- this.$emit('copy-nodes');
80
+ copySelection() {
81
+ this.$emit('copy-selection');
82
+ },
83
+ duplicateSelection() {
84
+ this.$emit('duplicate-selection');
76
85
  },
77
86
  deleteElement() {
78
87
  this.$emit('remove-nodes');
@@ -114,6 +114,7 @@ export function getOrFindDataInput(moddle, task, sourceNode) {
114
114
  inputSets: [],
115
115
  outputSets: [],
116
116
  });
117
+ task.definition.ioSpecification.$parent = task.definition;
117
118
  }
118
119
  // Check if dataInput exists
119
120
  if (!task.definition.ioSpecification.dataInputs) {
@@ -126,6 +127,7 @@ export function getOrFindDataInput(moddle, task, sourceNode) {
126
127
  isCollection: 'false',
127
128
  name: sourceNode.name,
128
129
  }));
130
+ task.definition.ioSpecification.dataInputs[task.definition.ioSpecification.dataInputs.length - 1].$parent = task.definition.ioSpecification;
129
131
  task.definition.ioSpecification.set('dataInputs', task.definition.ioSpecification.dataInputs);
130
132
  }
131
133
  dataInput = task.definition.ioSpecification.dataInputs.find(input => input.id === dataInputId);
@@ -164,13 +166,22 @@ export function getOrFindDataInput(moddle, task, sourceNode) {
164
166
  }
165
167
  inputSet = task.definition.ioSpecification.inputSets[0];
166
168
  // Check if dataInputRef exists
167
- const dataInputRef = inputSet.dataInputRefs.find(ref => ref.id === dataInputId);
169
+ const dataInputRef = inputSet.get('dataInputRefs').find(ref => ref.id === dataInputId);
168
170
  if (!dataInputRef) {
169
171
  inputSet.dataInputRefs.push(dataInput);
170
172
  }
171
173
  return dataInput;
172
174
  }
173
175
 
176
+ export function findIOSpecificationOwner(ioSpec, modeler) {
177
+ const owner = ioSpec.$parent;
178
+ if (!owner) {
179
+ return modeler.nodes.find(node => node.definition.ioSpecification === ioSpec ||
180
+ node.definition.ioSpecification?.id === ioSpec.id
181
+ )?.definition;
182
+ }
183
+ return owner;
184
+ }
174
185
 
175
186
  export function removeDataInput(task, sourceNode) {
176
187
  if (sourceNode.$type !== 'bpmn:DataObjectReference' && sourceNode.$type !== 'bpmn:DataStoreReference') {
@@ -0,0 +1,26 @@
1
+ export default {
2
+ methods: {
3
+ copyPasteHandler(event, options) {
4
+ const node = event.target.nodeName.toLowerCase();
5
+ const isBody = node === 'body';
6
+ const key = event.key.toLowerCase();
7
+ const isCopy = key === 'c';
8
+ const isPaste = key === 'v';
9
+
10
+ if (isBody && isCopy && options.mod) {
11
+ this.copy(event);
12
+ }
13
+ if (isBody && isPaste && options.mod) {
14
+ this.paste(event);
15
+ }
16
+ },
17
+ copy(event) {
18
+ event.preventDefault();
19
+ window.ProcessMaker.$modeler.copyElement();
20
+ },
21
+ paste(event) {
22
+ event.preventDefault();
23
+ window.ProcessMaker.$modeler.pasteElements();
24
+ },
25
+ },
26
+ };
@@ -1,9 +1,15 @@
1
1
  import ZoomInOut from './zoomInOut';
2
+ import CopyPaste from './copyPaste.js';
2
3
  import store from '@/store';
3
4
  import moveShapeByKeypress from './moveWithArrowKeys';
4
5
 
5
6
  export default {
6
- mixins: [ZoomInOut],
7
+ mixins: [ZoomInOut, CopyPaste],
8
+ computed: {
9
+ clientLeftPaper() {
10
+ return store.getters.clientLeftPaper;
11
+ },
12
+ },
7
13
  mounted() {
8
14
  document.addEventListener('keydown', this.keydownListener);
9
15
  document.addEventListener('keyup', this.keyupListener);
@@ -12,6 +18,7 @@ export default {
12
18
  handleHotkeys(event, options) {
13
19
  // Pass event to all handlers
14
20
  this.zoomInOutHandler(event, options);
21
+ this.copyPasteHandler(event, options);
15
22
  },
16
23
  keyupListener(event) {
17
24
  if (event.code === 'Space') {
@@ -43,7 +50,7 @@ export default {
43
50
  const scale = this.paperManager.scale;
44
51
  this.canvasDragPosition = { x: x * scale.sx, y: y * scale.sy };
45
52
  }
46
- if (this.canvasDragPosition) {
53
+ if (this.canvasDragPosition && !this.clientLeftPaper) {
47
54
  this.paperManager.translate(
48
55
  event.offsetX - this.canvasDragPosition.x,
49
56
  event.offsetY - this.canvasDragPosition.y
@@ -2,6 +2,7 @@
2
2
  <transition name="inspector">
3
3
  <b-col
4
4
  v-show="!compressed"
5
+ id="inspector"
5
6
  class="pl-0 h-100 overflow-hidden inspector-column"
6
7
  :class="[{ 'ignore-pointer': canvasDragPosition, 'inspector-column-compressed' : compressed }]"
7
8
  data-test="inspector-column"
@@ -115,6 +115,9 @@ export default {
115
115
  },
116
116
  loopMaximum: 0,
117
117
  loopCondition: null,
118
+ ioSpecification: {
119
+ dataInputs: [],
120
+ },
118
121
  },
119
122
  loopType: null,
120
123
  multiType: null,
@@ -218,7 +221,7 @@ export default {
218
221
  },
219
222
  getLoopDataOutputRef() {
220
223
  if (!this.local.loopCharacteristics || !this.local.loopCharacteristics.loopDataOutputRef) return null;
221
- return this.local.ioSpecification.dataOutputs[0].name;
224
+ return this.local.ioSpecification?.dataOutputs[0].name;
222
225
  },
223
226
  setLoopDataOutputRef(value) {
224
227
  if (!this.local.ioSpecification) {
@@ -252,7 +255,7 @@ export default {
252
255
  },
253
256
  getLoopDataInputRef() {
254
257
  if (!this.local.loopCharacteristics || !this.local.loopCharacteristics.loopDataInputRef) return null;
255
- return this.local.ioSpecification.dataInputs[0].name;
258
+ return this.local.ioSpecification?.dataInputs[0].name;
256
259
  },
257
260
  setLoopDataInputRef(value) {
258
261
  const dataDef = {
@@ -1,6 +1,6 @@
1
1
  import idConfigSettings from './idConfigSettings';
2
2
 
3
- export default {
3
+ const process = {
4
4
  id: 'processmaker-modeler-process',
5
5
  bpmnType: 'bpmn:Process',
6
6
  control: false,
@@ -38,3 +38,7 @@ export default {
38
38
  },
39
39
  ],
40
40
  };
41
+
42
+ export const id = process.id;
43
+
44
+ export default process;
@@ -103,6 +103,10 @@
103
103
  @replace-node="replaceNode"
104
104
  @replace-generic-flow="replaceGenericFlow"
105
105
  @copy-element="copyElement"
106
+ @copy-selection="copyElement"
107
+ @paste-element="pasteElements"
108
+ @duplicate-element="duplicateElement"
109
+ @duplicate-selection="duplicateSelection"
106
110
  @default-flow="toggleDefaultFlow"
107
111
  @shape-resize="shapeResize"
108
112
  />
@@ -112,6 +116,7 @@
112
116
  :graph="graph"
113
117
  :paperManager="paperManager"
114
118
  :useModelGeometry="false"
119
+ @duplicate-selection="duplicateSelection"
115
120
  @remove-nodes="removeNodes"
116
121
  :processNode="processNode"
117
122
  @save-state="pushToUndoStack"
@@ -134,12 +139,13 @@ import InspectorPanel from '@/components/inspectors/InspectorPanel';
134
139
  import undoRedoStore from '@/undoRedoStore';
135
140
  import { Linter } from 'bpmnlint';
136
141
  import linterConfig from '../../../.bpmnlintrc';
137
- import NodeIdGenerator from '../../NodeIdGenerator';
142
+ import { getNodeIdGenerator } from '../../NodeIdGenerator';
138
143
  import Process from '../inspectors/process';
139
144
  import runningInCypressTest from '@/runningInCypressTest';
140
145
  import getValidationProperties from '@/targetValidationUtils';
141
146
  import MiniPaper from '@/components/miniPaper/MiniPaper';
142
147
  import { id as laneId } from '@/components/nodes/poolLane/config';
148
+ import { id as processId } from '@/components/inspectors/process';
143
149
  import { id as sequenceFlowId } from '../nodes/sequenceFlow';
144
150
  import { id as associationId } from '../nodes/association';
145
151
  import { id as messageFlowId } from '../nodes/messageFlow/config';
@@ -162,6 +168,7 @@ import { removeNodeFlows, removeNodeMessageFlows, removeNodeAssociations, remove
162
168
  import { getInvalidNodes } from '@/components/modeler/modelerUtils';
163
169
  import { NodeMigrator } from '@/components/modeler/NodeMigrator';
164
170
  import addLoopCharacteristics from '@/setup/addLoopCharacteristics';
171
+ import cloneSelection from '../../mixins/cloneSelection';
165
172
 
166
173
  import ProcessmakerModelerGenericFlow from '@/components/nodes/genericFlow/genericFlow';
167
174
 
@@ -185,9 +192,11 @@ export default {
185
192
  },
186
193
  },
187
194
  },
188
- mixins: [hotkeys],
195
+ mixins: [hotkeys, cloneSelection],
189
196
  data() {
190
197
  return {
198
+ pasteInProgress: false,
199
+ internalClipboard: [],
191
200
  tooltipTarget: null,
192
201
 
193
202
  /* Custom parsers for handling certain bpmn node types */
@@ -285,6 +294,7 @@ export default {
285
294
  currentXML() {
286
295
  return undoRedoStore.getters.currentState;
287
296
  },
297
+ copiedElements: () => store.getters.copiedElements,
288
298
  /* connectors expect a highlightedNode property */
289
299
  highlightedNode: () => store.getters.highlightedNodes[0],
290
300
  highlightedNodes: () => store.getters.highlightedNodes,
@@ -308,13 +318,73 @@ export default {
308
318
  }
309
319
  source.set('default', flow);
310
320
  },
311
- copyElement(node, copyCount) {
321
+ duplicateElement(node, copyCount) {
312
322
  const clonedNode = node.clone(this.nodeRegistry, this.moddle, this.$t);
313
323
  const yOffset = (node.diagram.bounds.height + 30) * copyCount;
314
324
 
315
325
  clonedNode.diagram.bounds.y += yOffset;
316
326
  this.addNode(clonedNode);
317
327
  },
328
+ copyElement() {
329
+ // Checking if User selected a single flow and tries to copy it, to deny it.
330
+ const flows = [
331
+ sequenceFlowId,
332
+ dataOutputAssociationFlowId,
333
+ dataInputAssociationFlowId,
334
+ genericFlowId,
335
+ processId,
336
+ ];
337
+ if (this.highlightedNodes.length === 1 && flows.includes(this.highlightedNodes[0].type)) return;
338
+ store.commit('setCopiedElements', this.cloneSelection());
339
+ this.$bvToast.toast(this.$t('Object(s) have been copied'), { noCloseButton:true, variant: 'success', solid: true, toaster: 'b-toaster-top-center' });
340
+ },
341
+ async pasteElements() {
342
+ if (this.copiedElements && !this.pasteInProgress) {
343
+ this.pasteInProgress = true;
344
+ try {
345
+ await this.addClonedNodes(this.copiedElements);
346
+ await this.$nextTick();
347
+ await this.paperManager.awaitScheduledUpdates();
348
+ await this.$refs.selector.selectElements(this.findViewElementsFromNodes(this.copiedElements), true);
349
+ await this.$nextTick();
350
+ await store.commit('setCopiedElements', this.cloneSelection());
351
+ this.scrollToSelection();
352
+ } finally {
353
+ this.pasteInProgress = false;
354
+ }
355
+ }
356
+ },
357
+ async duplicateSelection() {
358
+ const clonedNodes = this.cloneSelection();
359
+ if (clonedNodes && clonedNodes.length === 0) {
360
+ return;
361
+ }
362
+ this.$refs.selector.clearSelection();
363
+ await this.addClonedNodes(clonedNodes);
364
+ await this.$nextTick();
365
+ await this.paperManager.awaitScheduledUpdates();
366
+ await this.$refs.selector.selectElements(this.findViewElementsFromNodes(clonedNodes));
367
+ this.scrollToSelection();
368
+ },
369
+ scrollToSelection() {
370
+ const containerRect = this.$refs['paper-container'].getBoundingClientRect();
371
+ const selector = this.$refs.selector;
372
+ const selectorRect = selector.$el.getBoundingClientRect();
373
+ // Scroll to the cloned elements only when they are not visible on the screen.
374
+ if (selectorRect.right > containerRect.right || selectorRect.bottom > containerRect.bottom || selectorRect.left < containerRect.left || selectorRect.top < containerRect.top) {
375
+ const currentPosition = this.paper.translate();
376
+ const newTy = currentPosition.ty - (selectorRect.top - containerRect.top - selectorRect.height);
377
+ this.paper.translate(currentPosition.tx, newTy);
378
+ selector.updateSelectionBox(true);
379
+ }
380
+ },
381
+ findViewElementsFromNodes(nodes) {
382
+ return nodes.map(node => {
383
+ const component = this.$refs.nodeComponent.find(cmp => cmp.node === node);
384
+ const shape = component.shape;
385
+ return this.paper.findViewByModel(shape);
386
+ });
387
+ },
318
388
  async close() {
319
389
  this.$emit('close');
320
390
  },
@@ -726,7 +796,7 @@ export default {
726
796
  async loadXML(xml = this.currentXML) {
727
797
  this.definitions = await this.xmlManager.getDefinitionsFromXml(xml);
728
798
  this.xmlManager.definitions = this.definitions;
729
- this.nodeIdGenerator = new NodeIdGenerator(this.definitions);
799
+ this.nodeIdGenerator = getNodeIdGenerator(this.definitions);
730
800
  store.commit('clearNodes');
731
801
  this.renderPaper();
732
802
  },
@@ -851,8 +921,29 @@ export default {
851
921
  });
852
922
  });
853
923
  },
924
+ async addClonedNodes(nodes) {
925
+ nodes.forEach(node => {
926
+ if (!node.pool) {
927
+ node.pool = this.poolTarget;
928
+ }
929
+
930
+ const targetProcess = node.getTargetProcess(this.processes, this.processNode);
931
+ addNodeToProcess(node, targetProcess);
932
+
933
+ this.planeElements.push(node.diagram);
934
+ store.commit('addNode', node);
935
+ this.poolTarget = null;
936
+ });
937
+
938
+ await this.pushToUndoStack();
939
+ },
854
940
  async removeNode(node, { removeRelationships = true } = {}) {
941
+ if (!node) {
942
+ // already removed
943
+ return;
944
+ }
855
945
  if (removeRelationships) {
946
+
856
947
  removeNodeFlows(node, this);
857
948
  removeNodeMessageFlows(node, this);
858
949
  removeNodeAssociations(node, this);
@@ -1152,10 +1243,19 @@ export default {
1152
1243
  this.pointerUpHandler(event, cellView);
1153
1244
  }, this);
1154
1245
 
1246
+ this.$el.addEventListener('mouseenter', () => {
1247
+ store.commit('setClientLeftPaper', false);
1248
+ });
1249
+
1155
1250
  this.$el.addEventListener('mousemove', event => {
1156
1251
  this.pointerMoveHandler(event);
1157
1252
  });
1158
1253
 
1254
+ this.$el.addEventListener('mouseleave', () => {
1255
+ this.paperManager.removeEventHandler('blank:pointermove');
1256
+ store.commit('setClientLeftPaper', true);
1257
+ });
1258
+
1159
1259
  this.paperManager.addEventHandler('cell:pointerclick', (cellView, evt, x, y) => {
1160
1260
  const clickHandler = cellView.model.get('onClick');
1161
1261
  if (clickHandler) {