Browse Source

refactor: port collection move/reorder

jamesgeorge007 1 year ago
parent
commit
b2531891c8

+ 56 - 1
packages/hoppscotch-common/src/components/new-collections/index.vue

@@ -1,6 +1,17 @@
 <template>
   <div v-if="!activeWorkspaceHandle">No Workspace Selected.</div>
-  <div v-else class="flex-1">
+  <div
+    v-else
+    :class="{
+      'rounded border border-divider': saveRequest,
+      'bg-primaryDark':
+        draggingToRoot && currentReorderingStatus.type !== 'request',
+    }"
+    class="flex-1"
+    @drop.prevent="dropToRoot"
+    @dragover.prevent="draggingToRoot = true"
+    @dragend="draggingToRoot = false"
+  >
     <div
       class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
       :style="{
@@ -30,7 +41,12 @@
 import { useService } from "dioc/vue"
 import { ref } from "vue"
 import { useI18n } from "~/composables/i18n"
+import { useReadonlyStream } from "~/composables/stream"
+import { useToast } from "~/composables/toast"
 import { Picked } from "~/helpers/types/HoppPicked"
+import toast from "~/modules/toast"
+import { moveRESTFolder } from "~/newstore/collections"
+import { currentReorderingStatus$ } from "~/newstore/reordering"
 import { NewWorkspaceService } from "~/services/new-workspace"
 
 defineProps<{
@@ -44,10 +60,49 @@ const emit = defineEmits<{
 }>()
 
 const t = useI18n()
+const toast = useToast()
 
+const draggingToRoot = ref(false)
 const searchText = ref("")
 
 const workspaceService = useService(NewWorkspaceService)
 
 const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle
+
+const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
+  type: "collection",
+  id: "",
+  parentID: "",
+})
+
+/**
+ * This function is called when the user drops the collection
+ * to the root
+ * @param payload - object containing the collection index dragged
+ */
+const dropToRoot = ({ dataTransfer }: DragEvent) => {
+  if (dataTransfer) {
+    const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+    if (!collectionIndexDragged) return
+    // check if the collection is already in the root
+    if (isAlreadyInRoot(collectionIndexDragged)) {
+      toast.error(`${t("collection.invalid_root_move")}`)
+    } else {
+      moveRESTFolder(collectionIndexDragged, null)
+      toast.success(`${t("collection.moved")}`)
+    }
+
+    draggingToRoot.value = false
+  }
+}
+
+/**
+ * Checks if the collection is already in the root
+ * @param id - path of the collection
+ * @returns boolean - true if the collection is already in the root
+ */
+const isAlreadyInRoot = (id: string) => {
+  const indexPath = id.split("/")
+  return indexPath.length === 1
+}
 </script>

+ 200 - 3
packages/hoppscotch-common/src/components/new-collections/rest/Collection.vue

@@ -1,8 +1,37 @@
 <template>
   <div class="flex flex-col">
+    <div
+      class="h-1 w-full transition"
+      :class="[
+        {
+          'bg-accentDark': isReorderable,
+        },
+      ]"
+      @drop="orderUpdateCollectionEvent"
+      @dragover.prevent="ordering = true"
+      @dragleave="ordering = false"
+      @dragend="resetDragState"
+    ></div>
     <div class="relative flex flex-col">
       <div
-        class="z-3 group pointer-events-auto relative flex cursor-pointer items-stretch"
+        class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
+        :class="{
+          'opacity-25':
+            dragging && notSameDestination && notSameParentDestination,
+        }"
+      ></div>
+      <div
+        class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch"
+        @dragstart="dragStart"
+        @drop="handelDrop($event)"
+        @dragover="handleDragOver($event)"
+        @dragleave="resetDragState"
+        @dragend="
+          () => {
+            resetDragState()
+            dropItemID = ''
+          }
+        "
         @contextmenu.prevent="options?.tippy.show()"
       >
         <div
@@ -149,14 +178,33 @@
         </div>
       </div>
     </div>
+    <div
+      v-if="isLastItem"
+      class="w-full transition"
+      :class="[
+        {
+          'bg-accentDark': isLastItemReorderable,
+          'h-1 ': isLastItem,
+        },
+      ]"
+      @drop="updateLastItemOrder"
+      @dragover.prevent="orderingLastItem = true"
+      @dragleave="orderingLastItem = false"
+      @dragend="resetDragState"
+    ></div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from "vue"
+import { computed, ref, watch } from "vue"
 import { TippyComponent } from "vue-tippy"
 
 import { useI18n } from "~/composables/i18n"
+import { useReadonlyStream } from "~/composables/stream"
+import {
+  currentReorderingStatus$,
+  changeCurrentReorderStatus,
+} from "~/newstore/reordering"
 import { RESTCollectionViewCollection } from "~/services/new-workspace/view"
 
 import IconCheckCircle from "~icons/lucide/check-circle"
@@ -176,11 +224,15 @@ const props = defineProps<{
   collectionView: RESTCollectionViewCollection
   isOpen: boolean
   isSelected?: boolean | null
+  isLastItem?: boolean
 }>()
 
 const emit = defineEmits<{
   (event: "add-child-collection", parentCollectionIndexPath: string): void
   (event: "add-request", parentCollectionIndexPath: string): void
+  (event: "dragging", payload: boolean): void
+  (event: "drop-event", payload: DataTransfer): void
+  (event: "drag-event", payload: DataTransfer): void
   (
     event: "edit-child-collection",
     payload: { collectionIndexPath: string; collectionName: string }
@@ -194,6 +246,8 @@ const emit = defineEmits<{
   (event: "remove-child-collection", collectionIndexPath: string): void
   (event: "remove-root-collection", collectionIndexPath: string): void
   (event: "toggle-children"): void
+  (event: "update-collection-order", payload: DataTransfer): void
+  (event: "update-last-collection-order", payload: DataTransfer): void
 }>()
 
 const tippyActions = ref<TippyComponent | null>(null)
@@ -205,11 +259,69 @@ const exportAction = ref<HTMLButtonElement | null>(null)
 const propertiesAction = ref<TippyComponent | null>(null)
 const options = ref<TippyComponent | null>(null)
 
+const dragging = ref(false)
+const ordering = ref(false)
+const orderingLastItem = ref(false)
+const dropItemID = ref("")
+
+const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
+  type: "collection",
+  id: "",
+  parentID: "",
+})
+
+// Used to determine if the collection is being dragged to a different destination
+// This is used to make the highlight effect work
+watch(
+  () => dragging.value,
+  (val) => {
+    if (val && notSameDestination.value && notSameParentDestination.value) {
+      emit("dragging", true)
+    } else {
+      emit("dragging", false)
+    }
+  }
+)
+
 const collectionIcon = computed(() => {
   if (props.isSelected) {
     return IconCheckCircle
   }
-  return !props.isOpen ? IconFolder : IconFolderOpen
+  return props.isOpen ? IconFolderOpen : IconFolder
+})
+
+const notSameParentDestination = computed(() => {
+  return (
+    currentReorderingStatus.value.parentID !== props.collectionView.collectionID
+  )
+})
+
+const isRequestDragging = computed(() => {
+  return currentReorderingStatus.value.type === "request"
+})
+
+const isSameParent = computed(() => {
+  return (
+    currentReorderingStatus.value.parentID ===
+    props.collectionView.parentCollectionID
+  )
+})
+
+const isReorderable = computed(() => {
+  return (
+    ordering.value &&
+    notSameDestination.value &&
+    !isRequestDragging.value &&
+    isSameParent.value
+  )
+})
+const isLastItemReorderable = computed(() => {
+  return (
+    orderingLastItem.value &&
+    notSameDestination.value &&
+    !isRequestDragging.value &&
+    isSameParent.value
+  )
 })
 
 const addChildCollection = () => {
@@ -234,6 +346,85 @@ const editCollection = () => {
     : emit("edit-root-collection", data)
 }
 
+const dragStart = ({ dataTransfer }: DragEvent) => {
+  if (dataTransfer) {
+    emit("drag-event", dataTransfer)
+    dropItemID.value = dataTransfer.getData("collectionIndex")
+    dragging.value = !dragging.value
+    changeCurrentReorderStatus({
+      type: "collection",
+      id: props.collectionView.collectionID,
+      parentID: props.collectionView.parentCollectionID,
+    })
+  }
+}
+
+// Trigger the re-ordering event when a collection is dragged over another collection's top section
+const handleDragOver = (e: DragEvent) => {
+  dragging.value = true
+  if (
+    e.offsetY < 10 &&
+    notSameDestination.value &&
+    !isRequestDragging.value &&
+    isSameParent.value
+  ) {
+    ordering.value = true
+    dragging.value = false
+    orderingLastItem.value = false
+  } else if (
+    e.offsetY > 18 &&
+    notSameDestination.value &&
+    !isRequestDragging.value &&
+    isSameParent.value &&
+    props.isLastItem
+  ) {
+    orderingLastItem.value = true
+    dragging.value = false
+    ordering.value = false
+  } else {
+    ordering.value = false
+    orderingLastItem.value = false
+  }
+}
+
+const handelDrop = (e: DragEvent) => {
+  if (ordering.value) {
+    orderUpdateCollectionEvent(e)
+  } else if (orderingLastItem.value) {
+    updateLastItemOrder(e)
+  } else {
+    notSameParentDestination.value ? dropEvent(e) : e.stopPropagation()
+  }
+}
+
+const dropEvent = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    emit("drop-event", e.dataTransfer)
+    resetDragState()
+  }
+}
+
+const orderUpdateCollectionEvent = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    emit("update-collection-order", e.dataTransfer)
+    resetDragState()
+  }
+}
+
+const updateLastItemOrder = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    emit("update-last-collection-order", e.dataTransfer)
+    resetDragState()
+  }
+}
+
+const notSameDestination = computed(() => {
+  return dropItemID.value !== props.collectionView.collectionID
+})
+
 const removeCollection = () => {
   const { collectionID } = props.collectionView
 
@@ -241,4 +432,10 @@ const removeCollection = () => {
     ? emit("remove-child-collection", collectionID)
     : emit("remove-root-collection", collectionID)
 }
+
+const resetDragState = () => {
+  dragging.value = false
+  ordering.value = false
+  orderingLastItem.value = false
+}
 </script>

+ 487 - 32
packages/hoppscotch-common/src/components/new-collections/rest/index.vue

@@ -34,10 +34,13 @@
 
     <div class="flex flex-1 flex-col">
       <HoppSmartTree :adapter="treeAdapter">
-        <template #content="{ node, toggleChildren, isOpen }">
+        <template
+          #content="{ highlightChildren, isOpen, node, toggleChildren }"
+        >
           <NewCollectionsRestCollection
             v-if="node.data.type === 'collection'"
             :collection-view="node.data.value"
+            :is-last-item="node.data.value.isLastItem"
             :is-open="isOpen"
             :is-selected="
               isSelected(
@@ -47,6 +50,14 @@
             :save-request="saveRequest"
             @add-request="addRequest"
             @add-child-collection="addChildCollection"
+            @dragging="
+              (isDraging) =>
+                highlightChildren(
+                  isDraging ? node.data.value.collectionID : null
+                )
+            "
+            @drag-event="dragEvent($event, node.data.value.collectionID)"
+            @drop-event="dropEvent($event, node.data.value.collectionID)"
             @edit-child-collection="editChildCollection"
             @edit-root-collection="editRootCollection"
             @edit-collection-properties="editCollectionProperties"
@@ -68,22 +79,55 @@
                     })
               }
             "
+            @update-collection-order="
+              updateCollectionOrder($event, {
+                destinationCollectionIndex: node.data.value.collectionID,
+                destinationCollectionParentIndex:
+                  node.data.value.parentCollectionID,
+              })
+            "
+            @update-last-collection-order="
+              updateCollectionOrder($event, {
+                destinationCollectionIndex: null,
+                destinationCollectionParentIndex:
+                  node.data.value.parentCollectionID,
+              })
+            "
           />
 
           <NewCollectionsRestRequest
             v-else-if="node.data.type === 'request'"
             :is-active="isActiveRequest(node.data.value)"
+            :is-last-item="node.data.value.isLastItem"
             :is-selected="
               isSelected(getRequestIndexPathArgs(node.data.value.requestID))
             "
             :request-view="node.data.value"
             :save-request="saveRequest"
+            @drag-request="
+              dragRequest($event, {
+                parentCollectionIndexPath: node.data.value.parentCollectionID,
+                requestIndex: node.data.value.requestID,
+              })
+            "
             @duplicate-request="duplicateRequest"
             @edit-request="editRequest"
             @remove-request="removeRequest"
             @select-pick="onSelectPick"
             @select-request="selectRequest"
             @share-request="shareRequest"
+            @update-request-order="
+              updateRequestOrder($event, {
+                parentCollectionIndexPath: node.data.value.parentCollectionID,
+                requestIndex: node.data.value.requestID,
+              })
+            "
+            @update-last-request-order="
+              updateRequestOrder($event, {
+                parentCollectionIndexPath: node.data.value.parentCollectionID,
+                requestIndex: null,
+              })
+            "
           />
           <div v-else @click="toggleChildren">
             {{ node.data.value }}
@@ -96,6 +140,13 @@
       </HoppSmartTree>
     </div>
 
+    <!-- <div
+      class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
+      :class="{
+        '!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
+      }"
+    > -->
+
     <CollectionsAdd
       :show="showModalAdd"
       :loading-state="modalLoadingState"
@@ -169,20 +220,29 @@ import { useService } from "dioc/vue"
 import { markRaw, nextTick, ref } from "vue"
 
 import { HoppCollection, HoppRESTAuth, HoppRESTRequest } from "@hoppscotch/data"
-import { cloneDeep } from "lodash-es"
+import { cloneDeep, isEqual } from "lodash-es"
 import { useI18n } from "~/composables/i18n"
 import { useReadonlyStream } from "~/composables/stream"
 import { useToast } from "~/composables/toast"
 import { invokeAction } from "~/helpers/actions"
 import { WorkspaceRESTCollectionTreeAdapter } from "~/helpers/adapters/WorkspaceRESTCollectionTreeAdapter"
 import { TeamCollection } from "~/helpers/backend/graphql"
-import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
+import {
+  getFoldersByPath,
+  resolveSaveContextOnCollectionReorder,
+  updateInheritedPropertiesForAffectedRequests,
+} from "~/helpers/collection/collection"
 import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
 import { Picked } from "~/helpers/types/HoppPicked"
 import {
+  cascadeParentCollectionForHeaderAuth,
+  moveRESTFolder,
+  moveRESTRequest,
   navigateToFolderWithIndexPath,
   restCollections$,
   saveRESTRequestAs,
+  updateRESTCollectionOrder,
+  updateRESTRequestOrder,
 } from "~/newstore/collections"
 import { platform } from "~/platform"
 import { NewWorkspaceService } from "~/services/new-workspace"
@@ -205,9 +265,42 @@ const props = defineProps<{
 }>()
 
 const emit = defineEmits<{
-  (event: "select", payload: Picked | null): void
   (e: "display-modal-add"): void
   (e: "display-modal-import-export"): void
+  (
+    event: "drop-collection",
+    payload: {
+      collectionIndexDragged: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "drop-request",
+    payload: {
+      parentCollectionIndexPath: string
+      requestIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (event: "select", payload: Picked | null): void
+  (
+    event: "update-collection-order",
+    payload: {
+      dragedCollectionIndex: string
+      destinationCollection: {
+        destinationCollectionIndex: string | null
+        destinationCollectionParentIndex: string | null
+      }
+    }
+  ): void
+  (
+    event: "update-request-order",
+    payload: {
+      dragedRequestIndex: string
+      destinationRequestIndex: string | null
+      destinationCollectionIndex: string
+    }
+  ): void
 }>()
 
 const workspaceService = useService(NewWorkspaceService)
@@ -1151,47 +1244,302 @@ const exportCollection = async (collectionIndexPath: string) => {
   }
 }
 
-const shareRequest = (request: HoppRESTRequest) => {
-  if (currentUser.value) {
-    // Opens the share request modal if the user is logged in
-    return invokeAction("share.request", { request })
+const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
+  dataTransfer.setData("collectionIndex", collectionIndex)
+}
+
+const dragRequest = (
+  dataTransfer: DataTransfer,
+  {
+    parentCollectionIndexPath,
+    requestIndex,
+  }: { parentCollectionIndexPath: string | null; requestIndex: string }
+) => {
+  if (!parentCollectionIndexPath) return
+  dataTransfer.setData("parentCollectionIndexPath", parentCollectionIndexPath)
+  dataTransfer.setData("requestIndex", requestIndex)
+}
+
+const dropEvent = (
+  dataTransfer: DataTransfer,
+  destinationCollectionIndex: string
+) => {
+  const parentCollectionIndexPath = dataTransfer.getData(
+    "parentCollectionIndexPath"
+  )
+  const requestIndex = dataTransfer.getData("requestIndex")
+  const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+
+  if (parentCollectionIndexPath && requestIndex) {
+    // emit("drop-request", {
+    //   parentCollectionIndexPath,
+    //   requestIndex,
+    //   destinationCollectionIndex,
+    // })
+    dropRequest({
+      parentCollectionIndexPath,
+      requestIndex,
+      destinationCollectionIndex,
+    })
+  } else {
+    dropCollection({
+      collectionIndexDragged,
+      destinationCollectionIndex,
+    })
   }
+}
 
-  // Else, prompts the user to login
-  invokeAction("modals.login.toggle")
+const dropRequest = (payload: {
+  parentCollectionIndexPath?: string | undefined
+  requestIndex: string
+  destinationCollectionIndex: string
+}) => {
+  const {
+    parentCollectionIndexPath,
+    requestIndex,
+    destinationCollectionIndex,
+  } = payload
+
+  if (
+    !requestIndex ||
+    !destinationCollectionIndex ||
+    !parentCollectionIndexPath
+  )
+    return
+
+  // const { auth, headers } = cascadeParentCollectionForHeaderAuth(
+  //   destinationCollectionIndex,
+  //   "rest"
+  // )
+
+  // const possibleTab = tabs.getTabRefWithSaveContext({
+  //   originLocation: "user-collection",
+  //   folderPath,
+  //   requestIndex: pathToLastIndex(requestIndex),
+  // })
+
+  // If there is a tab attached to this request, change save its save context
+  // if (possibleTab) {
+  //   possibleTab.value.document.saveContext = {
+  //     originLocation: "user-collection",
+  //     folderPath: destinationCollectionIndex,
+  //     requestIndex: getRequestsByPath(
+  //       restCollectionState.value,
+  //       destinationCollectionIndex
+  //     ).length,
+  //   }
+
+  //   possibleTab.value.document.inheritedProperties = {
+  //     auth,
+  //     headers,
+  //   }
+  // }
+
+  // When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
+  // resolveSaveContextOnRequestReorder({
+  //   lastIndex: pathToLastIndex(requestIndex),
+  //   newIndex: -1, // being deleted from last folder
+  //   folderPath,
+  //   length: getRequestsByPath(myCollections.value, folderPath).length,
+  // })
+  moveRESTRequest(
+    parentCollectionIndexPath,
+    pathToLastIndex(requestIndex),
+    destinationCollectionIndex
+  )
+
+  toast.success(`${t("request.moved")}`)
+  // draggingToRoot.value = false
 }
 
-const resolveConfirmModal = (title: string | null) => {
-  if (title === `${t("confirm.remove_collection")}`) {
-    onRemoveRootCollection()
-  } else if (title === `${t("confirm.remove_request")}`) {
-    onRemoveRequest()
-  } else if (title === `${t("confirm.remove_folder")}`) {
-    onRemoveChildCollection()
+const dropCollection = (payload: {
+  collectionIndexDragged: string
+  destinationCollectionIndex: string
+}) => {
+  const { collectionIndexDragged, destinationCollectionIndex } = payload
+  if (!collectionIndexDragged || !destinationCollectionIndex) return
+  if (collectionIndexDragged === destinationCollectionIndex) return
+
+  if (
+    checkIfCollectionIsAParentOfTheChildren(
+      collectionIndexDragged,
+      destinationCollectionIndex
+    )
+  ) {
+    toast.error(`${t("team.parent_coll_move")}`)
+    return
+  }
+
+  //check if the collection is being moved to its own parent
+  if (
+    isMoveToSameLocation(collectionIndexDragged, destinationCollectionIndex)
+  ) {
+    return
+  }
+
+  const parentFolder = collectionIndexDragged.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
+  const totalFoldersOfDestinationCollection =
+    getFoldersByPath(restCollectionState.value, destinationCollectionIndex)
+      .length - (parentFolder === destinationCollectionIndex ? 1 : 0)
+
+  moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
+
+  // resolveSaveContextOnCollectionReorder(
+  //   {
+  //     lastIndex: pathToLastIndex(collectionIndexDragged),
+  //     newIndex: -1,
+  //     folderPath: parentFolder,
+  //     length: getFoldersByPath(restCollectionState.value, parentFolder).length,
+  //   },
+  //   "drop"
+  // )
+
+  // updateSaveContextForAffectedRequests(
+  //   collectionIndexDragged,
+  //   `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
+  // )
+
+  const { auth, headers } = cascadeParentCollectionForHeaderAuth(
+    `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
+    "rest"
+  )
+
+  const inheritedProperty = {
+    auth,
+    headers,
+  }
+
+  updateInheritedPropertiesForAffectedRequests(
+    `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
+    inheritedProperty,
+    "rest"
+  )
+
+  // draggingToRoot.value = false
+  toast.success(`${t("collection.moved")}`)
+}
+
+const updateRequestOrder = (
+  dataTransfer: DataTransfer,
+  {
+    parentCollectionIndexPath,
+    requestIndex,
+  }: { parentCollectionIndexPath: string | null; requestIndex: string | null }
+) => {
+  if (!parentCollectionIndexPath) return
+  const dragedRequestIndex = dataTransfer.getData("requestIndex")
+  const destinationRequestIndex = requestIndex
+  const destinationCollectionIndex = parentCollectionIndexPath
+
+  if (
+    !dragedRequestIndex ||
+    !destinationCollectionIndex ||
+    dragedRequestIndex === destinationRequestIndex
+  ) {
+    return
+  }
+
+  if (
+    !isSameSameParent(
+      dragedRequestIndex,
+      destinationRequestIndex,
+      destinationCollectionIndex
+    )
+  ) {
+    toast.error(`${t("collection.different_parent")}`)
   } else {
-    console.error(
-      `Confirm modal title ${title} is not handled by the component`
+    updateRESTRequestOrder(
+      pathToLastIndex(dragedRequestIndex),
+      destinationRequestIndex ? pathToLastIndex(destinationRequestIndex) : null,
+      destinationCollectionIndex
     )
-    toast.error(t("error.something_went_wrong"))
-    displayConfirmModal(false)
+
+    toast.success(`${t("request.order_changed")}`)
   }
 }
 
-const resetSelectedData = () => {
-  editingCollectionIndexPath.value = ""
-  editingRootCollectionName.value = ""
-  editingChildCollectionName.value = ""
-  editingRequestName.value = ""
-  editingRequestIndexPath.value = ""
+const updateCollectionOrder = (
+  dataTransfer: DataTransfer,
+  destinationCollection: {
+    destinationCollectionIndex: string | null
+    destinationCollectionParentIndex: string | null
+  }
+) => {
+  const draggedCollectionIndex = dataTransfer.getData("collectionIndex")
+
+  const { destinationCollectionIndex, destinationCollectionParentIndex } =
+    destinationCollection
+
+  if (
+    !draggedCollectionIndex ||
+    draggedCollectionIndex === destinationCollectionIndex
+  ) {
+    return
+  }
+
+  if (
+    !isSameSameParent(
+      draggedCollectionIndex,
+      destinationCollectionIndex,
+      destinationCollectionParentIndex
+    )
+  ) {
+    toast.error(`${t("collection.different_parent")}`)
+  } else {
+    updateRESTCollectionOrder(
+      draggedCollectionIndex,
+      destinationCollectionIndex
+    )
+    // resolveSaveContextOnCollectionReorder({
+    //   lastIndex: pathToLastIndex(draggedCollectionIndex),
+    //   newIndex: pathToLastIndex(
+    //     destinationCollectionIndex ? destinationCollectionIndex : ""
+    //   ),
+    //   folderPath: draggedCollectionIndex.split("/").slice(0, -1).join("/"),
+    // })
+    toast.success(`${t("collection.order_changed")}`)
+  }
+}
+
+const shareRequest = (request: HoppRESTRequest) => {
+  if (currentUser.value) {
+    // Opens the share request modal if the user is logged in
+    return invokeAction("share.request", { request })
+  }
+
+  // Else, prompts the user to login
+  invokeAction("modals.login.toggle")
 }
 
 /**
- * @param path The path of the collection or request
- * @returns The index of the collection or request
+ * Generic helpers
  */
-const pathToIndex = (path: string) => {
-  const pathArr = path.split("/")
-  return pathArr
+
+/**
+ * Used to check if the collection exist as the parent of the childrens
+ * @param collectionIndexDragged The index of the collection dragged
+ * @param destinationCollectionIndex The index of the destination collection
+ * @returns True if the collection exist as the parent of the childrens
+ */
+const checkIfCollectionIsAParentOfTheChildren = (
+  collectionIndexDragged: string,
+  destinationCollectionIndex: string
+) => {
+  const collectionDraggedPath = pathToIndex(collectionIndexDragged)
+  const destinationCollectionPath = pathToIndex(destinationCollectionIndex)
+
+  if (collectionDraggedPath.length < destinationCollectionPath.length) {
+    const slicedDestinationCollectionPath = destinationCollectionPath.slice(
+      0,
+      collectionDraggedPath.length
+    )
+    if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
+      return true
+    }
+    return false
+  }
+
+  return false
 }
 
 /**
@@ -1242,4 +1590,111 @@ const getRequestIndexPathArgs = (requestIndexPath: string) => {
     requestIndex,
   }
 }
+
+const isMoveToSameLocation = (
+  draggedItemPath: string,
+  destinationPath: string
+) => {
+  const draggedItemPathArr = pathToIndex(draggedItemPath)
+  const destinationPathArr = pathToIndex(destinationPath)
+
+  if (draggedItemPathArr.length > 0) {
+    const draggedItemParentPathArr = draggedItemPathArr.slice(
+      0,
+      draggedItemPathArr.length - 1
+    )
+
+    if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
+      return true
+    }
+    return false
+  }
+}
+
+/**
+ * Used to check if the request/collection is being moved to the same parent since reorder is only allowed within the same parent
+ * @param draggedItem - path index of the dragged request
+ * @param destinationItem - path index of the destination request
+ * @param destinationCollectionIndex -  index of the destination collection
+ * @returns boolean - true if the request is being moved to the same parent
+ */
+const isSameSameParent = (
+  draggedItemPath: string,
+  destinationItemPath: string | null,
+  destinationCollectionIndex: string | null
+) => {
+  const draggedItemIndex = pathToIndex(draggedItemPath)
+
+  // if the destinationItemPath and destinationCollectionIndex is null, it means the request is being moved to the root
+  if (destinationItemPath === null && destinationCollectionIndex === null) {
+    return draggedItemIndex.length === 1
+  } else if (
+    destinationItemPath === null &&
+    destinationCollectionIndex !== null &&
+    draggedItemIndex.length === 1
+  ) {
+    return draggedItemIndex[0] === destinationCollectionIndex
+  } else if (
+    destinationItemPath === null &&
+    draggedItemIndex.length !== 1 &&
+    destinationCollectionIndex !== null
+  ) {
+    const dragedItemParent = draggedItemIndex.slice(0, -1)
+
+    return dragedItemParent.join("/") === destinationCollectionIndex
+  }
+  if (destinationItemPath === null) return false
+  const destinationItemIndex = pathToIndex(destinationItemPath)
+
+  // length of 1 means the request is in the root
+  if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
+    return true
+  } else if (draggedItemIndex.length === destinationItemIndex.length) {
+    const dragedItemParent = draggedItemIndex.slice(0, -1)
+    const destinationItemParent = destinationItemIndex.slice(0, -1)
+    if (isEqual(dragedItemParent, destinationItemParent)) {
+      return true
+    }
+    return false
+  }
+  return false
+}
+
+/**
+ * @param path The path of the collection or request
+ * @returns The index of the collection or request
+ */
+const pathToIndex = (path: string) => {
+  const pathArr = path.split("/")
+  return pathArr
+}
+
+const pathToLastIndex = (path: string) => {
+  const pathArr = pathToIndex(path)
+  return parseInt(pathArr[pathArr.length - 1])
+}
+
+const resolveConfirmModal = (title: string | null) => {
+  if (title === `${t("confirm.remove_collection")}`) {
+    onRemoveRootCollection()
+  } else if (title === `${t("confirm.remove_request")}`) {
+    onRemoveRequest()
+  } else if (title === `${t("confirm.remove_folder")}`) {
+    onRemoveChildCollection()
+  } else {
+    console.error(
+      `Confirm modal title ${title} is not handled by the component`
+    )
+    toast.error(t("error.something_went_wrong"))
+    displayConfirmModal(false)
+  }
+}
+
+const resetSelectedData = () => {
+  editingCollectionIndexPath.value = ""
+  editingRootCollectionName.value = ""
+  editingChildCollectionName.value = ""
+  editingRequestName.value = ""
+  editingRequestIndexPath.value = ""
+}
 </script>

+ 13 - 0
packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts

@@ -656,7 +656,12 @@ export class PersonalWorkspaceProviderService
                       type: "collection",
                       value: {
                         collectionID: `${collectionID}/${id}`,
+                        isLastItem:
+                          item.folders?.length > 1
+                            ? id === item.folders.length - 1
+                            : false,
                         name: childColl.name,
+                        parentCollectionID: collectionID,
                       },
                     }
                   })
@@ -665,6 +670,11 @@ export class PersonalWorkspaceProviderService
                     return <RESTCollectionViewItem>{
                       type: "request",
                       value: {
+                        isLastItem:
+                          item.requests?.length > 1
+                            ? id === item.requests.length - 1
+                            : false,
+                        parentCollectionID: collectionID,
                         requestID: `${collectionID}/${id}`,
                         request: req,
                       },
@@ -711,7 +721,10 @@ export class PersonalWorkspaceProviderService
                 return this.restCollectionState.value.state.map((coll, id) => {
                   return {
                     collectionID: id.toString(),
+                    isLastItem:
+                      id === this.restCollectionState.value.state.length - 1,
                     name: coll.name,
+                    parentIndex: null,
                   }
                 })
               }),

+ 4 - 0
packages/hoppscotch-common/src/services/new-workspace/view.ts

@@ -10,7 +10,9 @@ export type RESTCollectionLevelAuthHeadersView = {
 export type RESTCollectionViewCollection = {
   collectionID: string
 
+  isLastItem: boolean
   name: string
+  parentCollectionID: string | null
 }
 
 export type RESTCollectionViewRequest = {
@@ -18,6 +20,8 @@ export type RESTCollectionViewRequest = {
   requestID: string
 
   request: HoppRESTRequest
+  isLastItem: boolean
+  parentCollectionID: string | null
 }
 
 export type RESTCollectionViewItem =