Repository: ProcessMaker/modeler (js)

Issue:
User Task Icon Missing
The user task should have a little person icon in the corner (like the manual task) 

![image](https://user-images.githubusercontent.com/535300/66435366-87618100-e9d9-11e9-9fd5-546c9e5d45d5.png)


Pull request:
Add user icon to task
Fixes #730.

This PR adds the user icon to the task component. The regular task was extended to add this icon to preverse the behaviour of components that extend the task.

<img width="149" alt="Screen Shot 2019-11-13 at 5 00 49 PM" src="https://user-images.githubusercontent.com/7561061/68808264-59ee9f80-0637-11ea-917d-95998178f5b1.png">
<img width="274" alt="Screen Shot 2019-11-13 at 5 00 39 PM" src="https://user-images.githubusercontent.com/7561061/68808266-59ee9f80-0637-11ea-8daa-4c0680f57533.png">


Patch:
diff --git a/src/MessageEventIdGenerator.js b/src/MessageEventIdGenerator.js
deleted file mode 100644
index c1191c4c..00000000
--- a/src/MessageEventIdGenerator.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import  NodeIdGenerator from './NodeIdGenerator';
-import store from '@/store';
-import get from 'lodash/get';
-
-export default class MessageEventIdGenerator extends NodeIdGenerator {
-  generateNewMessageEventId() {
-    let id = super.generateNewNodeId('message_event_');
-
-    while (!this.isIdUnique(id)) {
-      id = this.generateNewMessageEventId();
-    }
-
-    return id;
-  }
-
-  isIdUnique(id) {
-    const nodes = store.getters.nodes;
-
-    const eventDefinitionIdList = nodes
-      .map(node => get(node, 'definition.eventDefinitions[0].id'))
-      .filter(id => id !== undefined);
-
-    return !eventDefinitionIdList.includes(id);
-  }
-}
diff --git a/src/NodeIdGenerator.js b/src/NodeIdGenerator.js
index 9c14cafd..518b0f3c 100644
--- a/src/NodeIdGenerator.js
+++ b/src/NodeIdGenerator.js
@@ -1,33 +1,55 @@
-const nodeIdPrefix = 'node_';
-
 export default class NodeIdGenerator {
+  static prefix = 'node_';
+
+  #counter = 1;
+  #diagramCounter = 1;
+
   constructor(definitions) {
-    this.counter = 1;
     this.definitions = definitions;
   }
 
-  generateUniqueNodeId() {
-    let id = this.generateNewNodeId();
+  generate() {
+    let definitionId = this.#generateDefinitionId();
+    let diagramId = this.#generateDiagramId();
 
-    while (!this.isIdUnique(id)) {
-      id = this.generateNewNodeId();
+    while (!this.#isDefinitionIdUnique(definitionId)) {
+      definitionId = this.#generateDefinitionId();
     }
 
-    return id;
+    while (!this.#isDiagramIdUnique(diagramId)) {
+      diagramId = this.#generateDiagramId();
+    }
+
+    return [definitionId, diagramId];
   }
 
-  generateNewNodeId(prefix = nodeIdPrefix) {
-    const id = prefix + this.counter;
-    this.counter++;
+  #generateDefinitionId = () => {
+    const id = NodeIdGenerator.prefix + this.#counter;
+    this.#counter++;
 
     return id;
-  }
+  };
 
-  isIdUnique(id) {
+  #generateDiagramId = () => {
+    const id = NodeIdGenerator.prefix + this.#diagramCounter + '_di';
+    this.#diagramCounter++;
+
+    return id;
+  };
+
+  #isDefinitionIdUnique = id => {
     const planeElementIds = this.definitions.diagrams[0].plane
       .get('planeElement')
       .map(planeElement => planeElement.get('bpmnElement').id);
 
     return !planeElementIds.includes(id);
-  }
+  };
+
+  #isDiagramIdUnique = id => {
+    const diagramElementIds = this.definitions.diagrams[0].plane
+      .get('planeElement')
+      .map(planeElement => planeElement.id);
+
+    return !diagramElementIds.includes(id);
+  };
 }
diff --git a/src/assets/user-task.svg b/src/assets/user-task.svg
new file mode 100644
index 00000000..f2a86ce2
--- /dev/null
+++ b/src/assets/user-task.svg
@@ -0,0 +1,5 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user" class="svg-inline--fa fa-user fa-w-14"
+     role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
+    <path fill="#788793"
+          d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"></path>
+</svg>
diff --git a/src/components/crownConfig.vue b/src/components/crownConfig.vue
index be42dacf..8bad8a70 100644
--- a/src/components/crownConfig.vue
+++ b/src/components/crownConfig.vue
@@ -6,6 +6,7 @@
       :title="$t('Sequence Flow')"
     />
     <message-flow-button
+      v-if="isValidMessageFlowSource"
       @click="addMessageFlow"
       class="crown-config__icon"
       :title="$t('Message Flow')"
@@ -59,6 +60,13 @@ export default {
       /* allowSetNodePosition is used to prevent setting a node position outside of a pool */
       allowSetNodePosition: true,
       savePositionOnPointerupEventSet: false,
+      validMessageFlowSources: [
+        'processmaker-modeler-start-event',
+        'processmaker-modeler-end-event',
+        'processmaker-modeler-task',
+        'processmaker-modeler-pool',
+        'processmaker-modeler-intermediate-message-catch-event',
+      ],
     };
   },
   created() {
@@ -80,9 +88,13 @@ export default {
   },
   computed: {
     style() {
-      const { x, y } = this.shape.findView(this.paper).getBBox();
+      const { x, y, width: shapeWidth } = this.shape.findView(this.paper).getBBox();
       const width = (this.node.diagram.bounds.width * this.paper.scale().sx);
-      return 'top:' + (y - 45) + 'px;left:' + (x + width - 20) + 'px;cursor:pointer';
+      const widthOffsetFromLabel = shapeWidth - width > 3 ? (shapeWidth / 2) - 15 : 0;
+      return 'top:' + (y - 45) + 'px;left:' + ((x + width - 20) + widthOffsetFromLabel) + 'px;cursor:pointer';
+    },
+    isValidMessageFlowSource() {
+      return this.validMessageFlowSources.includes(this.node.type);
     },
   },
   methods: {
diff --git a/src/components/nodeManager.js b/src/components/nodeManager.js
index ad5e99ea..fc3a871a 100644
--- a/src/components/nodeManager.js
+++ b/src/components/nodeManager.js
@@ -2,12 +2,14 @@ import { id as poolId } from './nodes/pool';
 import { id as laneId } from '@/components/nodes/poolLane';
 
 export function addIdToNodeAndSetUpDiagramReference(node, nodeIdGenerator) {
-  const id = node.definition.id || nodeIdGenerator.generateUniqueNodeId();
+  const [nodeId, diagramId] = nodeIdGenerator.generate();
 
-  node.definition.id = id;
+  if (!node.definition.id) {
+    node.definition.id = nodeId;
+  }
 
   if (node.diagram) {
-    node.diagram.id = `${id}_di`;
+    node.diagram.id = diagramId;
     node.diagram.bpmnElement = node.definition;
   }
 }
diff --git a/src/components/nodes/boundaryErrorEvent/boundaryErrorEvent.vue b/src/components/nodes/boundaryErrorEvent/boundaryErrorEvent.vue
index c5da93a8..6e6fbef8 100644
--- a/src/components/nodes/boundaryErrorEvent/boundaryErrorEvent.vue
+++ b/src/components/nodes/boundaryErrorEvent/boundaryErrorEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div />
-</template>
-
 <script>
 import BoundaryEvent from '@/components/nodes/boundaryEvent/boundaryEvent';
 import errorBoltIcon from '@/assets/boundary-error-event-icon.svg';
diff --git a/src/components/nodes/boundaryEvent/boundaryEvent.vue b/src/components/nodes/boundaryEvent/boundaryEvent.vue
index 24e13809..87f2b559 100644
--- a/src/components/nodes/boundaryEvent/boundaryEvent.vue
+++ b/src/components/nodes/boundaryEvent/boundaryEvent.vue
@@ -1,9 +1,23 @@
 <template>
-  <div />
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
-import crownConfig from '@/mixins/crownConfig';
 import portsConfig from '@/mixins/portsConfig';
 import connectIcon from '@/assets/connect-elements.svg';
 import EventShape from '@/components/nodes/boundaryEvent/shape';
@@ -12,10 +26,27 @@ import resetShapeColor from '@/components/resetShapeColor';
 import { getBoundaryAnchorPoint } from '@/portsUtils';
 import { invalidNodeColor } from '@/components/nodeColors';
 import hideLabelOnDrag from '@/mixins/hideLabelOnDrag';
+import CrownConfig from '@/components/crownConfig';
+import highlightConfig from '@/mixins/highlightConfig';
 
 export default {
-  props: ['graph', 'node', 'paper', 'highlighted'],
-  mixins: [crownConfig, portsConfig, hideLabelOnDrag],
+  components: {
+    CrownConfig,
+  },
+  props: [
+    'graph',
+    'node',
+    'id',
+    'highlighted',
+    'nodeRegistry',
+    'moddle',
+    'paper',
+    'collaboration',
+    'processNode',
+    'planeElements',
+    'isRendering',
+  ],
+  mixins: [highlightConfig, portsConfig, hideLabelOnDrag],
   data() {
     return {
       shape: null,
@@ -104,7 +135,6 @@ export default {
       const { x, y } = getBoundaryAnchorPoint(this.getCenterPosition(), task);
       const { width, height } = this.shape.size();
       this.shape.position(x - (width / 2), y - (height / 2));
-      this.updateCrownPosition();
 
       this.previousPosition = this.shape.position();
     },
@@ -131,7 +161,6 @@ export default {
 
       if (!task) {
         this.resetToInitialPosition();
-        this.updateCrownPosition();
         return;
       }
 
diff --git a/src/components/nodes/callActivity/callActivity.vue b/src/components/nodes/callActivity/callActivity.vue
index 33c71e3b..e0c03f32 100644
--- a/src/components/nodes/callActivity/callActivity.vue
+++ b/src/components/nodes/callActivity/callActivity.vue
@@ -1,11 +1,25 @@
 <template>
-  <div />
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
 import { util } from 'jointjs';
 import connectIcon from '@/assets/connect-elements.svg';
-import crownConfig from '@/mixins/crownConfig';
 import portsConfig from '@/mixins/portsConfig';
 import TaskShape from '@/components/nodes/task/shape';
 import { taskHeight } from '@/components/nodes/task/taskConfig';
@@ -14,13 +28,30 @@ import uniqBy from 'lodash/uniqBy';
 import hasMarkers, { markerSize } from '@/mixins/hasMarkers';
 import hideLabelOnDrag from '@/mixins/hideLabelOnDrag';
 import { elementIdParser } from '@/components/nodes/callActivity/elementIdParser';
+import CrownConfig from '@/components/crownConfig';
+import highlightConfig from '@/mixins/highlightConfig';
 
 const labelPadding = 15;
 const topAndBottomMarkersSpace = 2 * markerSize;
 
 export default {
-  props: ['graph', 'node', 'id'],
-  mixins: [crownConfig, portsConfig, hasMarkers, hideLabelOnDrag],
+  components: {
+    CrownConfig,
+  },
+  props: [
+    'graph',
+    'node',
+    'id',
+    'highlighted',
+    'nodeRegistry',
+    'moddle',
+    'paper',
+    'collaboration',
+    'processNode',
+    'planeElements',
+    'isRendering',
+  ],
+  mixins: [highlightConfig, portsConfig, hasMarkers, hideLabelOnDrag],
   data() {
     return {
       crownConfig: [
@@ -78,7 +109,6 @@ export default {
   },
   mounted() {
     this.shape = new TaskShape();
-
     let bounds = this.node.diagram.bounds;
     this.shape.position(bounds.x, bounds.y);
     this.shape.resize(bounds.width, bounds.height);
diff --git a/src/components/nodes/eventBasedGateway/eventBasedGateway.vue b/src/components/nodes/eventBasedGateway/eventBasedGateway.vue
index 843c8d38..faab4c61 100644
--- a/src/components/nodes/eventBasedGateway/eventBasedGateway.vue
+++ b/src/components/nodes/eventBasedGateway/eventBasedGateway.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import GatewayComponent from '@/components/nodes/gateway/gateway';
 import eventBasedSymbol from '@/assets/event-based-gateway-symbol.svg';
diff --git a/src/components/nodes/exclusiveGateway/exclusiveGateway.vue b/src/components/nodes/exclusiveGateway/exclusiveGateway.vue
index 864f3a0f..55bf52d9 100644
--- a/src/components/nodes/exclusiveGateway/exclusiveGateway.vue
+++ b/src/components/nodes/exclusiveGateway/exclusiveGateway.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import GatewayComponent from '@/components/nodes/gateway/gateway';
 import exclusiveGatewaySymbol from '@/assets/exclusive-gateway-symbol.svg';
diff --git a/src/components/nodes/gateway/gateway.vue b/src/components/nodes/gateway/gateway.vue
index b0746aa3..678261d9 100644
--- a/src/components/nodes/gateway/gateway.vue
+++ b/src/components/nodes/gateway/gateway.vue
@@ -1,17 +1,48 @@
 <template>
-  <div/>
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
-import crownConfig from '@/mixins/crownConfig';
 import portsConfig from '@/mixins/portsConfig';
 import connectIcon from '@/assets/connect-elements.svg';
 import GatewayShape from '@/components/nodes/gateway/shape';
 import hideLabelOnDrag from '@/mixins/hideLabelOnDrag';
+import CrownConfig from '@/components/crownConfig';
+import highlightConfig from '@/mixins/highlightConfig';
 
 export default {
-  props: ['graph', 'node'],
-  mixins: [crownConfig, portsConfig, hideLabelOnDrag],
+  components: {
+    CrownConfig,
+  },
+  props: [
+    'graph',
+    'node',
+    'id',
+    'highlighted',
+    'nodeRegistry',
+    'moddle',
+    'paper',
+    'collaboration',
+    'processNode',
+    'planeElements',
+    'isRendering',
+  ],
+  mixins: [highlightConfig, portsConfig, hideLabelOnDrag],
   data() {
     return {
       shape: null,
diff --git a/src/components/nodes/inclusiveGateway/inclusiveGateway.vue b/src/components/nodes/inclusiveGateway/inclusiveGateway.vue
index 2bd4e73a..817c20fc 100644
--- a/src/components/nodes/inclusiveGateway/inclusiveGateway.vue
+++ b/src/components/nodes/inclusiveGateway/inclusiveGateway.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import GatewayComponent from '@/components/nodes/gateway/gateway';
 import inclusiveGatewaySymbol from '@/assets/inclusive-gatway-symbol.svg';
diff --git a/src/components/nodes/index.js b/src/components/nodes/index.js
index 384e7d9b..07e5bb3f 100644
--- a/src/components/nodes/index.js
+++ b/src/components/nodes/index.js
@@ -14,7 +14,7 @@ export { default as startTimerEvent } from './startTimerEvent';
 export { default as intermediateTimerEvent } from './intermediateTimerEvent';
 export { default as intermediateMessageCatchEvent } from './intermediateMessageCatchEvent';
 export { default as intermediateMessageThrowEvent } from './intermediateMessageThrowEvent';
-export { default as task } from './task';
+export { default as task } from './userTask';
 export { default as callActivity } from './callActivity';
 export { default as scriptTask } from './scriptTask';
 export { default as serviceTask } from './serviceTask';
diff --git a/src/components/nodes/intermediateEvent/intermediateEvent.vue b/src/components/nodes/intermediateEvent/intermediateEvent.vue
index 094e371b..06666aec 100644
--- a/src/components/nodes/intermediateEvent/intermediateEvent.vue
+++ b/src/components/nodes/intermediateEvent/intermediateEvent.vue
@@ -1,18 +1,49 @@
 <template>
-  <div/>
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
-import crownConfig from '@/mixins/crownConfig';
 import portsConfig from '@/mixins/portsConfig';
 import connectIcon from '@/assets/connect-elements.svg';
 import EventShape from '@/components/nodes/intermediateEvent/shape';
 import hasMarkers from '@/mixins/hasMarkers';
 import hideLabelOnDrag from '@/mixins/hideLabelOnDrag';
+import CrownConfig from '@/components/crownConfig';
+import highlightConfig from '@/mixins/highlightConfig';
 
 export default {
-  props: ['graph', 'node', 'id'],
-  mixins: [crownConfig, portsConfig, hasMarkers, hideLabelOnDrag],
+  components: {
+    CrownConfig,
+  },
+  props: [
+    'graph',
+    'node',
+    'id',
+    'highlighted',
+    'nodeRegistry',
+    'moddle',
+    'paper',
+    'collaboration',
+    'processNode',
+    'planeElements',
+    'isRendering',
+  ],
+  mixins: [highlightConfig, portsConfig, hasMarkers, hideLabelOnDrag],
   data() {
     return {
       shape: null,
diff --git a/src/components/nodes/intermediateMessageCatchEvent/intermediateMessageCatchEvent.vue b/src/components/nodes/intermediateMessageCatchEvent/intermediateMessageCatchEvent.vue
index 4e28f3bd..67ae3a66 100644
--- a/src/components/nodes/intermediateMessageCatchEvent/intermediateMessageCatchEvent.vue
+++ b/src/components/nodes/intermediateMessageCatchEvent/intermediateMessageCatchEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import IntermediateEvent from '@/components/nodes/intermediateEvent/intermediateEvent';
 import intermediateMailIcon from '@/mixins/intermediateMailIcon';
diff --git a/src/components/nodes/intermediateMessageThrowEvent/intermediateMessageThrowEvent.vue b/src/components/nodes/intermediateMessageThrowEvent/intermediateMessageThrowEvent.vue
index 53afaec1..3ec63acc 100644
--- a/src/components/nodes/intermediateMessageThrowEvent/intermediateMessageThrowEvent.vue
+++ b/src/components/nodes/intermediateMessageThrowEvent/intermediateMessageThrowEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import IntermediateEvent from '@/components/nodes/intermediateEvent/intermediateEvent';
 import intermediateMailSymbol from '@/assets/intermediate-mail-alt.svg';
diff --git a/src/components/nodes/intermediateTimerEvent/intermediateTimerEvent.vue b/src/components/nodes/intermediateTimerEvent/intermediateTimerEvent.vue
index 9fc5497f..fad3a60a 100644
--- a/src/components/nodes/intermediateTimerEvent/intermediateTimerEvent.vue
+++ b/src/components/nodes/intermediateTimerEvent/intermediateTimerEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import IntermediateEvent from '@/components/nodes/intermediateEvent/intermediateEvent';
 import clockIcon from '@/assets/intermediate-clock.svg';
diff --git a/src/components/nodes/messageStartEvent/messageStartEvent.vue b/src/components/nodes/messageStartEvent/messageStartEvent.vue
index 1af398a4..fa5b4830 100644
--- a/src/components/nodes/messageStartEvent/messageStartEvent.vue
+++ b/src/components/nodes/messageStartEvent/messageStartEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import startEvent from '../startEvent/startEvent';
 import startMailSymbol from '@/assets/start-event-mail.svg';
diff --git a/src/components/nodes/parallelGateway/parallelGateway.vue b/src/components/nodes/parallelGateway/parallelGateway.vue
index 990e98dc..80423c48 100644
--- a/src/components/nodes/parallelGateway/parallelGateway.vue
+++ b/src/components/nodes/parallelGateway/parallelGateway.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import GatewayComponent from '@/components/nodes/gateway/gateway';
 import parallelGatewaySymbol from '@/assets/parallel-gateway-symbol.svg';
diff --git a/src/components/nodes/startEvent/eventShape.js b/src/components/nodes/startEvent/eventShape.js
index e4e6d2de..5f79ac2f 100644
--- a/src/components/nodes/startEvent/eventShape.js
+++ b/src/components/nodes/startEvent/eventShape.js
@@ -22,7 +22,6 @@ export default shapes.standard.Circle.extend({
 
   defaults: util.deepSupplement({
 
-    type: 'processmaker.components.nodes.startEvent.Shape',
     size: { width: 36, height: 36 },
     attrs: {
       'image': { 'ref-x': 5, 'ref-y': 5, ref: 'circle', width: 26, height: 26 },
diff --git a/src/components/nodes/startEvent/startEvent.vue b/src/components/nodes/startEvent/startEvent.vue
index f2ca9cd8..5d730eb7 100644
--- a/src/components/nodes/startEvent/startEvent.vue
+++ b/src/components/nodes/startEvent/startEvent.vue
@@ -1,19 +1,50 @@
 <template>
-  <div/>
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
-import crownConfig from '@/mixins/crownConfig';
 import portsConfig from '@/mixins/portsConfig';
 import connectIcon from '@/assets/connect-elements.svg';
 import EventShape from './eventShape';
 import hasMarkers from '@/mixins/hasMarkers';
 import hideLabelOnDrag from '@/mixins/hideLabelOnDrag';
 import { startColor } from '@/components/nodeColors';
+import CrownConfig from '@/components/crownConfig';
+import highlightConfig from '@/mixins/highlightConfig';
 
 export default {
-  props: ['graph', 'node', 'id'],
-  mixins: [crownConfig, portsConfig, hasMarkers, hideLabelOnDrag],
+  components: {
+    CrownConfig,
+  },
+  props: [
+    'graph',
+    'node',
+    'id',
+    'highlighted',
+    'nodeRegistry',
+    'moddle',
+    'paper',
+    'collaboration',
+    'processNode',
+    'planeElements',
+    'isRendering',
+  ],
+  mixins: [highlightConfig, portsConfig, hasMarkers, hideLabelOnDrag],
   data() {
     return {
       shape: null,
@@ -35,6 +66,7 @@ export default {
   },
   mounted() {
     this.shape = new EventShape();
+    this.shape.set('type', 'processmaker.components.nodes.startEvent.Shape');
     const bounds = this.node.diagram.bounds;
     this.shape.position(bounds.get('x'), bounds.get('y'));
     this.shape.resize(bounds.get('width'), bounds.get('height'));
diff --git a/src/components/nodes/startTimerEvent/startTimerEvent.vue b/src/components/nodes/startTimerEvent/startTimerEvent.vue
index 1d15bd92..3ac36050 100644
--- a/src/components/nodes/startTimerEvent/startTimerEvent.vue
+++ b/src/components/nodes/startTimerEvent/startTimerEvent.vue
@@ -1,7 +1,3 @@
-<template>
-  <div/>
-</template>
-
 <script>
 import StartEvent from '@/components/nodes/startEvent/startEvent';
 import clockIcon from '@/assets/clock.svg';
diff --git a/src/components/nodes/task/task.vue b/src/components/nodes/task/task.vue
index 2b4e52c6..8e2adaba 100644
--- a/src/components/nodes/task/task.vue
+++ b/src/components/nodes/task/task.vue
@@ -1,22 +1,20 @@
 <template>
-  <div>
-    <crown-config
-      :highlighted="highlighted"
-      :paper="paper"
-      :graph="graph"
-      :shape="shape"
-      :node="node"
-      :nodeRegistry="nodeRegistry"
-      :moddle="moddle"
-      :collaboration="collaboration"
-      :process-node="processNode"
-      :plane-elements="planeElements"
-      :is-rendering="isRendering"
-      @remove-node="$emit('remove-node', $event)"
-      @add-node="$emit('add-node', $event)"
-      @save-state="$emit('save-state', $event)"
-    />
-  </div>
+  <crown-config
+    :highlighted="highlighted"
+    :paper="paper"
+    :graph="graph"
+    :shape="shape"
+    :node="node"
+    :nodeRegistry="nodeRegistry"
+    :moddle="moddle"
+    :collaboration="collaboration"
+    :process-node="processNode"
+    :plane-elements="planeElements"
+    :is-rendering="isRendering"
+    @remove-node="$emit('remove-node', $event)"
+    @add-node="$emit('add-node', $event)"
+    @save-state="$emit('save-state', $event)"
+  />
 </template>
 
 <script>
@@ -98,7 +96,6 @@ export default {
   },
   mounted() {
     this.shape = new TaskShape();
-    this.shape.set('type', 'newCrown');
     let bounds = this.node.diagram.bounds;
     this.shape.position(bounds.x, bounds.y);
     this.shape.resize(bounds.width, bounds.height);
diff --git a/src/components/nodes/userTask/index.js b/src/components/nodes/userTask/index.js
new file mode 100644
index 00000000..ba8103f9
--- /dev/null
+++ b/src/components/nodes/userTask/index.js
@@ -0,0 +1,7 @@
+import component from './userTask';
+import taskConfig from '../task';
+
+export default {
+  ...taskConfig,
+  component,
+};
diff --git a/src/components/nodes/userTask/userTask.vue b/src/components/nodes/userTask/userTask.vue
new file mode 100644
index 00000000..033c5df2
--- /dev/null
+++ b/src/components/nodes/userTask/userTask.vue
@@ -0,0 +1,11 @@
+<script>
+import Task from '../task/task';
+import userIcon from '@/assets/user-task.svg';
+
+export default {
+  extends: Task,
+  mounted() {
+    this.shape.attr('image/xlink:href', userIcon);
+  },
+};
+</script>


Test patch:
diff --git a/tests/e2e/fixtures/setUpForDuplicateDiagramId.xml b/tests/e2e/fixtures/setUpForDuplicateDiagramId.xml
new file mode 100644
index 00000000..152f3ace
--- /dev/null
+++ b/tests/e2e/fixtures/setUpForDuplicateDiagramId.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_03dabax" targetNamespace="http://bpmn.io/schema/bpmn" exporter="ProcessMaker Modeler" exporterVersion="1.0">
+  <bpmn:process id="Process_1" isExecutable="true">
+    <bpmn:task id="node_2" name="Task" />
+  </bpmn:process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
+    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
+      <bpmndi:BPMNShape id="node_1_di" bpmnElement="node_2">
+        <dc:Bounds x="310" y="170" width="116" height="76" />
+      </bpmndi:BPMNShape>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</bpmn:definitions>
diff --git a/tests/e2e/specs/BoundaryEventCommonBehaviour.spec.js b/tests/e2e/specs/BoundaryEventCommonBehaviour.spec.js
index 7933807d..e64d0b90 100644
--- a/tests/e2e/specs/BoundaryEventCommonBehaviour.spec.js
+++ b/tests/e2e/specs/BoundaryEventCommonBehaviour.spec.js
@@ -21,32 +21,24 @@ const boundaryEventData = [{
   eventXMLSnippet: '<bpmn:boundaryEvent id="node_3" name="New Boundary Timer Event" attachedToRef="node_2"><bpmn:timerEventDefinition><bpmn:timeDuration>PT1H</bpmn:timeDuration></bpmn:timerEventDefinition></bpmn:boundaryEvent>',
   taskType: nodeTypes.task,
   invalidTargets: [{ type: nodeTypes.startEvent }],
-  taskSelector: '.main-paper ' +
-    '[data-type="newCrown"]',
 }, {
   type: 'Boundary Error Event',
   nodeType: nodeTypes.boundaryErrorEvent,
   eventXMLSnippet: '<bpmn:boundaryEvent id="node_3" name="New Boundary Error Event" attachedToRef="node_2"><bpmn:errorEventDefinition /></bpmn:boundaryEvent>',
   taskType: nodeTypes.task,
   invalidTargets: [{ type: nodeTypes.startEvent }],
-  taskSelector: '.main-paper ' +
-    '[data-type="newCrown"]',
 }, {
   type: 'Boundary Escalation Event',
   nodeType: nodeTypes.boundaryEscalationEvent,
   eventXMLSnippet: '<bpmn:boundaryEvent id="node_3" name="New Boundary Escalation Event" attachedToRef="node_2"><bpmn:escalationEventDefinition /></bpmn:boundaryEvent>',
   taskType: nodeTypes.callActivity,
   invalidTargets: [{ type: nodeTypes.startEvent }, { type: nodeTypes.task, color: defaultNodeColor }],
-  taskSelector: '.main-paper ' +
-    '[data-type="processmaker.components.nodes.task.Shape"]',
 }, {
   type: 'Boundary Message Event',
   nodeType: nodeTypes.boundaryMessageEvent,
   eventXMLSnippet: '<bpmn:boundaryEvent id="node_3" name="New Boundary Message Event" attachedToRef="node_2"><bpmn:messageEventDefinition /></bpmn:boundaryEvent>',
   taskType: nodeTypes.callActivity,
   invalidTargets: [{ type: nodeTypes.startEvent }],
-  taskSelector: '.main-paper ' +
-    '[data-type="processmaker.components.nodes.task.Shape"]',
 }];
 
 function testThatBoundaryEventIsCloseToTask(boundaryEvent, task) {
@@ -69,7 +61,7 @@ function configurePool(poolPosition, nodeType, taskType) {
   dragFromSourceToDest(nodeTypes.pool, poolPosition);
 }
 
-boundaryEventData.forEach(({ type, nodeType, eventXMLSnippet, taskType, invalidTargets, taskSelector }) => {
+boundaryEventData.forEach(({ type, nodeType, eventXMLSnippet, taskType, invalidTargets }) => {
   describe(`Common behaviour test for boundary event type ${type}`, () => {
     it('can render a boundary event of this type', function() {
       dragFromSourceToDest(taskType, taskPosition);
@@ -109,6 +101,8 @@ boundaryEventData.forEach(({ type, nodeType, eventXMLSnippet, taskType, invalidT
 
     it('can stay anchored to task when moving pool', function() {
       configurePool({ x: 300, y: 300 }, nodeType, taskType);
+      const taskSelector = '.main-paper ' +
+        '[data-type="processmaker.components.nodes.boundaryEvent.Shape"]';
 
       cy.get(boundaryEventSelector).then($boundaryEvent => {
         cy.get(taskSelector).then($task => {
diff --git a/tests/e2e/specs/BoundaryTimerEvent.spec.js b/tests/e2e/specs/BoundaryTimerEvent.spec.js
index 51ff9b86..07d43a3f 100644
--- a/tests/e2e/specs/BoundaryTimerEvent.spec.js
+++ b/tests/e2e/specs/BoundaryTimerEvent.spec.js
@@ -156,7 +156,7 @@ describe('Boundary Timer Event', () => {
     dragFromSourceToDest(nodeTypes.task, task2Position);
 
     const boundaryTimerEventSelector = '.main-paper ' +
-      '[data-type="newCrown"] ~ ' +
+      '[data-type="processmaker.components.nodes.task.Shape"] ~ ' +
       '[data-type="processmaker.components.nodes.boundaryEvent.Shape"]';
 
     getPositionInPaperCoords(task2Position).then(newPosition => {
@@ -205,7 +205,7 @@ describe('Boundary Timer Event', () => {
     dragFromSourceToDest(nodeTypes.task, task2Position);
 
     const boundaryTimerEventSelector = '.main-paper ' +
-      '[data-type="newCrown"] ~ ' +
+      '[data-type="processmaker.components.nodes.task.Shape"] ~ ' +
       '[data-type="processmaker.components.nodes.boundaryEvent.Shape"]';
 
     getPositionInPaperCoords(task2Position).then(newPosition => {
diff --git a/tests/e2e/specs/Modeler.spec.js b/tests/e2e/specs/Modeler.spec.js
index dba8ad19..0338b8bb 100644
--- a/tests/e2e/specs/Modeler.spec.js
+++ b/tests/e2e/specs/Modeler.spec.js
@@ -5,6 +5,7 @@ import {
   getElementAtPosition,
   getGraphElements,
   getLinksConnectedToElement,
+  getXml,
   isElementCovered,
   removeIndentationAndLinebreaks,
   typeIntoTextInput,
@@ -444,4 +445,13 @@ describe('Modeler', () => {
       getType().
       should('equal', nodeTypes.task);
   });
+
+  it('should not generate duplicate diagram IDs', function() {
+    uploadXml('setUpForDuplicateDiagramId.xml');
+    dragFromSourceToDest(nodeTypes.task, { x: 300, y: 300 });
+    getXml().then(xml => {
+      expect(xml.match(/node_1_di/g)).to.have.length(1);
+      expect(xml.match(/node_2_di/g)).to.have.length(1);
+    });
+  });
 });
diff --git a/tests/e2e/support/utils.js b/tests/e2e/support/utils.js
index 5030921c..88a83029 100644
--- a/tests/e2e/support/utils.js
+++ b/tests/e2e/support/utils.js
@@ -92,7 +92,7 @@ export function getPositionInPaperCoords(position) {
 }
 
 export function getCrownButtonForElement($element, crownButton) {
-  if ($element.data('type') === 'newCrown') {
+  if (hasNewCrownConfig($element)) {
     return cy.get(`#${ crownButton }`);
   }
   return cy
@@ -101,6 +101,17 @@ export function getCrownButtonForElement($element, crownButton) {
     .then(crownButtons => crownButtons[0]);
 }
 
+function hasNewCrownConfig($element) {
+  const newCrown = [
+    'processmaker.components.nodes.task.Shape',
+    'processmaker.components.nodes.boundaryEvent.Shape',
+    'processmaker.components.nodes.intermediateEvent.Shape',
+    'processmaker.components.nodes.startEvent.Shape',
+    'processmaker.components.nodes.gateway.Shape',
+  ];
+  return newCrown.includes($element.data('type'));
+}
+
 export function typeIntoTextInput(selector, value) {
   cy.get(selector).clear().type(value);
   waitToRenderNodeUpdates();
@@ -248,22 +259,21 @@ export function getNumberOfLinks() {
     .then(({ graph }) => graph.getLinks().length);
 }
 
-export function assertDownloadedXmlContainsExpected(xmlString) {
+export function getXml() {
   cy.get('[data-test=downloadXMLBtn]').click();
-  cy.window()
+  return cy.window()
     .its('xml')
-    .then(removeIndentationAndLinebreaks)
-    .then(xml => {
-      expect(xml).to.contain(removeIndentationAndLinebreaks(xmlString));
-    });
+    .then(removeIndentationAndLinebreaks);
+}
+
+export function assertDownloadedXmlContainsExpected(xmlString) {
+  getXml().then(xml => {
+    expect(xml).to.contain(removeIndentationAndLinebreaks(xmlString));
+  });
 }
 
 export function assertDownloadedXmlDoesNotContainExpected(xmlString) {
-  cy.get('[data-test=downloadXMLBtn]').click();
-  cy.window()
-    .its('xml')
-    .then(removeIndentationAndLinebreaks)
-    .then(xml => {
-      expect(xml).to.not.contain(removeIndentationAndLinebreaks(xmlString));
-    });
+  getXml().then(xml => {
+    expect(xml).to.not.contain(removeIndentationAndLinebreaks(xmlString));
+  });
 }
diff --git a/tests/unit/nodeIdGenerator.spec.js b/tests/unit/nodeIdGenerator.spec.js
new file mode 100644
index 00000000..966263e4
--- /dev/null
+++ b/tests/unit/nodeIdGenerator.spec.js
@@ -0,0 +1,81 @@
+import NodeIdGenerator from '@/NodeIdGenerator';
+import zip from 'lodash/zip';
+
+function definitionsFactory(definitionIds = [], diagramIds = []) {
+  const planeElement = zip(definitionIds, diagramIds).map(([definitionId, diagramId]) => ({
+    id: diagramId,
+    bpmnElement: { id: definitionId },
+    get(key) {
+      return this[key];
+    },
+  }));
+
+  return {
+    diagrams: [{
+      plane: {
+        planeElement,
+        get(key) {
+          return this[key];
+        },
+      },
+    }],
+  };
+}
+
+describe('NodeIdGenerator', () => {
+  it.each([['definition', 0], ['diagram', 1]])('should generate unique %s IDs', (name, idIndex) => {
+    const generator = new NodeIdGenerator(definitionsFactory());
+    const ids = [];
+    const numberOfIdsToGenerate = 10;
+
+    for (let i = 0; i < numberOfIdsToGenerate; i++) {
+      ids.push(generator.generate()[idIndex]);
+    }
+
+    while (ids.length > 0) {
+      expect(ids).not.toContain(ids.pop());
+    }
+  });
+
+  it('should not generate definition IDs that already exist in definitions', () => {
+    const duplicateGenerator = new NodeIdGenerator(definitionsFactory());
+    const definitionIds = [
+      duplicateGenerator.generate()[0],
+      duplicateGenerator.generate()[0],
+      duplicateGenerator.generate()[0],
+    ];
+    const generator = new NodeIdGenerator(definitionsFactory(definitionIds));
+
+    const newDefinitionIds = [
+      generator.generate()[0],
+      generator.generate()[0],
+      generator.generate()[0],
+      generator.generate()[0],
+    ];
+
+    while (newDefinitionIds.length > 0) {
+      expect(definitionIds).not.toContain(newDefinitionIds.pop());
+    }
+  });
+
+  it('should not generate diagram IDs that already exist in definitions', () => {
+    const duplicateGenerator = new NodeIdGenerator(definitionsFactory());
+    const diagramIds = [
+      duplicateGenerator.generate()[1],
+      duplicateGenerator.generate()[1],
+      duplicateGenerator.generate()[1],
+    ];
+    const generator = new NodeIdGenerator(definitionsFactory([], diagramIds));
+
+    const newDefinitionIds = [
+      generator.generate()[1],
+      generator.generate()[1],
+      generator.generate()[1],
+      generator.generate()[1],
+    ];
+
+    while (newDefinitionIds.length > 0) {
+      expect(diagramIds).not.toContain(newDefinitionIds.pop());
+    }
+  });
+});
diff --git a/tests/unit/nodeManager.spec.js b/tests/unit/nodeManager.spec.js
index 66f1ba84..ab9d275e 100644
--- a/tests/unit/nodeManager.spec.js
+++ b/tests/unit/nodeManager.spec.js
@@ -4,17 +4,22 @@ import { id as laneId } from '@/components/nodes/poolLane';
 
 describe('nodeManager', () => {
   describe('addIdToNodeAndSetUpDiagramReference', () => {
-    it('should not change diagram ID if it already has an ID set', () => {
+    it('should not change node ID if it already has an ID set', () => {
       const nodeId = 'foobar';
+      const newNodeId = 'baz';
+      const diagramId = 'diagram_id';
       const node = {
         definition: { id: nodeId },
         diagram: {},
       };
+      const nodeIdGenerator = {
+        generate: () => ([newNodeId, diagramId]),
+      };
 
-      addIdToNodeAndSetUpDiagramReference(node);
+      addIdToNodeAndSetUpDiagramReference(node, nodeIdGenerator);
 
       expect(node.definition.id).toBe(nodeId);
-      expect(node.diagram.id).toBe(`${nodeId}_di`);
+      expect(node.diagram.id).toBe(diagramId);
       expect(node.diagram.bpmnElement).toBe(node.definition);
     });
 
@@ -22,13 +27,17 @@ describe('nodeManager', () => {
       const node = {
         definition: { id: 123 },
       };
+      const nodeIdGenerator = {
+        generate: () => ([]),
+      };
 
-      expect(() => addIdToNodeAndSetUpDiagramReference(node)).not.toThrow();
+      expect(() => addIdToNodeAndSetUpDiagramReference(node, nodeIdGenerator)).not.toThrow();
     });
 
     it('should use ID generator if definition does not have ID', () => {
       const nodeId = 'foobar';
-      const nodeIdGenerator = { generateUniqueNodeId: () => nodeId };
+      const diagramId = 'baz';
+      const nodeIdGenerator = { generate: () => [nodeId, diagramId] };
       const node = {
         definition: {},
         diagram: {},
@@ -37,7 +46,7 @@ describe('nodeManager', () => {
       addIdToNodeAndSetUpDiagramReference(node, nodeIdGenerator);
 
       expect(node.definition.id).toBe(nodeId);
-      expect(node.diagram.id).toBe(`${nodeId}_di`);
+      expect(node.diagram.id).toBe(diagramId);
     });
   });