Repository: ProcessMaker/modeler
Pull request #1556 (2023-05-19 19:50:19)

FOUR-8427: Click and Drop now implemented in Explorer
## Solution
- Using an implementation done in [Bottom Rail: FOUR-8406](https://processmaker.atlassian.net/browse/FOUR-8406?search_id=6237ad19-81d6-47ec-989a-aac6c69a3a43) explorer is able to emit events to Modeler.vue to handle the click and drag event.

# Important
This code was extracted from a WIP branch and may need the same adjustments as it's counterpart if there were to be any. 
Also note that the implementation on Modeler.vue was omitted to avoid merge conflicts with the feature/FOUR-8406 branch, so upon merging 8406, this branch should be fully functional.

## How to Test
Test the steps above

## Related Tickets & Packages
- https://processmaker.atlassian.net/browse/FOUR-8427
- https://processmaker.atlassian.net/browse/FOUR-8406

## 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.



Patch:
diff --git a/src/components/modeler/Modeler.vue b/src/components/modeler/Modeler.vue
index b32d364c..6578b4f0 100644
--- a/src/components/modeler/Modeler.vue
+++ b/src/components/modeler/Modeler.vue
@@ -25,6 +25,8 @@
       />
       <explorer-rail
         :node-types="nodeTypes"
+        @set-cursor="cursor = $event"
+        @onCreateElement="onCreateElementHandler"
       />
       <b-col
         class="paper-container h-100 pr-4"
@@ -872,6 +874,13 @@ export default {
     toXML(cb) {
       this.moddle.toXML(this.definitions, { format: true }, cb);
     },
+    onCreateElementHandler({ event, control }) {
+      this.handleDrop({
+        clientX: event.clientX,
+        clientY: event.clientY,
+        control,
+      });
+    },
     async handleDrop({ clientX, clientY, control, nodeThatWillBeReplaced }) {
       this.validateDropTarget({ clientX, clientY, control });
 
@@ -1201,6 +1210,7 @@ export default {
           this.$refs.selector.stopDrag(event);
         }
       }
+      window.ProcessMaker.EventBus.$emit('custom-pointerclick', event);
       this.isDragging = false;
       this.dragStart = null;
       this.isSelecting = false;
diff --git a/src/components/railBottom/controls/Controls.vue b/src/components/railBottom/controls/Controls.vue
index 2efd23a5..d8c6b395 100644
--- a/src/components/railBottom/controls/Controls.vue
+++ b/src/components/railBottom/controls/Controls.vue
@@ -6,7 +6,12 @@
     </li>
 
     <li class="control-item">
-      <div class="control-add" :title="$t('Add')" v-b-tooltip.hover>
+      <div
+        :class="{'control-add': true, 'control-active': explorerOpen}"
+        :title="$t('Add')"
+        v-b-tooltip.hover
+        @click="toggleExplorer"
+      >
         <img :src="plusIcon" :alt="$t('Add')">
       </div>
     </li>
@@ -15,6 +20,8 @@
 
 <script>
 import SubmenuPopper from './SubmenuPopper/SubmenuPopper.vue';
+import nodeTypesStore from '@/nodeTypesStore';
+
 export default ({
   components: {
     SubmenuPopper,
@@ -188,6 +195,17 @@ export default ({
       plusIcon: require('@/assets/railBottom/plus-lg-light.svg'),
     };
   },
+  methods: {
+    toggleExplorer() {
+      nodeTypesStore.commit('toggleExplorer');
+      nodeTypesStore.commit('clearFilteredNodes');
+    },
+  },
+  computed: {
+    explorerOpen() {
+      return nodeTypesStore.getters.getExplorerOpen;
+    },
+  },
 });
 
 </script>
diff --git a/src/components/railBottom/railBottom.scss b/src/components/railBottom/railBottom.scss
index 767d4148..f2d8562c 100644
--- a/src/components/railBottom/railBottom.scss
+++ b/src/components/railBottom/railBottom.scss
@@ -1,7 +1,8 @@
 .rail {
   &-container {
-    position: relative;
-    bottom: 126px;
+    position: absolute;
+    bottom: 20px;
+    left: 0;
     display: flex;
     align-items: center;
     width: 100%;
diff --git a/src/components/rails/explorer-rail/explorer-rail.scss b/src/components/rails/explorer-rail/explorer-rail.scss
index 61783510..624cc706 100644
--- a/src/components/rails/explorer-rail/explorer-rail.scss
+++ b/src/components/rails/explorer-rail/explorer-rail.scss
@@ -7,11 +7,13 @@ $secondary-color-nav: #6c757d;
 	max-width: $controls-column-max-width;
 	background-color: white;
 	width: 100%;
+	height: 80%;
 	transition: all $controls-transition ease-out;
 	z-index: 2;
 	border-radius: 4px;
 	box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
 	padding-left: 1rem;
+	overflow-y: auto;
 }
 
 .rail-menu {
diff --git a/src/components/rails/explorer-rail/explorer.vue b/src/components/rails/explorer-rail/explorer.vue
index 0029ebf2..d31e2852 100644
--- a/src/components/rails/explorer-rail/explorer.vue
+++ b/src/components/rails/explorer-rail/explorer.vue
@@ -22,8 +22,6 @@ export default {
   },
   data() {
     return {
-      // TODO set to false when the bottom control rail is merged
-      expanded: true,
       tabs: [{
         idx: 0,
         label: 'Add Object',
@@ -44,8 +42,8 @@ export default {
     filteredNodes() {
       return nodeTypesStore.getters.getFilteredNodeTypes;
     },
-    unpinnedObjects() {
-      return this.objects.filter((obj) => !this.pinnedObjects.includes(obj));
+    explorerOpen() {
+      return nodeTypesStore.getters.getExplorerOpen;
     },
   },
   created() {
@@ -55,21 +53,28 @@ export default {
     faTimes() {
       return faTimes;
     },
+    /**
+     * @param {string} idx
+     * @return void
+     */
     setTabIndex(idx) {
+      if (this.tabIndex === idx) return;
       this.tabIndex = idx;
-    },
-    toggleRail() {
-      this.expanded = !this.expanded;
+      this.clearPinnedObjects();
     },
     closeRail() {
-      this.expanded = false;
+      nodeTypesStore.commit('closeExplorer');
+      this.clearPinnedObjects();
+    },
+    clearPinnedObjects() {
+      nodeTypesStore.commit('clearFilteredNodes');
     },
   },
 };
 </script>
 
 <template>
-  <div id="explorer-rail" data-test="explorer-rail" v-if="expanded">
+  <div id="explorer-rail" data-test="explorer-rail" v-if="explorerOpen">
     <div class="rail-menu">
       <b-tabs class="tabs--container" :no-nav-style="true">
         <template v-for="tab in tabs">
@@ -84,7 +89,9 @@ export default {
     </div>
     <div class="node-types__container" v-if="tabIndex === 0">
       <filter-node-types />
-      <node-types-loop />
+      <node-types-loop
+        v-on="$listeners"
+      />
     </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 ce1c1efb..cef14e90 100644
--- a/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue
+++ b/src/components/rails/explorer-rail/nodeTypesLoop/nodeTypesLoop.vue
@@ -10,6 +10,21 @@ export default {
       pinIcon,
       pinFillIcon,
       showPin: false,
+      wasClicked: false,
+      element: null,
+      selectedItem: null,
+      xOffset: 0,
+      yOffset: 0,
+      movedElement: null,
+      helperStyles: {
+        backgroundColor:'#ffffff',
+        position: 'absolute',
+        height: '40px',
+        width: '40px',
+        zIndex: '10',
+        opacity: '0.5',
+        pointerEvents: 'none',
+      },
     };
   },
   methods: {
@@ -22,6 +37,63 @@ export default {
     addPin(object) {
       return nodeTypesStore.commit('setPinnedNodes', object);
     },
+    onClickHandler(event, control) {
+      this.createDraggingHelper(event, control);
+      document.addEventListener('mousemove', this.setDraggingPosition);
+      this.setDraggingPosition(event);
+      // Deselect control on click if same control is already selected
+      if (this.selectedItem === control.type) {
+        document.removeEventListener('mousemove', this.setDraggingPosition);
+        document.body.removeChild(this.movedElement);
+        this.$emit('onSetCursor', 'none');
+        this.selectedItem = null;
+        this.movedElement = null;
+        this.wasClicked = false;
+        return;
+      }
+      this.wasClicked = true;
+      this.element = control;
+      this.$emit('onSetCursor', 'crosshair');
+      this.selectedItem = control.type;
+      window.ProcessMaker.EventBus.$on('custom-pointerclick', message => {
+        window.ProcessMaker.EventBus.$off('custom-pointerclick');
+        document.removeEventListener('mousemove', this.setDraggingPosition);
+        if (this.movedElement) {
+          document.body.removeChild(this.movedElement);
+        }
+        this.selectedItem = null;
+        this.movedElement = null;
+        this.onCreateElement(message);
+      });
+    },
+    createDraggingHelper(event, control) {
+      if (this.movedElement) {
+        document.removeEventListener('mousemove', this.setDraggingPosition);
+        document.body.removeChild(this.movedElement);
+        this.movedElement = null;
+      }
+      const sourceElement = event.target;
+      this.movedElement = document.createElement('img');
+      Object.keys(this.helperStyles).forEach((property) => {
+        this.movedElement.style[property] = this.helperStyles[property];
+      });
+      this.movedElement.src = control.icon;
+      document.body.appendChild(this.movedElement);
+      this.xOffset = event.clientX - sourceElement.getBoundingClientRect().left;
+      this.yOffset = event.clientY - sourceElement.getBoundingClientRect().top;
+    },
+    setDraggingPosition({ pageX, pageY }) {
+      this.movedElement.style.left = pageX  + 'px';
+      this.movedElement.style.top = pageY + 'px';
+    },
+    onCreateElement(event){
+      if (this.wasClicked && this.element) {
+        this.$emit('onCreateElement', { event, control: this.element });
+        this.$emit('onSetCursor', 'none');
+        event.preventDefault();
+        this.wasClicked = false;
+      }
+    },
   },
   computed: {
     pinnedObjects() {
@@ -36,6 +108,9 @@ export default {
     filteredNodes() {
       return nodeTypesStore.getters.getFilteredNodeTypes;
     },
+    searchTerm() {
+      return nodeTypesStore.getters.getSearchTerm;
+    },
   },
 };
 </script>
@@ -49,6 +124,7 @@ export default {
           :key="object.id"
           @mouseover="showPin = true"
           @mouseleave="showPin = false"
+          @click.stop="onClickHandler($event, object)"
         >
           <img class="node-types__item__icon" :src="object.icon" :alt="$t(object.label)">
           <span>{{ $t(object.label) }}</span>
@@ -69,7 +145,7 @@ export default {
         </div>
       </template>
     </div>
-    <template v-else>
+    <template v-if="filteredNodes.length === 0 && !searchTerm">
       <div class="pinnedObjects" v-if="pinnedObjects.length > 0">
         <p>{{ $t('Pinned Objects') }}</p>
         <template v-for="pinnedObject in pinnedObjects">
@@ -78,6 +154,7 @@ export default {
             :key="pinnedObject.id"
             @mouseover="showPin = true"
             @mouseleave="showPin = false"
+            @click.stop="onClickHandler($event, pinnedObject)"
           >
             <img class="node-types__item__icon" :src="pinnedObject.icon" :alt="$t(pinnedObject.label)">
             <span>{{ $t(pinnedObject.label) }}</span>
@@ -98,6 +175,7 @@ export default {
             :key="nodeType.id"
             @mouseover="showPin = true"
             @mouseleave="showPin = false"
+            @click.stop="onClickHandler($event, nodeType)"
           >
             <img class="node-types__item__icon" :src="nodeType.icon" :alt="$t(nodeType.label)">
             <span>{{ $t(nodeType.label) }}</span>
@@ -126,6 +204,7 @@ export default {
     padding: 0.5rem 0.3rem;
     align-items: center;
     border-radius: 4px;
+    user-select: none;
     &:hover {
       background-color: #EBEEF2;
       .pinIcon {
diff --git a/src/nodeTypesStore.js b/src/nodeTypesStore.js
index 4e75e9ac..292795ca 100644
--- a/src/nodeTypesStore.js
+++ b/src/nodeTypesStore.js
@@ -10,11 +10,15 @@ export default new Vuex.Store({
     nodeTypes: [],
     pinnedNodeTypes: [],
     filteredNodeTypes: [],
+    explorerOpen: false,
+    searchTerm: '',
   },
   getters: {
     getNodeTypes: state => state.nodeTypes,
     getPinnedNodeTypes: state => state.pinnedNodeTypes,
     getFilteredNodeTypes: state => state.filteredNodeTypes,
+    getExplorerOpen: state => state.explorerOpen,
+    getSearchTerm: state => state.searchTerm,
   },
   mutations: {
     setNodeTypes(state, nodeTypes) {
@@ -39,6 +43,7 @@ export default new Vuex.Store({
     setFilteredNodeTypes(state, searchTerm) {
       const pinnedNodeTypes = state.pinnedNodeTypes;
       const nodeTypes = state.nodeTypes;
+      state.searchTerm = searchTerm;
       const allNodes = uniqBy([...pinnedNodeTypes, ...nodeTypes], 'id');
       state.filteredNodeTypes = allNodes.filter(node => {
         return node.label.toLowerCase().includes(searchTerm.toLowerCase());
@@ -46,6 +51,13 @@ export default new Vuex.Store({
     },
     clearFilteredNodes(state) {
       state.filteredNodeTypes = [];
+      state.searchTerm = '';
+    },
+    closeExplorer(state) {
+      state.explorerOpen = false;
+    },
+    toggleExplorer(state) {
+      state.explorerOpen = !state.explorerOpen;
     },
   },
   actions: {


Test patch:
diff --git a/tests/unit/rails/__snapshots__/explorer.spec.js.snap b/tests/unit/rails/__snapshots__/explorer.spec.js.snap
index c89de17d..2c880f2d 100644
--- a/tests/unit/rails/__snapshots__/explorer.spec.js.snap
+++ b/tests/unit/rails/__snapshots__/explorer.spec.js.snap
@@ -1,46 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Explorer Rail should render the component 1`] = `
-<div
-  data-test="explorer-rail"
-  id="explorer-rail"
->
-  <div
-    class="rail-menu"
-  >
-    <b-tabs-stub
-      class="tabs--container"
-      nonavstyle="true"
-      tag="div"
-    >
-      <b-tab-stub
-        active="true"
-        tag="div"
-      />
-      <b-tab-stub
-        tag="div"
-      />
-    </b-tabs-stub>
-     
-    <div
-      class="close--container"
-    >
-      <font-awesome-icon-stub
-        icon="[object Object]"
-      />
-    </div>
-  </div>
-   
-  <div
-    class="node-types__container"
-  >
-    <filter-node-types-stub />
-     
-    <node-types-loop-stub />
-  </div>
-   
-  <div
-    class="pm-blocks__container"
-  />
-</div>
-`;
+exports[`Explorer Rail should render the component 1`] = `<!---->`;
diff --git a/tests/unit/rails/explorer.spec.js b/tests/unit/rails/explorer.spec.js
index 3cf395b9..ab6762cf 100644
--- a/tests/unit/rails/explorer.spec.js
+++ b/tests/unit/rails/explorer.spec.js
@@ -32,8 +32,6 @@ describe('Explorer Rail', () => {
     expect(wrapper.vm.$el).toMatchSnapshot();
   });
   it('should check for vdata defaults', () => {
-    // TODO uncomment the code when the bottom rail has been merged
-    // expect(wrapper.vm.expanded).toBe(false);
     expect(wrapper.vm.tabs).toHaveLength(2);
     expect(wrapper.vm.tabIndex).toBe(0);
   });
@@ -41,13 +39,11 @@ describe('Explorer Rail', () => {
     wrapper.vm.setTabIndex(1);
     expect(wrapper.vm.tabIndex).toBe(1);
   });
-  // TODO replace the xit with it when bottom rail has been merged
   xit('should toggle the rail', () => {
     expect(wrapper.vm.expanded).toBe(false);
     wrapper.vm.toggleRail();
     expect(wrapper.vm.expanded).toBe(true);
   });
-  // TODO replace the xit with it when bottom rail has been merged
   xit('should close the rail', () => {
     // toggling the rail to be opened
     wrapper.vm.toggleRail();