Repository: ProcessMaker/modeler
Pull request #1553 (2023-05-17 19:20:04)

FOUR-8402 Pin / Unpin node types (Front end)
## Issue & Reproduction Steps

Expected behavior: 
User should be able to pin the node types from the explorer rail

## Solution 
- Added a vuex state to keep track of the node types pinned by the user
- Fixed some bugs related to the search bar
- Styling

## How to Test
Test the steps above

## Related Tickets & Packages
- https://processmaker.atlassian.net/browse/FOUR-8402

## Code Review Checklist
- [ ] I have pulled this code locally and tested it on my instance, along with any associated packages.
- [ ] This code adheres to [ProcessMaker Coding Guidelines](https://github.com/ProcessMaker/processmaker/wiki/Coding-Guidelines).
- [ ] This code includes a unit test or an E2E test that tests its functionality, or is covered by an existing test.
- [ ] This solution fixes the bug reported in the original ticket.
- [ ] This solution does not alter the expected output of a component in a way that would break existing Processes.
- [ ] This solution does not implement any breaking changes that would invalidate documentation or cause existing Processes to fail.
- [ ] This solution has been tested with enterprise packages that rely on its functionality and does not introduce bugs in those packages.
- [ ] This code does not duplicate functionality that already exists in the framework or in ProcessMaker.
- [ ] This ticket conforms to the PRD associated with this part of ProcessMaker.

ci:deploy



Patch:
diff --git a/src/assets/pin-angle-fill.svg b/src/assets/pin-angle-fill.svg
new file mode 100644
index 00000000..b10a2091
--- /dev/null
+++ b/src/assets/pin-angle-fill.svg
@@ -0,0 +1,3 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.3284 0.721802C10.461 0.721802 10.5881 0.77448 10.6819 0.868248L15.6317 5.818C15.8269 6.01326 15.8269 6.32984 15.6317 6.5251C15.1513 7.0055 14.5597 7.11349 14.1291 7.11349C13.9517 7.11349 13.7936 7.09548 13.6683 7.07423L10.5348 10.2078C10.5417 10.2357 10.5488 10.2652 10.5559 10.2962C10.6098 10.5298 10.6703 10.8554 10.6947 11.2206C10.7415 11.9231 10.663 12.908 9.97481 13.5962C9.77955 13.7914 9.46296 13.7914 9.2677 13.5962L6.43928 10.7677L3.2573 13.9497C3.06203 14.145 2.03835 14.8521 1.84308 14.6568C1.64782 14.4616 2.35493 13.4379 2.55019 13.2426L5.73217 10.0606L2.90374 7.23221C2.70848 7.03695 2.70848 6.72036 2.90374 6.5251C3.59191 5.83693 4.57677 5.7584 5.27929 5.80524C5.64448 5.82959 5.97016 5.8901 6.20368 5.94399C6.23473 5.95116 6.26426 5.95823 6.29215 5.96512L9.42568 2.83159C9.40444 2.70635 9.38642 2.54824 9.38642 2.37085C9.38642 1.94022 9.49442 1.34864 9.97481 0.868248C10.0686 0.77448 10.1958 0.721802 10.3284 0.721802Z" fill="#5F666D"/>
+</svg>
diff --git a/src/assets/pin-angle.svg b/src/assets/pin-angle.svg
new file mode 100644
index 00000000..1c3bd97a
--- /dev/null
+++ b/src/assets/pin-angle.svg
@@ -0,0 +1,3 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.3284 0.721802C10.461 0.721802 10.5881 0.77448 10.6819 0.868248L15.6317 5.818C15.8269 6.01326 15.8269 6.32984 15.6317 6.5251C15.1513 7.0055 14.5597 7.11349 14.1291 7.11349C13.9517 7.11349 13.7936 7.09548 13.6683 7.07423L10.5348 10.2078C10.5417 10.2357 10.5488 10.2652 10.5559 10.2962C10.6098 10.5298 10.6703 10.8554 10.6947 11.2206C10.7415 11.9231 10.663 12.908 9.97481 13.5962C9.77955 13.7914 9.46296 13.7914 9.2677 13.5962L6.43928 10.7677L3.2573 13.9497C3.06203 14.145 2.03835 14.8521 1.84308 14.6568C1.64782 14.4616 2.35493 13.4379 2.55019 13.2426L5.73217 10.0606L2.90374 7.23221C2.70848 7.03695 2.70848 6.72036 2.90374 6.5251C3.59191 5.83693 4.57677 5.7584 5.27929 5.80524C5.64448 5.82959 5.97016 5.8901 6.20368 5.94399C6.23473 5.95116 6.26426 5.95823 6.29215 5.96512L9.42568 2.83159C9.40444 2.70635 9.38642 2.54824 9.38642 2.37085C9.38642 1.94022 9.49442 1.34864 9.97481 0.868248C10.0686 0.77448 10.1958 0.721802 10.3284 0.721802ZM10.45 2.83413L10.4493 2.83181L10.45 2.83413ZM10.4493 2.83181L10.45 2.83413C10.5086 3.01308 10.4616 3.20988 10.3284 3.34312L6.79283 6.87866C6.65903 7.01245 6.46118 7.05925 6.28164 6.9996L6.27962 6.99895L6.2665 6.9948C6.25402 6.99091 6.23426 6.9849 6.20796 6.97734C6.1553 6.96221 6.0769 6.94102 5.97882 6.91839C5.78144 6.87284 5.5105 6.82287 5.21277 6.80302C4.791 6.77491 4.37709 6.8108 4.03811 6.95237L9.54755 12.4618C9.68911 12.1228 9.72501 11.7089 9.69689 11.2871C9.67704 10.9894 9.62707 10.7185 9.58153 10.5211C9.55889 10.423 9.5377 10.3446 9.52257 10.2919C9.51501 10.2657 9.509 10.2459 9.50511 10.2334L9.50096 10.2203L9.50047 10.2188C9.44071 10.0392 9.4874 9.84094 9.62126 9.70708L13.1568 6.17155C13.2905 6.03781 13.4883 5.99099 13.6678 6.05053C13.6713 6.05161 13.6786 6.05373 13.6889 6.05653C13.7094 6.06214 13.7424 6.07045 13.7852 6.07901C13.8717 6.09631 13.9927 6.11349 14.1291 6.11349C14.2426 6.11349 14.3588 6.1019 14.472 6.07253L10.4274 2.02792C10.398 2.14114 10.3864 2.25733 10.3864 2.37085C10.3864 2.50717 10.4036 2.6282 10.4209 2.71473C10.4295 2.7575 10.4378 2.79047 10.4434 2.81105C10.4462 2.82131 10.4482 2.82828 10.4493 2.83181Z" fill="#5F666D"/>
+</svg>
diff --git a/src/components/rails/explorer-rail/explorer-rail.scss b/src/components/rails/explorer-rail/explorer-rail.scss
index b436e0aa..61783510 100644
--- a/src/components/rails/explorer-rail/explorer-rail.scss
+++ b/src/components/rails/explorer-rail/explorer-rail.scss
@@ -1,8 +1,10 @@
+$controls-column-max-width: 340px;
 $controls-transition: 0.3s;
 $secondary-color-nav: #6c757d;
 
 #explorer-rail {
-	max-width: 355px;
+	overflow: hidden;
+	max-width: $controls-column-max-width;
 	background-color: white;
 	width: 100%;
 	transition: all $controls-transition ease-out;
@@ -48,3 +50,4 @@ $secondary-color-nav: #6c757d;
 		}
 	}
 }
+
diff --git a/src/components/rails/explorer-rail/explorer.vue b/src/components/rails/explorer-rail/explorer.vue
index 8fa2ce61..0029ebf2 100644
--- a/src/components/rails/explorer-rail/explorer.vue
+++ b/src/components/rails/explorer-rail/explorer.vue
@@ -8,6 +8,9 @@ import NodeTypesLoop from '@/components/rails/explorer-rail/nodeTypesLoop/nodeTy
 export default {
   name: 'ExplorerRail',
   props: {
+    explorerExpanded: {
+      type: Boolean,
+    },
     nodeTypes: {
       type: Array,
     },
@@ -41,6 +44,9 @@ export default {
     filteredNodes() {
       return nodeTypesStore.getters.getFilteredNodeTypes;
     },
+    unpinnedObjects() {
+      return this.objects.filter((obj) => !this.pinnedObjects.includes(obj));
+    },
   },
   created() {
     nodeTypesStore.commit('setNodeTypes', this.nodeTypes);
@@ -78,19 +84,7 @@ export default {
     </div>
     <div class="node-types__container" v-if="tabIndex === 0">
       <filter-node-types />
-      <template v-if="filteredNodes.length > 0">
-        <node-types-loop :nodeTypes="filteredNodes" />
-      </template>
-      <template v-else>
-        <node-types-loop  v-if="pinnedObjects.length > 0" 
-          label="Pinned Objects"
-          :nodeTypes="pinnedObjects"
-        />
-        <node-types-loop v-if="objects.length > 0"
-          label="Object Category"
-          :nodeTypes="objects"
-        />
-      </template>
+      <node-types-loop />
     </div>
     <div class="pm-blocks__container">
       <!--   Here goes the PM Blocks   -->
diff --git a/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue b/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue
index c5289e65..ce1c1efb 100644
--- a/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue
+++ b/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue
@@ -1,44 +1,151 @@
 <script>
+import pinIcon from '@/assets/pin-angle.svg';
+import pinFillIcon from '@/assets/pin-angle-fill.svg';
+import nodeTypesStore from '@/nodeTypesStore';
+
 export default {
   name: 'NodeTypesLoop',
-  props: {
-    label: {
-      type: String,
+  data() {
+    return {
+      pinIcon,
+      pinFillIcon,
+      showPin: false,
+    };
+  },
+  methods: {
+    nodeTypeAlreadyPinned(object, type) {
+      return !!this.pinnedObjects.find(obj => obj.type === type);
+    },
+    unPin(object) {
+      return nodeTypesStore.commit('setUnpinNode', object);
     },
-    nodeTypes: {
-      type: Array,
+    addPin(object) {
+      return nodeTypesStore.commit('setPinnedNodes', object);
+    },
+  },
+  computed: {
+    pinnedObjects() {
+      return nodeTypesStore.getters.getPinnedNodeTypes;
+    },
+    objects() {
+      return nodeTypesStore.getters.getNodeTypes;
+    },
+    unpinnedObjects() {
+      return this.objects.filter((obj) => !this.pinnedObjects.includes(obj));
+    },
+    filteredNodes() {
+      return nodeTypesStore.getters.getFilteredNodeTypes;
     },
   },
 };
 </script>
 
 <template>
-  <div>
-    <p>{{ $t(label) }}</p>
-    <template v-for="nodeType in nodeTypes">
-      <div class="node-types__item" :key="nodeType.id">
-        <img :src="nodeType.icon" :alt="$t(nodeType.label)">
-        <span>{{ $t(nodeType.label) }}</span>
+  <div id="nodeTypesList">
+    <div id="filteredNodes-container" v-if="filteredNodes.length > 0">
+      <template v-for="object in filteredNodes">
+        <div
+          class="node-types__item"
+          :key="object.id"
+          @mouseover="showPin = true"
+          @mouseleave="showPin = false"
+        >
+          <img class="node-types__item__icon" :src="object.icon" :alt="$t(object.label)">
+          <span>{{ $t(object.label) }}</span>
+          <img
+            v-if="nodeTypeAlreadyPinned(object, object.type)"
+            :src="pinFillIcon"
+            class="pinIcon"
+            alt="Unpin Element"
+            @click="unPin(object)"
+          >
+          <img
+            v-else
+            :src="pinIcon"
+            class="pinIcon"
+            alt="Pin Element"
+            @click="addPin(object)"
+          >
+        </div>
+      </template>
+    </div>
+    <template v-else>
+      <div class="pinnedObjects" v-if="pinnedObjects.length > 0">
+        <p>{{ $t('Pinned Objects') }}</p>
+        <template v-for="pinnedObject in pinnedObjects">
+          <div
+            class="node-types__item"
+            :key="pinnedObject.id"
+            @mouseover="showPin = true"
+            @mouseleave="showPin = false"
+          >
+            <img class="node-types__item__icon" :src="pinnedObject.icon" :alt="$t(pinnedObject.label)">
+            <span>{{ $t(pinnedObject.label) }}</span>
+            <img
+              :src="pinFillIcon"
+              class="pinIcon"
+              alt="Pin/Unpin Element"
+              @click="unPin(pinnedObject)"
+            >
+          </div>
+        </template>
+      </div>
+      <div class="objectCategory">
+        <p>{{ $t('Object Category') }}</p>
+        <template v-for="nodeType in unpinnedObjects">
+          <div
+            class="node-types__item"
+            :key="nodeType.id"
+            @mouseover="showPin = true"
+            @mouseleave="showPin = false"
+          >
+            <img class="node-types__item__icon" :src="nodeType.icon" :alt="$t(nodeType.label)">
+            <span>{{ $t(nodeType.label) }}</span>
+            <img
+              :src="pinIcon"
+              class="pinIcon"
+              alt="Pin/Unpin Element"
+              @click="addPin(nodeType)"
+            >
+          </div>
+        </template>
       </div>
     </template>
   </div>
 </template>
 
 <style lang="scss">
+#nodeTypesList {
+  .pinnedObjects {
+    margin-bottom: 1rem;
+  }
+}
 .node-types {
   &__item {
     display: flex;
-    margin-bottom: 1rem;
+    padding: 0.5rem 0.3rem;
     align-items: center;
-    img {
-      height: 1.5rem;
+    border-radius: 4px;
+    &:hover {
+      background-color: #EBEEF2;
+      .pinIcon {
+        background-color: #DADDDF;
+      }
+    }
+    &__icon {
       width: 1.5rem;
+      height: 1.5rem;
     }
     span {
       margin-left: 0.8rem;
       font-size: 13px;
       line-height: 19px;
     }
+    .pinIcon {
+      margin-left: auto;
+      border-radius: 4px;
+      padding: 0.3rem;
+    }
   }
 }
 </style>
diff --git a/src/nodeTypesStore.js b/src/nodeTypesStore.js
index a98d026d..4e75e9ac 100644
--- a/src/nodeTypesStore.js
+++ b/src/nodeTypesStore.js
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 import Vuex from 'vuex';
+import { uniqueId, uniqBy } from 'lodash';
 import { BOTTOM } from '@/components/rails/explorer-rail/rankConstants';
 
 Vue.use(Vuex);
@@ -20,6 +21,7 @@ export default new Vuex.Store({
       state.nodeTypes = nodeTypes
         .filter(nodeType => nodeType.control)
         .map(nodeType => ({
+          id: uniqueId('nodeType_'),
           type: nodeType.id,
           icon: nodeType.icon,
           label: nodeType.label,
@@ -29,18 +31,21 @@ export default new Vuex.Store({
         .sort((node1, node2) => node1.rank - node2.rank);
     },
     setPinnedNodes(state, payload) {
-      state.pinnedNodeTypes = payload;
+      state.pinnedNodeTypes.push(payload);
+    },
+    setUnpinNode(state, payload) {
+      state.pinnedNodeTypes = state.pinnedNodeTypes.filter(node => node !== payload);
     },
     setFilteredNodeTypes(state, searchTerm) {
       const pinnedNodeTypes = state.pinnedNodeTypes;
       const nodeTypes = state.nodeTypes;
-      const allNodes = [...pinnedNodeTypes, ...nodeTypes];
+      const allNodes = uniqBy([...pinnedNodeTypes, ...nodeTypes], 'id');
       state.filteredNodeTypes = allNodes.filter(node => {
         return node.label.toLowerCase().includes(searchTerm.toLowerCase());
       });
     },
     clearFilteredNodes(state) {
-      state.filteredNodeTypes.length = 0;
+      state.filteredNodeTypes = [];
     },
   },
   actions: {


Test patch:
diff --git a/tests/unit/rails/__snapshots__/explorer.spec.js.snap b/tests/unit/rails/__snapshots__/explorer.spec.js.snap
index b55cf7a0..c89de17d 100644
--- a/tests/unit/rails/__snapshots__/explorer.spec.js.snap
+++ b/tests/unit/rails/__snapshots__/explorer.spec.js.snap
@@ -36,9 +36,7 @@ exports[`Explorer Rail should render the component 1`] = `
   >
     <filter-node-types-stub />
      
-    <!---->
-     
-    <!---->
+    <node-types-loop-stub />
   </div>
    
   <div