@processmaker/modeler 1.25.0 → 1.27.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@processmaker/modeler",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "scripts": {
5
5
  "serve": "vue-cli-service serve",
6
6
  "open-cypress": "TZ=UTC cypress open",
@@ -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
  },
@@ -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');
@@ -0,0 +1,23 @@
1
+ export default {
2
+ methods: {
3
+ copyPasteHandler(event, options) {
4
+ const isCopy = event.key === 'c';
5
+ const isPaste = event.key === 'v';
6
+
7
+ if (isCopy && options.mod) {
8
+ this.copy(event);
9
+ }
10
+ if (isPaste && options.mod) {
11
+ this.paste(event);
12
+ }
13
+ },
14
+ copy(event) {
15
+ event.preventDefault();
16
+ window.ProcessMaker.$modeler.copyElement();
17
+ },
18
+ paste(event) {
19
+ event.preventDefault();
20
+ window.ProcessMaker.$modeler.pasteElements();
21
+ },
22
+ },
23
+ };
@@ -1,9 +1,10 @@
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],
7
8
  mounted() {
8
9
  document.addEventListener('keydown', this.keydownListener);
9
10
  document.addEventListener('keyup', this.keyupListener);
@@ -12,6 +13,7 @@ export default {
12
13
  handleHotkeys(event, options) {
13
14
  // Pass event to all handlers
14
15
  this.zoomInOutHandler(event, options);
16
+ this.copyPasteHandler(event, options);
15
17
  },
16
18
  keyupListener(event) {
17
19
  if (event.code === 'Space') {
@@ -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"
@@ -11,6 +11,7 @@
11
11
  @toggle-panels-compressed="panelsCompressed = !panelsCompressed"
12
12
  @toggle-mini-map-open="miniMapOpen = $event"
13
13
  @saveBpmn="saveBpmn"
14
+ @close="close"
14
15
  @save-state="pushToUndoStack"
15
16
  @clearSelection="clearSelection"
16
17
  />
@@ -102,6 +103,10 @@
102
103
  @replace-node="replaceNode"
103
104
  @replace-generic-flow="replaceGenericFlow"
104
105
  @copy-element="copyElement"
106
+ @copy-selection="copyElement"
107
+ @paste-element="pasteElements"
108
+ @duplicate-element="duplicateElement"
109
+ @duplicate-selection="duplicateSelection"
105
110
  @default-flow="toggleDefaultFlow"
106
111
  @shape-resize="shapeResize"
107
112
  />
@@ -111,6 +116,7 @@
111
116
  :graph="graph"
112
117
  :paperManager="paperManager"
113
118
  :useModelGeometry="false"
119
+ @duplicate-selection="duplicateSelection"
114
120
  @remove-nodes="removeNodes"
115
121
  :processNode="processNode"
116
122
  @save-state="pushToUndoStack"
@@ -187,6 +193,7 @@ export default {
187
193
  mixins: [hotkeys],
188
194
  data() {
189
195
  return {
196
+ internalClipboard: [],
190
197
  tooltipTarget: null,
191
198
 
192
199
  /* Custom parsers for handling certain bpmn node types */
@@ -284,6 +291,7 @@ export default {
284
291
  currentXML() {
285
292
  return undoRedoStore.getters.currentState;
286
293
  },
294
+ copiedElements: () => store.getters.copiedElements,
287
295
  /* connectors expect a highlightedNode property */
288
296
  highlightedNode: () => store.getters.highlightedNodes[0],
289
297
  highlightedNodes: () => store.getters.highlightedNodes,
@@ -307,13 +315,115 @@ export default {
307
315
  }
308
316
  source.set('default', flow);
309
317
  },
310
- copyElement(node, copyCount) {
318
+ duplicateElement(node, copyCount) {
311
319
  const clonedNode = node.clone(this.nodeRegistry, this.moddle, this.$t);
312
320
  const yOffset = (node.diagram.bounds.height + 30) * copyCount;
313
321
 
314
322
  clonedNode.diagram.bounds.y += yOffset;
315
323
  this.addNode(clonedNode);
316
324
  },
325
+ copyElement() {
326
+ // Checking if User selected a single flow and tries to copy it, to deny it.
327
+ const flows = [
328
+ sequenceFlowId,
329
+ dataOutputAssociationFlowId,
330
+ dataInputAssociationFlowId,
331
+ genericFlowId,
332
+ ];
333
+ if (this.highlightedNodes.length === 1 && flows.includes(this.highlightedNodes[0].type)) return;
334
+ store.commit('setCopiedElements', this.cloneSelection());
335
+ this.$bvToast.toast(this.$t('Object(s) have been copied'), { noCloseButton:true, variant: 'success', solid: true, toaster: 'b-toaster-top-center' });
336
+ },
337
+ async pasteElements() {
338
+ if (this.copiedElements) {
339
+ await this.addClonedNodes(this.copiedElements);
340
+ this.$refs.selector.selectElements(this.findViewElementsFromNodes(this.copiedElements));
341
+ store.commit('setCopiedElements', this.cloneSelection());
342
+ }
343
+ },
344
+ cloneSelection() {
345
+ let clonedNodes = [], clonedFlows = [], originalFlows = [];
346
+ const nodes = this.highlightedNodes;
347
+ const selector = this.$refs.selector.$el;
348
+ const { height: sheight } = selector.getBoundingClientRect();
349
+ if (typeof selector.getBoundingClientRect === 'function') {
350
+ // get selector height
351
+ nodes.forEach(node => {
352
+ // Add flows described in the definitions property
353
+ if (node.definition.incoming || node.definition.outgoing) {
354
+ // Since both incoming and outgoing reference the same flow, any of them is copied
355
+ let flowsToCopy = [...(node.definition.incoming || node.definition.outgoing)];
356
+ // Check if flow is already in array before pushing
357
+ flowsToCopy.forEach(flow => {
358
+ if (!originalFlows.some(el => el.id === flow.id)) {
359
+ originalFlows.push(flow);
360
+ }
361
+ });
362
+ }
363
+
364
+ // Check node type to clone
365
+ if ([
366
+ sequenceFlowId,
367
+ laneId,
368
+ associationId,
369
+ messageFlowId,
370
+ dataOutputAssociationFlowId,
371
+ dataInputAssociationFlowId,
372
+ genericFlowId,
373
+ ].includes(node.type)) {
374
+ // Add offset for all waypoints on cloned flow
375
+ const clonedFlow = node.cloneFlow(this.nodeRegistry, this.moddle, this.$t);
376
+ clonedFlow.setIds(this.nodeIdGenerator);
377
+ clonedFlows.push(clonedFlow);
378
+ clonedNodes.push(clonedFlow);
379
+ } else {
380
+ // Clone node and calculate offset
381
+ const clonedNode = node.clone(this.nodeRegistry, this.moddle, this.$t);
382
+ const yOffset = sheight;
383
+ clonedNode.diagram.bounds.y += yOffset;
384
+ // Set cloned node id
385
+ clonedNode.setIds(this.nodeIdGenerator);
386
+ clonedNodes.push(clonedNode);
387
+ }
388
+ });
389
+ }
390
+ // Connect flows
391
+ clonedFlows.forEach(flow => {
392
+ // Look up the original flow
393
+ const flowClonedFrom = { definition: originalFlows.find(el => el.id === flow.definition.cloneOf) };
394
+ // Get the id's of the sourceRef and targetRef of original flow
395
+ const src = flowClonedFrom.definition.sourceRef;
396
+ const target = flowClonedFrom.definition.targetRef;
397
+ const srcClone = clonedNodes.find(node => node.definition.cloneOf === src.id);
398
+ const targetClone = clonedNodes.find(node => node.definition.cloneOf === target.id);
399
+ // Reference the elements to the flow that connects them
400
+ flow.definition.sourceRef = srcClone.definition;
401
+ flow.definition.targetRef = targetClone.definition;
402
+ // Reference the flow to the elements that are connected by it
403
+ srcClone.definition.outgoing ? srcClone.definition.outgoing.push(flow.definition) : srcClone.definition.outgoing = [flow.definition];
404
+ targetClone.definition.incoming ? targetClone.definition.incoming.push(flow.definition) : targetClone.definition.incoming = [flow.definition];
405
+ // Translate flow waypoints to where they should be
406
+ flow.diagram.waypoint.forEach(point => {
407
+ point.y += sheight;
408
+ });
409
+ });
410
+ return clonedNodes;
411
+ },
412
+ async duplicateSelection() {
413
+ const clonedNodes = this.cloneSelection();
414
+ await this.addClonedNodes(clonedNodes);
415
+ this.$refs.selector.selectElements(this.findViewElementsFromNodes(clonedNodes));
416
+ },
417
+ findViewElementsFromNodes(nodes) {
418
+ return nodes.map(node => {
419
+ const component = this.$refs.nodeComponent.find(cmp => cmp.node === node);
420
+ const shape = component.shape;
421
+ return this.paper.findViewByModel(shape);
422
+ });
423
+ },
424
+ async close() {
425
+ this.$emit('close');
426
+ },
317
427
  async saveBpmn() {
318
428
  const svg = document.querySelector('.mini-paper svg');
319
429
  const css = 'text { font-family: sans-serif; }';
@@ -847,6 +957,22 @@ export default {
847
957
  });
848
958
  });
849
959
  },
960
+ async addClonedNodes(nodes) {
961
+ nodes.forEach(node => {
962
+ if (!node.pool) {
963
+ node.pool = this.poolTarget;
964
+ }
965
+
966
+ const targetProcess = node.getTargetProcess(this.processes, this.processNode);
967
+ addNodeToProcess(node, targetProcess);
968
+
969
+ this.planeElements.push(node.diagram);
970
+ store.commit('addNode', node);
971
+ this.poolTarget = null;
972
+ });
973
+
974
+ await this.pushToUndoStack();
975
+ },
850
976
  async removeNode(node, { removeRelationships = true } = {}) {
851
977
  if (removeRelationships) {
852
978
  removeNodeFlows(node, this);
@@ -975,6 +1101,18 @@ export default {
975
1101
  this.previouslyStackedShape = shape;
976
1102
  this.paperManager.performAtomicAction(() => ensureShapeIsNotCovered(shape, this.graph));
977
1103
  },
1104
+ showSavedNotification() {
1105
+ undoRedoStore.dispatch('saved');
1106
+ },
1107
+ enableVersions() {
1108
+ undoRedoStore.dispatch('enableVersions');
1109
+ },
1110
+ setVersionIndicator(isDraft) {
1111
+ undoRedoStore.dispatch('setVersionIndicator', isDraft);
1112
+ },
1113
+ setLoadingState(isLoading) {
1114
+ undoRedoStore.dispatch('setLoadingState', isLoading);
1115
+ },
978
1116
  clearSelection(){
979
1117
  this.$refs.selector.clearSelection();
980
1118
  },
@@ -1137,7 +1275,13 @@ export default {
1137
1275
  }, this);
1138
1276
 
1139
1277
  this.$el.addEventListener('mousemove', event => {
1278
+ const { clientX, clientY } = event;
1140
1279
  this.pointerMoveHandler(event);
1280
+ store.commit('setClientMousePosition', { clientX, clientY });
1281
+ });
1282
+
1283
+ this.$el.addEventListener('mouseleave', () => {
1284
+ store.commit('clientLeftPaper');
1141
1285
  });
1142
1286
 
1143
1287
  this.paperManager.addEventHandler('cell:pointerclick', (cellView, evt, x, y) => {
@@ -29,6 +29,13 @@ import { id as poolId } from '@/components/nodes/pool/config';
29
29
  import { id as laneId } from '@/components/nodes/poolLane/config';
30
30
  import { id as genericFlowId } from '@/components/nodes/genericFlow/config';
31
31
  import { labelWidth, poolPadding } from '../nodes/pool/poolSizes';
32
+ const boundaryElements = [
33
+ 'processmaker-modeler-boundary-timer-event',
34
+ 'processmaker-modeler-boundary-error-event',
35
+ 'processmaker-modeler-boundary-signal-event',
36
+ 'processmaker-modeler-boundary-conditional-event',
37
+ 'processmaker-modeler-boundary-message-event',
38
+ ];
32
39
  export default {
33
40
  name: 'Selection',
34
41
  components: {
@@ -85,6 +92,17 @@ export default {
85
92
  },
86
93
  },
87
94
  methods: {
95
+ async selectElements(elements) {
96
+ await this.$nextTick();
97
+ this.clearSelection();
98
+ this.selected = elements;
99
+ this.showLasso = true;
100
+ this.isSelected = true;
101
+ this.isSelecting = true;
102
+ this.start = null;
103
+ await this.$nextTick();
104
+ this.updateSelectionBox();
105
+ },
88
106
  /**
89
107
  * Select an element dinamically.
90
108
  * Shift key will manage the condition to push to selection
@@ -321,19 +339,36 @@ export default {
321
339
  * Filter the selected elements
322
340
  */
323
341
  filterSelected() {
324
- // remove from selection the selected child nodes in the pool
342
+ // Get the selected pools IDs
325
343
  const selectedPoolsIds = this.selected
326
344
  .filter(shape => shape.model.component)
327
345
  .filter(shape => shape.model.component.node.type === 'processmaker-modeler-pool')
328
346
  .map(shape => shape.model.component.node.id);
347
+ // remove from selection the selected children that belongs to a selected pool
329
348
  this.selected = this.selected.filter(shape => {
330
349
  if (shape.model.component && shape.model.component.node.pool) {
331
350
  return shape.model.component.node.pool && !selectedPoolsIds.includes(shape.model.component.node.pool.component.node.id);
332
351
  }
333
352
  return true;
334
- }).filter(shape => {
335
- return !(shape.model.getParentCell() && shape.model.getParentCell().get('parent'));
336
353
  });
354
+ // A boundary event could only be selected alone
355
+ const firstSelectedBoundary = this.selected.find(shape => {
356
+ return shape.model.component &&
357
+ boundaryElements.includes(shape.model.component.node.type);
358
+ });
359
+ const firstSelectedElement = this.selected[0];
360
+ if (firstSelectedBoundary) {
361
+ this.selected = this.selected.filter(shape => {
362
+ if (firstSelectedElement === firstSelectedBoundary) {
363
+ // boundary event selected alone
364
+ return shape.model.component &&
365
+ shape === firstSelectedBoundary;
366
+ }
367
+ // do not allow to select a boundary event with another element
368
+ return shape.model.component &&
369
+ !boundaryElements.includes(shape.model.component.node.type);
370
+ });
371
+ }
337
372
  },
338
373
  /**
339
374
  * Pan paper handler
@@ -377,7 +412,7 @@ export default {
377
412
  return shapes && selected.length === shapes.length;
378
413
  },
379
414
  /**
380
- * Start the drag procedure for the selext box
415
+ * Start the drag procedure for the select box
381
416
  * @param {Object} event
382
417
  */
383
418
  startDrag(event) {
@@ -11,6 +11,7 @@ import cloneDeep from 'lodash/cloneDeep';
11
11
  export default class Node {
12
12
  static diagramPropertiesToCopy = ['x', 'y', 'width', 'height'];
13
13
  static definitionPropertiesToNotCopy = ['$type', 'id'];
14
+ static flowDefinitionPropertiesToNotCopy = ['$type', 'id', 'sourceRef', 'targetRef'];
14
15
  static eventDefinitionPropertiesToNotCopy = ['errorRef', 'messageRef'];
15
16
 
16
17
  type;
@@ -89,6 +90,8 @@ export default class Node {
89
90
 
90
91
  clonedNode.id = null;
91
92
  clonedNode.pool = this.pool;
93
+ clonedNode.definition.cloneOf = this.id;
94
+
92
95
  Node.diagramPropertiesToCopy.forEach(prop => clonedNode.diagram.bounds[prop] = this.diagram.bounds[prop]);
93
96
  Object.keys(this.definition).filter(key => !Node.definitionPropertiesToNotCopy.includes(key)).forEach(key => {
94
97
  const definition = this.definition.get(key);
@@ -112,6 +115,42 @@ export default class Node {
112
115
  return clonedNode;
113
116
  }
114
117
 
118
+ cloneFlow(nodeRegistry, moddle, $t) {
119
+ const definition = nodeRegistry[this.type].definition(moddle, $t);
120
+ const diagram = nodeRegistry[this.type].diagram(moddle);
121
+ const clonedFlow = new this.constructor(this.type, definition, diagram);
122
+
123
+ clonedFlow.id = null;
124
+ clonedFlow.pool = this.pool;
125
+ clonedFlow.definition.cloneOf = this.id;
126
+ clonedFlow.diagram.waypoint = [];
127
+
128
+ this.diagram.waypoint.forEach(point => clonedFlow.diagram.waypoint.push(point));
129
+
130
+ Object.keys(this.definition).filter(key => !Node.flowDefinitionPropertiesToNotCopy.includes(key)).forEach(key => {
131
+ const definition = this.definition.get(key);
132
+ const clonedDefinition = typeof definition === 'object' ? cloneDeep(definition) : definition;
133
+ if (key === 'eventDefinitions') {
134
+ for (var i in clonedDefinition) {
135
+ if (definition[i].signalRef && !clonedDefinition[i].signalRef) {
136
+ clonedDefinition[i].signalRef = { ...definition[i].signalRef };
137
+ }
138
+ }
139
+ }
140
+ clonedFlow.definition.set(key, clonedDefinition);
141
+ clonedFlow.definition.sourceRef = clonedFlow.definition.targetRef = null;
142
+ });
143
+
144
+ Node.eventDefinitionPropertiesToNotCopy.forEach(
145
+ prop => clonedFlow.definition.eventDefinitions &&
146
+ clonedFlow.definition.eventDefinitions[0] &&
147
+ clonedFlow.definition.eventDefinitions[0].hasOwnProperty(prop) &&
148
+ clonedFlow.definition.eventDefinitions[0].set(prop, null)
149
+ );
150
+
151
+ return clonedFlow;
152
+ }
153
+
115
154
  getTargetProcess(processes, processNode) {
116
155
  return this.pool
117
156
  ? processes.find(({ id }) => id === this.pool.component.node.definition.get('processRef').id)