Browse Source

feat: rest revamp (#2918)

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Anwarul Islam 1 year ago
parent
commit
defece95fc

+ 2 - 0
packages/hoppscotch-common/locales/en.json

@@ -138,6 +138,7 @@
   },
   "confirm": {
     "exit_team": "Are you sure you want to leave this team?",
+    "save_unsaved_tab": "Do you want to save changes made in this tab ?",
     "logout": "Are you sure you want to logout?",
     "remove_collection": "Are you sure you want to permanently delete this collection?",
     "remove_environment": "Are you sure you want to permanently delete this environment?",
@@ -317,6 +318,7 @@
   "modal": {
     "collections": "Collections",
     "confirm": "Confirm",
+    "close_unsaved_tab": "Close Unsaved Tab ?",
     "edit_request": "Edit Request",
     "import_export": "Import / Export"
   },

+ 5 - 0
packages/hoppscotch-common/src/components.d.ts

@@ -89,7 +89,11 @@ declare module '@vue/runtime-core' {
     HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
     HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
     HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
+    HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
+    HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
     HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
+    HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
+    HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
     HttpBody: typeof import('./components/http/Body.vue')['default']
     HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
     HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
@@ -102,6 +106,7 @@ declare module '@vue/runtime-core' {
     HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
     HttpRequest: typeof import('./components/http/Request.vue')['default']
     HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
+    HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
     HttpResponse: typeof import('./components/http/Response.vue')['default']
     HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
     HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']

+ 2 - 0
packages/hoppscotch-common/src/components/app/PaneLayout.vue

@@ -22,6 +22,7 @@
           <slot name="primary" />
         </Pane>
         <Pane
+          v-if="hasSecondary"
           :size="PANE_MAIN_BOTTOM_SIZE"
           class="flex flex-col !overflow-auto"
         >
@@ -62,6 +63,7 @@ const SIDEBAR = useSetting("SIDEBAR")
 const slots = useSlots()
 
 const hasSidebar = computed(() => !!slots.sidebar)
+const hasSecondary = computed(() => !!slots.secondary)
 
 const props = defineProps({
   layoutId: {

+ 2 - 2
packages/hoppscotch-common/src/components/collections/AddRequest.vue

@@ -43,7 +43,7 @@
 import { ref, watch } from "vue"
 import { useI18n } from "@composables/i18n"
 import { useToast } from "@composables/toast"
-import { getRESTRequest } from "~/newstore/RESTSession"
+import { currentActiveTab } from "~/helpers/rest/tab"
 
 const toast = useToast()
 const t = useI18n()
@@ -70,7 +70,7 @@ watch(
   () => props.show,
   (show) => {
     if (show) {
-      name.value = getRESTRequest().name
+      name.value = currentActiveTab.value.document.request.name
     }
   }
 )

+ 1 - 1
packages/hoppscotch-common/src/components/collections/Collection.vue

@@ -203,7 +203,7 @@ const props = defineProps({
   parentID: {
     type: String as PropType<string | null>,
     default: null,
-    required: true,
+    required: false,
   },
   data: {
     type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,

+ 18 - 20
packages/hoppscotch-common/src/components/collections/MyCollections.vue

@@ -298,11 +298,10 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
 import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
 import { useI18n } from "@composables/i18n"
 import { useColorMode } from "@composables/theming"
-import { useReadonlyStream } from "~/composables/stream"
-import { restSaveContext$ } from "~/newstore/RESTSession"
 import { pipe } from "fp-ts/function"
 import * as O from "fp-ts/Option"
 import { Picked } from "~/helpers/types/HoppPicked.js"
+import { currentActiveTab } from "~/helpers/rest/tab"
 
 export type Collection = {
   type: "collections"
@@ -508,23 +507,21 @@ const isSelected = computed(() => {
   }
 })
 
-const active = useReadonlyStream(restSaveContext$, null)
-
-const isActiveRequest = computed(() => {
-  return (folderPath: string, requestIndex: number) => {
-    return pipe(
-      active.value,
-      O.fromNullable,
-      O.filter(
-        (active) =>
-          active.originLocation === "user-collection" &&
-          active.folderPath === folderPath &&
-          active.requestIndex === requestIndex
-      ),
-      O.isSome
-    )
-  }
-})
+const active = computed(() => currentActiveTab.value.document.saveContext)
+
+const isActiveRequest = (folderPath: string, requestIndex: number) => {
+  return pipe(
+    active.value,
+    O.fromNullable,
+    O.filter(
+      (active) =>
+        active.originLocation === "user-collection" &&
+        active.folderPath === folderPath &&
+        active.requestIndex === requestIndex
+    ),
+    O.isSome
+  )
+}
 
 const selectRequest = (data: {
   request: HoppRESTRequest
@@ -532,6 +529,7 @@ const selectRequest = (data: {
   requestIndex: string
 }) => {
   const { request, folderPath, requestIndex } = data
+
   if (props.saveRequest) {
     emit("select", {
       pickedType: "my-request",
@@ -543,7 +541,7 @@ const selectRequest = (data: {
       request,
       folderPath,
       requestIndex,
-      isActive: isActiveRequest.value(folderPath, parseInt(requestIndex)),
+      isActive: isActiveRequest(folderPath, parseInt(requestIndex)),
     })
   }
 }

+ 2 - 16
packages/hoppscotch-common/src/components/collections/Request.vue

@@ -152,14 +152,12 @@ import { ref, PropType, watch, computed } from "vue"
 import { HoppRESTRequest } from "@hoppscotch/data"
 import { useI18n } from "@composables/i18n"
 import { TippyComponent } from "vue-tippy"
-import { pipe } from "fp-ts/function"
-import * as RR from "fp-ts/ReadonlyRecord"
-import * as O from "fp-ts/Option"
 import {
   changeCurrentReorderStatus,
   currentReorderingStatus$,
 } from "~/newstore/reordering"
 import { useReadonlyStream } from "~/composables/stream"
+import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
 
 type CollectionType = "my-collections" | "team-collections"
 
@@ -242,20 +240,8 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
   parentID: "",
 })
 
-const requestMethodLabels = {
-  get: "text-green-500",
-  post: "text-yellow-500",
-  put: "text-blue-500",
-  delete: "text-red-500",
-  default: "text-gray-500",
-} as const
-
 const requestLabelColor = computed(() =>
-  pipe(
-    requestMethodLabels,
-    RR.lookup(props.request.method.toLowerCase()),
-    O.getOrElseW(() => requestMethodLabels.default)
-  )
+  getMethodLabelColorClassOf(props.request)
 )
 
 watch(

+ 53 - 37
packages/hoppscotch-common/src/components/collections/SaveRequest.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable prettier/prettier -->
 <template>
   <HoppSmartModal
     v-if="show"
@@ -61,8 +62,8 @@
 </template>
 
 <script setup lang="ts">
-import { useI18n } from "@composables/i18n"
-import { useToast } from "@composables/toast"
+import { reactive, ref, watch } from "vue"
+import { cloneDeep } from "lodash-es"
 import {
   HoppGQLRequest,
   HoppRESTRequest,
@@ -70,8 +71,6 @@ import {
 } from "@hoppscotch/data"
 import { pipe } from "fp-ts/function"
 import * as TE from "fp-ts/TaskEither"
-import { cloneDeep } from "lodash-es"
-import { reactive, ref, watch } from "vue"
 import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
 import {
   createRequestInCollection,
@@ -79,11 +78,8 @@ import {
 } from "~/helpers/backend/mutations/TeamRequest"
 import { Picked } from "~/helpers/types/HoppPicked"
 import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
-import {
-  getRESTRequest,
-  setRESTSaveContext,
-  useRESTRequestName,
-} from "~/newstore/RESTSession"
+import { useI18n } from "@composables/i18n"
+import { useToast } from "@composables/toast"
 import {
   editGraphqlRequest,
   editRESTRequest,
@@ -91,6 +87,8 @@ import {
   saveRESTRequestAs,
 } from "~/newstore/collections"
 import { GQLError } from "~/helpers/backend/GQLClient"
+import { computedWithControl } from "@vueuse/core"
+import { currentActiveTab } from "~/helpers/rest/tab"
 
 const t = useI18n()
 const toast = useToast()
@@ -127,8 +125,13 @@ const emit = defineEmits<{
   (e: "hide-modal"): void
 }>()
 
-const requestName = ref(
-  props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
+const gqlRequestName = useGQLRequestName()
+const requestName = computedWithControl(
+  () => [currentActiveTab.value, gqlRequestName.value],
+  () =>
+    props.mode === "rest"
+      ? currentActiveTab.value.document.request.name
+      : gqlRequestName.value
 )
 
 const requestData = reactive({
@@ -186,7 +189,7 @@ const saveRequestAs = async () => {
 
   const requestUpdated =
     props.mode === "rest"
-      ? cloneDeep(getRESTRequest())
+      ? cloneDeep(currentActiveTab.value.document.request)
       : cloneDeep(getGQLSession().request)
 
   if (picked.value.pickedType === "my-collection") {
@@ -198,12 +201,15 @@ const saveRequestAs = async () => {
       requestUpdated
     )
 
-    setRESTSaveContext({
-      originLocation: "user-collection",
-      folderPath: `${picked.value.collectionIndex}`,
-      requestIndex: insertionIndex,
-      req: requestUpdated,
-    })
+    currentActiveTab.value.document = {
+      request: requestUpdated,
+      isDirty: false,
+      saveContext: {
+        originLocation: "user-collection",
+        folderPath: `${picked.value.collectionIndex}`,
+        requestIndex: insertionIndex,
+      },
+    }
 
     requestSaved()
   } else if (picked.value.pickedType === "my-folder") {
@@ -215,12 +221,15 @@ const saveRequestAs = async () => {
       requestUpdated
     )
 
-    setRESTSaveContext({
-      originLocation: "user-collection",
-      folderPath: picked.value.folderPath,
-      requestIndex: insertionIndex,
-      req: requestUpdated,
-    })
+    currentActiveTab.value.document = {
+      request: requestUpdated,
+      isDirty: false,
+      saveContext: {
+        originLocation: "user-collection",
+        folderPath: picked.value.folderPath,
+        requestIndex: insertionIndex,
+      },
+    }
 
     requestSaved()
   } else if (picked.value.pickedType === "my-request") {
@@ -233,12 +242,15 @@ const saveRequestAs = async () => {
       requestUpdated
     )
 
-    setRESTSaveContext({
-      originLocation: "user-collection",
-      folderPath: picked.value.folderPath,
-      requestIndex: picked.value.requestIndex,
-      req: requestUpdated,
-    })
+    currentActiveTab.value.document = {
+      request: requestUpdated,
+      isDirty: false,
+      saveContext: {
+        originLocation: "user-collection",
+        folderPath: picked.value.folderPath,
+        requestIndex: picked.value.requestIndex,
+      },
+    }
 
     requestSaved()
   } else if (picked.value.pickedType === "teams-collection") {
@@ -341,13 +353,17 @@ const updateTeamCollectionOrFolder = (
       (result) => {
         const { createRequestInCollection } = result
 
-        setRESTSaveContext({
-          originLocation: "team-collection",
-          requestID: createRequestInCollection.id,
-          collectionID: createRequestInCollection.collection.id,
-          teamID: createRequestInCollection.collection.team.id,
-          req: requestUpdated,
-        })
+        currentActiveTab.value.document = {
+          request: requestUpdated,
+          isDirty: false,
+          saveContext: {
+            originLocation: "team-collection",
+            requestID: createRequestInCollection.id,
+            collectionID: createRequestInCollection.collection.id,
+            teamID: createRequestInCollection.collection.team.id,
+          },
+        }
+
         modalLoadingState.value = false
         requestSaved()
       }

+ 2 - 3
packages/hoppscotch-common/src/components/collections/TeamCollections.vue

@@ -316,11 +316,10 @@ import { TeamRequest } from "~/helpers/teams/TeamRequest"
 import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
 import { cloneDeep } from "lodash-es"
 import { HoppRESTRequest } from "@hoppscotch/data"
-import { useReadonlyStream } from "~/composables/stream"
-import { restSaveContext$ } from "~/newstore/RESTSession"
 import { pipe } from "fp-ts/function"
 import * as O from "fp-ts/Option"
 import { Picked } from "~/helpers/types/HoppPicked.js"
+import { currentActiveTab } from "~/helpers/rest/tab"
 
 const t = useI18n()
 const colorMode = useColorMode()
@@ -520,7 +519,7 @@ const isSelected = computed(() => {
   }
 })
 
-const active = useReadonlyStream(restSaveContext$, null)
+const active = computed(() => currentActiveTab.value.document.saveContext)
 
 const isActiveRequest = computed(() => {
   return (requestID: string) => {

+ 209 - 216
packages/hoppscotch-common/src/components/collections/index.vue

@@ -146,18 +146,6 @@
       @import-to-teams="importToTeams"
       @hide-modal="displayModalImportExport(false)"
     />
-    <HttpReqChangeConfirmModal
-      :show="confirmChangeToRequest"
-      :loading="modalLoadingState"
-      @hide-modal="confirmChangeToRequest = false"
-      @save-change="saveRequestChange"
-      @discard-change="discardRequestChange"
-    />
-    <CollectionsSaveRequest
-      mode="rest"
-      :show="showSaveRequestModal"
-      @hide-modal="showSaveRequestModal = false"
-    />
     <TeamsAdd
       :show="showTeamModalAdd"
       @hide-modal="displayTeamModalAdd(false)"
@@ -166,7 +154,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, PropType, reactive, ref, watch } from "vue"
+import { computed, PropType, ref, watch } from "vue"
 import { useToast } from "@composables/toast"
 import { useI18n } from "@composables/i18n"
 import { Picked } from "~/helpers/types/HoppPicked"
@@ -183,7 +171,6 @@ import {
   editRESTCollection,
   editRESTFolder,
   editRESTRequest,
-  moveRESTFolder,
   moveRESTRequest,
   removeRESTCollection,
   removeRESTFolder,
@@ -192,23 +179,14 @@ import {
   saveRESTRequestAs,
   updateRESTRequestOrder,
   updateRESTCollectionOrder,
+  moveRESTFolder,
 } from "~/newstore/collections"
 import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
 import {
   HoppCollection,
   HoppRESTRequest,
-  isEqualHoppRESTRequest,
   makeCollection,
-  safelyExtractRESTRequest,
-  translateToNewRequest,
 } from "@hoppscotch/data"
-import {
-  getDefaultRESTRequest,
-  getRESTRequest,
-  getRESTSaveContext,
-  setRESTRequest,
-  setRESTSaveContext,
-} from "~/newstore/RESTSession"
 import { cloneDeep, isEqual } from "lodash-es"
 import { GQLError } from "~/helpers/backend/GQLClient"
 import {
@@ -234,12 +212,26 @@ import {
   getTeamCollectionJSON,
   teamCollToHoppRESTColl,
 } from "~/helpers/backend/helpers"
-import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
 import * as E from "fp-ts/Either"
 import { platform } from "~/platform"
 import { createCollectionGists } from "~/helpers/gist"
 import { workspaceStatus$ } from "~/newstore/workspace"
 import IconListEnd from "~icons/lucide/list-end"
+import {
+  createNewTab,
+  currentActiveTab,
+  currentTabID,
+  getTabRefWithSaveContext,
+} from "~/helpers/rest/tab"
+import {
+  getRequestsByPath,
+  resolveSaveContextOnRequestReorder,
+} from "~/helpers/collection/request"
+import {
+  getFoldersByPath,
+  resolveSaveContextOnCollectionReorder,
+  updateSaveContextForAffectedRequests,
+} from "~/helpers/collection/collection"
 
 const t = useI18n()
 const toast = useToast()
@@ -314,15 +306,6 @@ const exportingTeamCollections = ref(false)
 const creatingGistCollection = ref(false)
 const importingMyCollections = ref(false)
 
-// Confirm Change to request modal
-const confirmChangeToRequest = ref(false)
-const showSaveRequestModal = ref(false)
-const clickedRequest = reactive({
-  folderPath: "" as string | undefined,
-  requestIndex: null as string | null,
-  request: null as HoppRESTRequest | null,
-})
-
 // TeamList-Adapter
 const teamListAdapter = new TeamListAdapter(true)
 const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
@@ -637,7 +620,7 @@ const addRequest = (payload: {
 
 const onAddRequest = (requestName: string) => {
   const newRequest = {
-    ...cloneDeep(getRESTRequest()),
+    ...cloneDeep(currentActiveTab.value.document.request),
     name: requestName,
   }
 
@@ -646,10 +629,14 @@ const onAddRequest = (requestName: string) => {
     if (!path) return
     const insertionIndex = saveRESTRequestAs(path, newRequest)
 
-    setRESTRequest(newRequest, {
-      originLocation: "user-collection",
-      folderPath: path,
-      requestIndex: insertionIndex,
+    createNewTab({
+      request: newRequest,
+      isDirty: false,
+      saveContext: {
+        originLocation: "user-collection",
+        folderPath: path,
+        requestIndex: insertionIndex,
+      },
     })
 
     displayModalAddRequest(false)
@@ -677,12 +664,17 @@ const onAddRequest = (requestName: string) => {
         (result) => {
           const { createRequestInCollection } = result
 
-          setRESTRequest(newRequest, {
-            originLocation: "team-collection",
-            requestID: createRequestInCollection.id,
-            collectionID: createRequestInCollection.collection.id,
-            teamID: createRequestInCollection.collection.team.id,
+          createNewTab({
+            request: newRequest,
+            isDirty: false,
+            saveContext: {
+              originLocation: "team-collection",
+              requestID: createRequestInCollection.id,
+              collectionID: createRequestInCollection.collection.id,
+              teamID: createRequestInCollection.collection.team.id,
+            },
           })
+
           modalLoadingState.value = false
           displayModalAddRequest(false)
         }
@@ -873,27 +865,22 @@ const updateEditingRequest = (newName: string) => {
     ...request,
     name: newName || request.name,
   }
-
-  const saveCtx = getRESTSaveContext()
-
   if (collectionsType.value.type === "my-collections") {
     const folderPath = editingFolderPath.value
     const requestIndex = editingRequestIndex.value
 
     if (folderPath === null || requestIndex === null) return
 
+    const possibleActiveTab = getTabRefWithSaveContext({
+      originLocation: "user-collection",
+      requestIndex,
+      folderPath,
+    })
+
     editRESTRequest(folderPath, requestIndex, requestUpdated)
 
-    if (
-      saveCtx &&
-      saveCtx.originLocation === "user-collection" &&
-      saveCtx.requestIndex === editingRequestIndex.value &&
-      saveCtx.folderPath === editingFolderPath.value
-    ) {
-      setRESTRequest({
-        ...getRESTRequest(),
-        name: requestUpdated.name,
-      })
+    if (possibleActiveTab) {
+      possibleActiveTab.value.document.request.name = requestUpdated.name
     }
 
     displayModalEditRequest(false)
@@ -925,15 +912,13 @@ const updateEditingRequest = (newName: string) => {
       )
     )()
 
-    if (
-      saveCtx &&
-      saveCtx.originLocation === "team-collection" &&
-      saveCtx.requestID === editingRequestID.value
-    ) {
-      setRESTRequest({
-        ...getRESTRequest(),
-        name: requestName,
-      })
+    const possibleTab = getTabRefWithSaveContext({
+      originLocation: "team-collection",
+      requestID,
+    })
+
+    if (possibleTab) {
+      possibleTab.value.document.request.name = requestName
     }
   }
 }
@@ -1030,6 +1015,13 @@ const onRemoveCollection = () => {
 
     removeRESTCollection(collectionIndex)
 
+    resolveSaveContextOnCollectionReorder({
+      lastIndex: collectionIndex,
+      newIndex: -1,
+      folderPath: "", // root folder
+      length: myCollections.value.length,
+    })
+
     toast.success(t("state.deleted"))
     displayConfirmModal(false)
   } else if (hasTeamWriteAccess.value) {
@@ -1074,6 +1066,14 @@ const onRemoveFolder = () => {
 
     removeRESTFolder(folderPath)
 
+    const parentFolder = folderPath.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
+    resolveSaveContextOnCollectionReorder({
+      lastIndex: pathToLastIndex(folderPath),
+      newIndex: -1,
+      folderPath: parentFolder,
+      length: getFoldersByPath(myCollections.value, parentFolder).length,
+    })
+
     toast.success(t("state.deleted"))
     displayConfirmModal(false)
   } else if (hasTeamWriteAccess.value) {
@@ -1124,8 +1124,28 @@ const onRemoveRequest = () => {
       emit("select", null)
     }
 
+    const possibleTab = getTabRefWithSaveContext({
+      originLocation: "user-collection",
+      folderPath,
+      requestIndex,
+    })
+
+    // If there is a tab attached to this request, dissociate its state and mark it dirty
+    if (possibleTab) {
+      possibleTab.value.document.saveContext = null
+      possibleTab.value.document.isDirty = true
+    }
+
     removeRESTRequest(folderPath, requestIndex)
 
+    // the same function is used to reorder requests since after removing, it's basically doing reorder
+    resolveSaveContextOnRequestReorder({
+      lastIndex: requestIndex,
+      newIndex: -1,
+      folderPath,
+      length: getRequestsByPath(myCollections.value, folderPath).length,
+    })
+
     toast.success(t("state.deleted"))
     displayConfirmModal(false)
   } else if (hasTeamWriteAccess.value) {
@@ -1157,6 +1177,17 @@ const onRemoveRequest = () => {
         }
       )
     )()
+
+    // If there is a tab attached to this request, dissociate its state and mark it dirty
+    const possibleTab = getTabRefWithSaveContext({
+      originLocation: "team-collection",
+      requestID,
+    })
+
+    if (possibleTab) {
+      possibleTab.value.document.saveContext = undefined
+      possibleTab.value.document.isDirty = true
+    }
   }
 }
 
@@ -1165,41 +1196,6 @@ const selectPicked = (payload: Picked | null) => {
   emit("select", payload)
 }
 
-// select request change modal functions
-const noChangeSetRESTRequest = () => {
-  const folderPath = clickedRequest.folderPath
-  const requestIndex = clickedRequest.requestIndex
-  const request = clickedRequest.request
-
-  let newContext: HoppRequestSaveContext | null = null
-  if (collectionsType.value.type === "my-collections") {
-    if (!folderPath || !requestIndex || !request) return
-
-    newContext = {
-      originLocation: "user-collection",
-      requestIndex: parseInt(requestIndex),
-      folderPath,
-      req: cloneDeep(request),
-    }
-  } else if (collectionsType.value.type === "team-collections") {
-    if (!requestIndex || !request) return
-    newContext = {
-      originLocation: "team-collection",
-      requestID: requestIndex,
-      req: cloneDeep(request),
-    }
-  }
-  setRESTRequest(
-    cloneDeep(
-      safelyExtractRESTRequest(
-        translateToNewRequest(request),
-        getDefaultRESTRequest()
-      )
-    ),
-    newContext
-  )
-}
-
 /**
  * This function is called when the user clicks on a request
  * @param selectedRequest The request that the user clicked on emited from the collection tree
@@ -1210,131 +1206,51 @@ const selectRequest = (selectedRequest: {
   requestIndex: string
   isActive: boolean
 }) => {
-  const { request, folderPath, requestIndex, isActive } = selectedRequest
-  // If the request is already active, then we reset the save context
-  if (isActive) {
-    setRESTSaveContext(null)
-    return
-  }
+  const { request, folderPath, requestIndex } = selectedRequest
 
-  const currentRESTRequest = getRESTRequest()
+  // If there is a request with this save context, switch into it
+  let possibleTab = null
 
-  const currentRESTSaveContext = getRESTSaveContext()
-
-  clickedRequest.folderPath = folderPath
-  clickedRequest.requestIndex = requestIndex
-  clickedRequest.request = request
-
-  // If there is no active context,
-  if (!currentRESTSaveContext) {
-    // Check if the use is clicking on the same request
-    if (isEqualHoppRESTRequest(currentRESTRequest, request)) {
-      noChangeSetRESTRequest()
+  if (collectionsType.value.type === "team-collections") {
+    possibleTab = getTabRefWithSaveContext({
+      originLocation: "team-collection",
+      requestID: requestIndex,
+    })
+    if (possibleTab) {
+      currentTabID.value = possibleTab.value.id
     } else {
-      // can show the save change modal here since there is change in the request
-      // and the user is clicking on the different request
-      // and currently we dont have any active context
-
-      confirmChangeToRequest.value = true
+      createNewTab({
+        request: cloneDeep(request),
+        isDirty: false,
+        saveContext: {
+          originLocation: "team-collection",
+          requestID: requestIndex,
+        },
+      })
     }
   } else {
-    if (isEqualHoppRESTRequest(currentRESTRequest, request)) {
-      noChangeSetRESTRequest()
+    possibleTab = getTabRefWithSaveContext({
+      originLocation: "user-collection",
+      requestIndex: parseInt(requestIndex),
+      folderPath: folderPath!,
+    })
+    if (possibleTab) {
+      currentTabID.value = possibleTab.value.id
     } else {
-      const currentReqWithNoChange = currentRESTSaveContext.req
-      // now we compare the current request
-      // with the request inside the active context
-      if (
-        currentReqWithNoChange &&
-        isEqualHoppRESTRequest(currentReqWithNoChange, currentRESTRequest)
-      ) {
-        noChangeSetRESTRequest()
-      } else {
-        // there is change in the request
-        // so we can show the save change modal here
-        confirmChangeToRequest.value = true
-      }
-    }
-  }
-}
-
-/**
- * This function is called when the user clicks on the save button in the confirm change modal
- * There are two cases
- * 1. There is no active context
- * 2. There is active context
- * In the first case, we can show the save request as modal and user can select the location to save the request
- * In the second case, we can save the request in the same location and update the request
- */
-const saveRequestChange = () => {
-  const currentRESTSaveContext = getRESTSaveContext()
-
-  if (!currentRESTSaveContext) {
-    showSaveRequestModal.value = true
-    confirmChangeToRequest.value = false
-    return
-  }
-
-  const currentRESTRequest = getRESTRequest()
-
-  if (currentRESTSaveContext.originLocation === "user-collection") {
-    const folderPath = currentRESTSaveContext.folderPath
-    const requestIndex = currentRESTSaveContext.requestIndex
-
-    editRESTRequest(folderPath, requestIndex, currentRESTRequest)
-
-    // after saving the request, we need to change the context
-    // to the new request (clicked request)
-    noChangeSetRESTRequest()
-
-    toast.success(`${t("request.saved")}`)
-    confirmChangeToRequest.value = false
-  } else {
-    modalLoadingState.value = true
-
-    const requestID = currentRESTSaveContext.requestID
-
-    const data = {
-      request: JSON.stringify(currentRESTRequest),
-      title: currentRESTRequest.name,
-    }
-
-    pipe(
-      updateTeamRequest(requestID, data),
-      TE.match(
-        (err: GQLError<string>) => {
-          toast.error(`${getErrorMessage(err)}`)
-          confirmChangeToRequest.value = false
-          showSaveRequestModal.value = true
-          modalLoadingState.value = false
+      // If not, open the request in a new tab
+      createNewTab({
+        request: cloneDeep(request),
+        isDirty: false,
+        saveContext: {
+          originLocation: "user-collection",
+          folderPath: folderPath!,
+          requestIndex: parseInt(requestIndex),
         },
-        () => {
-          toast.success(`${t("request.saved")}`)
-          modalLoadingState.value = false
-          confirmChangeToRequest.value = false
-
-          const clickedRequestID = clickedRequest.requestIndex
-
-          if (!clickedRequestID) return
-
-          noChangeSetRESTRequest()
-        }
-      )
-    )()
+      })
+    }
   }
 }
 
-/**
- * This function is called when the user clicks on the
- * don't save button in the confirm change modal
- * This function will change the request to the clicked request
- * without saving the changes
- */
-const discardRequestChange = () => {
-  noChangeSetRESTRequest()
-  confirmChangeToRequest.value = false
-}
-
 /**
  * Used to get the index of the request from the path
  * @param path The path of the request
@@ -1355,13 +1271,42 @@ const dropRequest = (payload: {
   destinationCollectionIndex: string
 }) => {
   const { folderPath, requestIndex, destinationCollectionIndex } = payload
+
   if (!requestIndex || !destinationCollectionIndex) return
+
   if (collectionsType.value.type === "my-collections" && folderPath) {
     moveRESTRequest(
       folderPath,
       pathToLastIndex(requestIndex),
       destinationCollectionIndex
     )
+
+    const possibleTab = 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(
+          myCollections.value,
+          destinationCollectionIndex
+        ).length,
+      }
+    }
+
+    // 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,
+    })
+
     toast.success(`${t("request.moved")}`)
     draggingToRoot.value = false
   } else if (hasTeamWriteAccess.value) {
@@ -1384,6 +1329,18 @@ const dropRequest = (payload: {
             requestMoveLoading.value.indexOf(requestIndex),
             1
           )
+
+          const possibleTab = getTabRefWithSaveContext({
+            originLocation: "team-collection",
+            requestID: requestIndex,
+          })
+
+          if (possibleTab) {
+            possibleTab.value.document.saveContext = {
+              originLocation: "team-collection",
+              requestID: requestIndex,
+            }
+          }
           toast.success(`${t("request.moved")}`)
         }
       )
@@ -1440,6 +1397,7 @@ const dropCollection = (payload: {
   const { collectionIndexDragged, destinationCollectionIndex } = payload
   if (!collectionIndexDragged || !destinationCollectionIndex) return
   if (collectionIndexDragged === destinationCollectionIndex) return
+
   if (collectionsType.value.type === "my-collections") {
     if (
       checkIfCollectionIsAParentOfTheChildren(
@@ -1450,7 +1408,32 @@ const dropCollection = (payload: {
       toast.error(`${t("team.parent_coll_move")}`)
       return
     }
+
+    const parentFolder = collectionIndexDragged
+      .split("/")
+      .slice(0, -1)
+      .join("/") // remove last folder to get parent folder
+    const totalFoldersOfDestinationCollection =
+      getFoldersByPath(myCollections.value, destinationCollectionIndex).length -
+      (parentFolder === destinationCollectionIndex ? 1 : 0)
+
     moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
+
+    resolveSaveContextOnCollectionReorder(
+      {
+        lastIndex: pathToLastIndex(collectionIndexDragged),
+        newIndex: -1,
+        folderPath: parentFolder,
+        length: getFoldersByPath(myCollections.value, parentFolder).length,
+      },
+      "drop"
+    )
+
+    updateSaveContextForAffectedRequests(
+      collectionIndexDragged,
+      `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
+    )
+
     draggingToRoot.value = false
     toast.success(`${t("collection.moved")}`)
   } else if (hasTeamWriteAccess.value) {
@@ -1600,6 +1583,11 @@ const updateRequestOrder = (payload: {
         pathToLastIndex(destinationRequestIndex),
         destinationCollectionIndex
       )
+      resolveSaveContextOnRequestReorder({
+        lastIndex: pathToLastIndex(dragedRequestIndex),
+        newIndex: pathToLastIndex(destinationRequestIndex),
+        folderPath: destinationCollectionIndex,
+      })
       toast.success(`${t("request.order_changed")}`)
     }
   } else if (hasTeamWriteAccess.value) {
@@ -1654,6 +1642,11 @@ const updateCollectionOrder = (payload: {
         dragedCollectionIndex,
         destinationCollectionIndex
       )
+      resolveSaveContextOnCollectionReorder({
+        lastIndex: pathToLastIndex(dragedCollectionIndex),
+        newIndex: pathToLastIndex(destinationCollectionIndex),
+        folderPath: dragedCollectionIndex.split("/").slice(0, -1).join("/"),
+      })
       toast.success(`${t("collection.order_changed")}`)
     }
   } else if (hasTeamWriteAccess.value) {

Some files were not shown because too many files changed in this diff