@processmaker/modeler 1.39.6 → 1.39.8

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.39.6",
3
+ "version": "1.39.8",
4
4
  "scripts": {
5
5
  "serve": "vue-cli-service serve",
6
6
  "test:unit": "vue-cli-service test:unit",
@@ -68,7 +68,9 @@
68
68
  "vue-monaco": "^1.2.1",
69
69
  "vue-popperjs": "^2.3.0",
70
70
  "vue-upload-component": "^2.8.20",
71
- "vuex": "^3.5.1"
71
+ "vuex": "^3.5.1",
72
+ "y-websocket": "^1.5.0",
73
+ "yjs": "^13.6.7"
72
74
  },
73
75
  "devDependencies": {
74
76
  "@babel/core": "^7.12.16",
@@ -58,6 +58,14 @@ export default class NodeIdGenerator {
58
58
  this.refreshLastIdCounter();
59
59
  return NodeIdGenerator.#counter;
60
60
  }
61
+ getDefinitionNumber() {
62
+ return NodeIdGenerator.#counter;
63
+ }
64
+
65
+ updateCounters() {
66
+ NodeIdGenerator.#counter++;
67
+ NodeIdGenerator.#diagramCounter++;
68
+ }
61
69
 
62
70
  refreshLastDiagramIdCounter() {
63
71
  let lastIdCounter = this.matchIds(new RegExp(`^${NodeIdGenerator.prefix}(\\d+)_di$`), this.definitions.diagrams);
@@ -23,6 +23,7 @@
23
23
  @close="close"
24
24
  @save-state="pushToUndoStack"
25
25
  @clearSelection="clearSelection"
26
+ :players="players"
26
27
  @action="handleToolbarAction"
27
28
  />
28
29
  <b-row class="modeler h-100">
@@ -153,6 +154,15 @@
153
154
  @save-state="pushToUndoStack"
154
155
  />
155
156
  </b-row>
157
+
158
+ <RemoteCursor
159
+ v-for="player in players"
160
+ :cursor-color="player.color"
161
+ :username="player.name"
162
+ :key="player.id"
163
+ :top="player.top"
164
+ :left="player.left"
165
+ />
156
166
  </span>
157
167
  </template>
158
168
 
@@ -213,7 +223,7 @@ import RailBottom from '@/components/railBottom/RailBottom.vue';
213
223
  import ProcessmakerModelerGenericFlow from '@/components/nodes/genericFlow/genericFlow';
214
224
 
215
225
  import Selection from './Selection';
216
-
226
+ import RemoteCursor from '@/components/multiplayer/remoteCursor/RemoteCursor.vue';
217
227
 
218
228
  export default {
219
229
  components: {
@@ -225,6 +235,7 @@ export default {
225
235
  ProcessmakerModelerGenericFlow,
226
236
  Selection,
227
237
  RailBottom,
238
+ RemoteCursor,
228
239
  },
229
240
  props: {
230
241
  owner: Object,
@@ -313,8 +324,11 @@ export default {
313
324
  isSelecting: false,
314
325
  isIntoTheSelection: false,
315
326
  dragStart: null,
327
+ players: [],
316
328
  showInspectorButton: true,
317
329
  inspectorButtonRight: 65,
330
+ multiplayer: null,
331
+ isMultiplayer: false,
318
332
  };
319
333
  },
320
334
  watch: {
@@ -907,7 +921,7 @@ export default {
907
921
 
908
922
  const config = definition.config ? JSON.parse(definition.config) : {};
909
923
  const type = config?.processKey || parser(definition, this.moddle);
910
-
924
+
911
925
  const unnamedElements = ['bpmn:TextAnnotation', 'bpmn:Association', 'bpmn:DataOutputAssociation', 'bpmn:DataInputAssociation'];
912
926
  const requireName = unnamedElements.indexOf(bpmnType) === -1;
913
927
  if (requireName && !definition.get('name')) {
@@ -1012,7 +1026,21 @@ export default {
1012
1026
  control,
1013
1027
  });
1014
1028
  },
1015
- async handleDrop({ clientX, clientY, control, nodeThatWillBeReplaced }) {
1029
+ handleDrop(data) {
1030
+ const { clientX, clientY, control} = data;
1031
+ if (this.isMultiplayer) {
1032
+ window.ProcessMaker.EventBus.$emit('multiplayer-addNode', {
1033
+ clientX,
1034
+ clientY,
1035
+ control,
1036
+ id: `node_${this.nodeIdGenerator.getDefinitionNumber()}`,
1037
+ });
1038
+ } else {
1039
+ this.handleDropProcedure(data);
1040
+ }
1041
+ },
1042
+ async handleDropProcedure(data, selected=true) {
1043
+ const { clientX, clientY, control, nodeThatWillBeReplaced, id } = data;
1016
1044
  this.validateDropTarget({ clientX, clientY, control });
1017
1045
  if (!this.allowDrop) {
1018
1046
  return;
@@ -1031,9 +1059,11 @@ export default {
1031
1059
  if (newNode.isBpmnType('bpmn:BoundaryEvent')) {
1032
1060
  this.setShapeCenterUnderCursor(diagram);
1033
1061
  }
1034
-
1035
- this.highlightNode(newNode);
1036
- await this.addNode(newNode);
1062
+ if (selected) {
1063
+ this.highlightNode(newNode);
1064
+ }
1065
+
1066
+ await this.addNode(newNode, id, selected);
1037
1067
  if (!nodeThatWillBeReplaced) {
1038
1068
  return;
1039
1069
  }
@@ -1050,6 +1080,7 @@ export default {
1050
1080
 
1051
1081
  return newNode;
1052
1082
  },
1083
+
1053
1084
  setShapeCenterUnderCursor(diagram) {
1054
1085
  diagram.bounds.x -= (diagram.bounds.width / 2);
1055
1086
  diagram.bounds.y -= (diagram.bounds.height / 2);
@@ -1061,14 +1092,14 @@ export default {
1061
1092
  const view = newNodeComponent.shapeView;
1062
1093
  await this.$refs.selector.selectElement(view);
1063
1094
  },
1064
- async addNode(node) {
1095
+ async addNode(node, id = null, selected = true) {
1065
1096
  if (!node.pool) {
1066
1097
  node.pool = this.poolTarget;
1067
1098
  }
1068
1099
 
1069
1100
  const targetProcess = node.getTargetProcess(this.processes, this.processNode);
1070
1101
  addNodeToProcess(node, targetProcess);
1071
- node.setIds(this.nodeIdGenerator);
1102
+ node.setIds(this.nodeIdGenerator, id);
1072
1103
 
1073
1104
  this.planeElements.push(node.diagram);
1074
1105
  store.commit('addNode', node);
@@ -1086,9 +1117,11 @@ export default {
1086
1117
  ].includes(node.type)) {
1087
1118
  return;
1088
1119
  }
1089
-
1090
- // Select the node after it has been added to the store (does not apply to flows)
1091
- this.selectNewNode(node);
1120
+ if (selected) {
1121
+ // Select the node after it has been added to the store (does not apply to flows)
1122
+ this.selectNewNode(node);
1123
+ }
1124
+
1092
1125
 
1093
1126
  return new Promise(resolve => {
1094
1127
  setTimeout(() => {
@@ -1159,7 +1192,7 @@ export default {
1159
1192
  this.performSingleUndoRedoTransaction(async() => {
1160
1193
  await this.paperManager.performAtomicAction(async() => {
1161
1194
  const { x: clientX, y: clientY } = this.paper.localToClientPoint(node.diagram.bounds);
1162
- const newNode = await this.handleDrop({
1195
+ const newNode = await this.handleDropProcedure({
1163
1196
  clientX, clientY,
1164
1197
  control: { type: typeToReplaceWith },
1165
1198
  nodeThatWillBeReplaced: node,
@@ -1363,6 +1396,9 @@ export default {
1363
1396
  this.dragStart = null;
1364
1397
  this.isSelecting = false;
1365
1398
  },
1399
+ enableMultiplayer() {
1400
+ this.isMultiplayer = true;
1401
+ },
1366
1402
  },
1367
1403
  created() {
1368
1404
  if (runningInCypressTest()) {
@@ -1528,6 +1564,12 @@ export default {
1528
1564
  loadXML: async(xml) => {
1529
1565
  await this.loadXML(xml);
1530
1566
  await undoRedoStore.dispatch('pushState', xml);
1567
+ if (this.isMultiplayer) {
1568
+ window.ProcessMaker.EventBus.$emit('multiplayer-start', {
1569
+ modeler: this,
1570
+ callback: this.enableMultiplayer,
1571
+ });
1572
+ }
1531
1573
  },
1532
1574
  addWarnings: warnings => this.$emit('warnings', warnings),
1533
1575
  addBreadcrumbs: breadcrumbs => this.breadcrumbData.push(breadcrumbs),
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div class="remote-cursor" :style="{ left: left + 'px', top: top + 'px' }">
3
+ <inline-svg :src="cursorIcon" :fill="cursorColor" />
4
+
5
+ <div class="remote-username">
6
+ {{ username }}
7
+ </div>
8
+ </div>
9
+ </template>
10
+
11
+ <script>
12
+ import InlineSvg from 'vue-inline-svg';
13
+
14
+ export default {
15
+ components: {
16
+ InlineSvg,
17
+ },
18
+ props: {
19
+ cursorColor: {
20
+ type: String,
21
+ default: '#000000',
22
+ },
23
+ username: {
24
+ type: String,
25
+ },
26
+ top: {
27
+ type: Number,
28
+ },
29
+ left: {
30
+ type: Number,
31
+ },
32
+ },
33
+ data() {
34
+ return {
35
+ cursorIcon: require('@/components/multiplayer/remoteCursor/cursor.svg'),
36
+ };
37
+ },
38
+ };
39
+ </script>
40
+
41
+ <style scoped lang="scss">
42
+ .remote {
43
+ &-cursor {
44
+ position: absolute;
45
+ display: flex;
46
+ width: auto;
47
+ height: 34px;
48
+ }
49
+
50
+ &-username {
51
+ display: flex;
52
+ justify-content: center;
53
+ align-items: center;
54
+ margin-top: 12px;
55
+ padding: 4px 10px;
56
+ gap: 10px;
57
+ border-radius: 4px;
58
+ background-color: #212529;
59
+
60
+ color: #FFFFFF;
61
+ font-size: 12px;
62
+ font-style: normal;
63
+ font-weight: 400;
64
+ line-height: normal;
65
+ }
66
+ }
67
+ </style>
@@ -0,0 +1,3 @@
1
+ <svg width="23" height="19" viewBox="0 0 23 19" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M11.5 17.5L2 1L20.5 7L13 9L11.5 17.5Z" stroke="#212529" />
3
+ </svg>
@@ -70,9 +70,8 @@ export default class Node {
70
70
  this.definition.id = id;
71
71
  }
72
72
 
73
- setIds(nodeIdGenerator) {
74
- const [nodeId, diagramId] = nodeIdGenerator.generate();
75
-
73
+ setIds(nodeIdGenerator, id) {
74
+ const [nodeId, diagramId] = id ? [ id + '_di'] : nodeIdGenerator.generate();
76
75
  if (!this.id) {
77
76
  this.id = nodeId;
78
77
  }
@@ -8,6 +8,7 @@
8
8
  <TopRail
9
9
  :validation-errors="validationErrors"
10
10
  :warnings="warnings"
11
+ :players="players"
11
12
  >
12
13
  <component
13
14
  :is="component.button"
@@ -129,6 +130,7 @@ export default {
129
130
  'warnings',
130
131
  'xmlManager',
131
132
  'validationBar',
133
+ 'players',
132
134
  'extraActions',
133
135
  ],
134
136
  watch: {
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <div class="top-rail-container">
3
+ <MultiplayerViewUsers :players="players"/>
3
4
  <ValidateIssue
4
5
  v-show="isOpenIssue"
5
6
  :number-of-errors="numberOfErrors"
@@ -20,12 +21,13 @@
20
21
  <script>
21
22
  import store from '@/store';
22
23
  import { ValidateButton, ValidateIssue, ValidatePanel } from '@/components/topRail/validateControl';
23
-
24
+ import MultiplayerViewUsers from '@/components/topRail/multiplayerViewUsers/MultiplayerViewUsers';
24
25
  export default {
25
26
  components: {
26
27
  ValidateButton,
27
28
  ValidateIssue,
28
29
  ValidatePanel,
30
+ MultiplayerViewUsers,
29
31
  },
30
32
  props: {
31
33
  validationErrors: {
@@ -36,6 +38,10 @@ export default {
36
38
  type: Array,
37
39
  required: true,
38
40
  },
41
+ players: {
42
+ type: Array,
43
+ required: false,
44
+ },
39
45
  },
40
46
  data() {
41
47
  return {
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <b-avatar-group class="container">
3
+ <template v-for="item in players" >
4
+ <Avatar :badgeBackgroundColor="item.color" :imgSrc="item.imgSrc" :userName="item.name" :key="item.key"/>
5
+ </template>
6
+ </b-avatar-group>
7
+ </template>
8
+
9
+ <script>
10
+ import Avatar from '@/components/topRail/multiplayerViewUsers/avatar/Avatar';
11
+ export default {
12
+ components:{
13
+ Avatar,
14
+ },
15
+ props: {
16
+ players: {
17
+ type: Array,
18
+ required: true,
19
+ },
20
+ },
21
+ };
22
+ </script>
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <span
3
+ class="b-avatar rounded-circle"
4
+ :style="{'backgroundColor': generateColorHsl(userName, saturationRange, lightnessRange)}"
5
+ >
6
+ <span v-if="imgSrc" class="b-avatar-img">
7
+ <img :src="imgSrc" :alt=userName>
8
+ </span>
9
+ <span v-else class="b-avatar-text avatar-initials">
10
+ <span>
11
+ {{ this.getInitials(userName) }}
12
+ </span>
13
+
14
+ </span>
15
+ <span class="b-avatar-badge badge-danger"
16
+ :style="{bottom: '0px', right: '0px', backgroundColor: badgeBackgroundColor}"
17
+ />
18
+ </span>
19
+ </template>
20
+
21
+ <script>
22
+
23
+ export default {
24
+ props: {
25
+ badgeBackgroundColor: {
26
+ type: String,
27
+ required: false,
28
+ },
29
+ imgSrc: {
30
+ type: String,
31
+ required: false,
32
+ },
33
+ userName: {
34
+ type: String,
35
+ required: false,
36
+ },
37
+ },
38
+ data() {
39
+ return {
40
+ saturation: 50,
41
+ lightness: 50,
42
+ range: 10,
43
+ };
44
+ },
45
+ computed: {
46
+ saturationRange() {
47
+ return this.getRange(this.saturation, this.range);
48
+ },
49
+ lightnessRange() {
50
+ return this.getRange(this.lightness, this.range);
51
+ },
52
+ },
53
+ methods: {
54
+ /**
55
+ * Get the initials from a given name.
56
+ *
57
+ * @param {string} name - The full name from which to extract initials.
58
+ * @returns {string} The initials of the first and last names.
59
+ */
60
+ getInitials(name = '') {
61
+ const nameArray = name.split(' ');
62
+ const firstNameIn = nameArray[0].charAt(0).toUpperCase();
63
+ const lastNameIn = nameArray[nameArray.length - 1].charAt(0).toUpperCase();
64
+ return `${firstNameIn}${lastNameIn}`;
65
+ },
66
+ /**
67
+ * Calculates a hash value for a given string.
68
+ *
69
+ * @param {string} str - The input string for which the hash needs to be calculated.
70
+ * @returns {number} The calculated hash value for the input string.
71
+ */
72
+ getHashOfString(str){
73
+ let hash = 0;
74
+ for (let i = 0; i < str.length; i++) {
75
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
76
+ }
77
+ hash = Math.abs(hash);
78
+ return hash;
79
+ },
80
+ /**
81
+ * Calculates a range around a given value.
82
+ *
83
+ * @param {number} value - The central value.
84
+ * @param {number} range - The range value.
85
+ * @returns {number[]} An array containing the lower and upper bounds of the range.
86
+ */
87
+ getRange(value, range) {
88
+ return [Math.max(0, value-range), Math.min(value + range, 100)];
89
+ },
90
+ /**
91
+ * Get the hash number to within our range
92
+ *
93
+ * @param {Number} hash
94
+ * @param {Number} min
95
+ * @param {Number} max
96
+ * @returns {Number}
97
+ */
98
+ normalizeHash(hash, min, max){
99
+ return Math.floor((hash % (max - min)) + min);
100
+ },
101
+ /**
102
+ *Generate Unique Color, create a string using our h,s,l values.
103
+ * @param {String} name
104
+ * @param {Array} saturationRange
105
+ * @param {Array} lightnessRange
106
+ * @returns {Number}
107
+ */
108
+ generateHSL(name, saturationRange, lightnessRange) {
109
+ const hash = this.getHashOfString(name);
110
+ const h = this.normalizeHash(hash, 0, 360);
111
+ const s = this.normalizeHash(hash, saturationRange[0], saturationRange[1]);
112
+ const l = this.normalizeHash(hash, lightnessRange[0], lightnessRange[1]);
113
+ return [h, s, l];
114
+ },
115
+ /**
116
+ * Convert HSL array to string
117
+ * @param {Array} hsl
118
+ * @returns {String}
119
+ */
120
+ HSLtoString(hsl) {
121
+ return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
122
+ },
123
+ /**
124
+ * Generate a unique hsl value.
125
+ * @param {String} name
126
+ * @param {Array} saturationRange
127
+ * @param {Array} lightnessRange
128
+ * @returns {String}
129
+ */
130
+ generateColorHsl(id, saturationRange, lightnessRange) {
131
+ return this.HSLtoString(this.generateHSL(id, saturationRange, lightnessRange));
132
+ },
133
+ },
134
+ };
135
+
136
+ </script>
137
+ <style scoped lang="scss" src="./avatar.scss"></style>
@@ -0,0 +1,6 @@
1
+ .avatar-initials {
2
+ text-align: center;
3
+ line-height: 2.4rem;
4
+ color: #ffffff;
5
+ font-weight: bold;
6
+ }
@@ -0,0 +1,48 @@
1
+ import * as Y from 'yjs';
2
+ import { WebsocketProvider } from 'y-websocket';
3
+ import { getNodeIdGenerator } from '../NodeIdGenerator';
4
+ import Room from './room';
5
+ export default class Multiplayer {
6
+ ydoc = null;
7
+ yarray = null;
8
+ modeler = null;
9
+ #nodeIdGenerator = null;
10
+ room = null;
11
+ constructor(modeler) {
12
+ // define document
13
+ this.ydoc = new Y.Doc();
14
+ this.modeler = modeler;
15
+ }
16
+ init() {
17
+ this.#nodeIdGenerator = getNodeIdGenerator(this.modeler.definitions);
18
+
19
+ this.room = new Room('room-' + window.ProcessMaker.modeler.process.id);
20
+ const wsProvider = new WebsocketProvider('ws://localhost:1234', this.room.getRoom(), this.ydoc);
21
+ wsProvider.on('status', () => {
22
+ // todo status handler
23
+ });
24
+ // array of numbers which produce a sum
25
+ this.yarray = this.ydoc.getArray('modeler');
26
+ // observe changes of the diagram
27
+ this.yarray.observe(event => {
28
+ event.changes.delta.forEach((value) =>{
29
+ if (value.insert) {
30
+ value.insert.forEach((value) => {
31
+ this.createShape(value);
32
+ this.#nodeIdGenerator.updateCounters();
33
+ });
34
+ }
35
+ });
36
+ });
37
+ window.ProcessMaker.EventBus.$on('multiplayer-addNode', ( data ) => {
38
+ this.addNode(data);
39
+ });
40
+ }
41
+ addNode(data) {
42
+ this.yarray.push([data]);
43
+
44
+ }
45
+ createShape(value) {
46
+ this.modeler.handleDropProcedure(value, false);
47
+ }
48
+ }
@@ -0,0 +1,10 @@
1
+
2
+ export default class Room {
3
+ #room ='';
4
+ constructor(name) {
5
+ this.#room = name;
6
+ }
7
+ getRoom() {
8
+ return this.#room;
9
+ }
10
+ }
@@ -51,7 +51,7 @@ window.ProcessMaker = {
51
51
  },
52
52
  modeler: {
53
53
  process: {
54
- id: 1,
54
+ id: 3,
55
55
  },
56
56
  },
57
57
  };
@@ -5,7 +5,7 @@ import './extensions/twitterConnector';
5
5
  import './extensions/testCustomConnector';
6
6
  import './extensions/customMarker';
7
7
  import registerNodes from '@/setup/registerNodes';
8
-
8
+ import Multiplayer from '../multiplayer/multiplayer';
9
9
  const blank = `
10
10
  <?xml version="1.0" encoding="UTF-8"?>
11
11
  <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_03dabax" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.3">
@@ -20,3 +20,9 @@ window.ProcessMaker.EventBus.$on('modeler-init', registerNodes);
20
20
  window.ProcessMaker.EventBus.$on('modeler-start', ({ loadXML }) => {
21
21
  loadXML(blank);
22
22
  });
23
+ window.ProcessMaker.EventBus.$on('multiplayer-start', (params) => {
24
+ const multiplayer = new Multiplayer(params.modeler);
25
+ multiplayer.init();
26
+ params.callback();
27
+ });
28
+