Browse Source

refactor: port import/export functionality

jamesgeorge007 1 year ago
parent
commit
24cefe6c6f

+ 55 - 30
packages/hoppscotch-common/src/components/collections/ImportExport.vue

@@ -30,7 +30,7 @@ import { defineStep } from "~/composables/step-components"
 
 import { useI18n } from "~/composables/i18n"
 import { useToast } from "~/composables/toast"
-import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
+import { restCollections$ } from "~/newstore/collections"
 import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
 
 import IconFolderPlus from "~icons/lucide/folder-plus"
@@ -47,13 +47,14 @@ import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
 
 import { platform } from "~/platform"
 
-import { initializeDownloadCollection } from "~/helpers/import-export/export"
+import { initializeDownloadFile } from "~/helpers/import-export/export"
 import { gistExporter } from "~/helpers/import-export/export/gist"
-import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
 import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
 
 import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
 import { ImporterOrExporter } from "~/components/importExport/types"
+import { useService } from "dioc/vue"
+import { NewWorkspaceService } from "~/services/new-workspace"
 import { TeamWorkspace } from "~/services/workspace.service"
 
 const t = useI18n()
@@ -84,15 +85,43 @@ const currentUser = useReadonlyStream(
 
 const myCollections = useReadonlyStream(restCollections$, [])
 
+const workspaceService = useService(NewWorkspaceService)
+
+const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle
+
 const showImportFailedError = () => {
   toast.error(t("import.failed"))
 }
 
 const handleImportToStore = async (collections: HoppCollection[]) => {
-  const importResult =
-    props.collectionsType.type === "my-collections"
-      ? await importToPersonalWorkspace(collections)
-      : await importToTeamsWorkspace(collections)
+  if (props.collectionsType.type === "my-collections") {
+    if (!activeWorkspaceHandle.value) {
+      return
+    }
+
+    const collectionHandleResult = await workspaceService.importRESTCollections(
+      activeWorkspaceHandle.value,
+      collections
+    )
+
+    if (E.isLeft(collectionHandleResult)) {
+      // INVALID_WORKSPACE_HANDLE
+      return toast.error(t("import.failed"))
+    }
+
+    const resultHandle = collectionHandleResult.right
+
+    if (resultHandle.value.type === "invalid") {
+      // WORKSPACE_INVALIDATED
+    }
+
+    toast.success(t("state.file_imported"))
+    emit("hide-modal")
+
+    return
+  }
+
+  const importResult = await importToTeamsWorkspace(collections)
 
   if (E.isRight(importResult)) {
     toast.success(t("state.file_imported"))
@@ -102,13 +131,6 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
   }
 }
 
-const importToPersonalWorkspace = (collections: HoppCollection[]) => {
-  appendRESTCollections(collections)
-  return E.right({
-    success: true,
-  })
-}
-
 function translateToTeamCollectionFormat(x: HoppCollection) {
   const folders: HoppCollection[] = (x.folders ?? []).map(
     translateToTeamCollectionFormat
@@ -388,28 +410,34 @@ const HoppMyCollectionsExporter: ImporterOrExporter = {
     applicableTo: ["personal-workspace"],
     isLoading: isHoppMyCollectionExporterInProgress,
   },
-  action: () => {
+  action: async () => {
     if (!myCollections.value.length) {
       return toast.error(t("error.no_collections_to_export"))
     }
 
+    if (!activeWorkspaceHandle.value) {
+      return
+    }
+
     isHoppMyCollectionExporterInProgress.value = true
 
-    const message = initializeDownloadCollection(
-      myCollectionsExporter(myCollections.value),
-      "Collections"
+    const result = await workspaceService.exportRESTCollections(
+      activeWorkspaceHandle.value,
+      myCollections.value
     )
 
-    if (E.isRight(message)) {
-      toast.success(t(message.right))
-
-      platform.analytics?.logEvent({
-        type: "HOPP_EXPORT_COLLECTION",
-        exporter: "json",
-        platform: "rest",
-      })
+    if (E.isLeft(result)) {
+      // INVALID_WORKSPACE_HANDLE
     }
 
+    toast.success(t("state.download_started"))
+
+    platform.analytics?.logEvent({
+      type: "HOPP_EXPORT_COLLECTION",
+      exporter: "json",
+      platform: "rest",
+    })
+
     isHoppMyCollectionExporterInProgress.value = false
   },
 }
@@ -443,10 +471,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
           return toast.error(t("error.no_collections_to_export"))
         }
 
-        initializeDownloadCollection(
-          exportCollectionsToJSON,
-          "team-collections"
-        )
+        initializeDownloadFile(exportCollectionsToJSON, "team-collections")
 
         platform.analytics?.logEvent({
           type: "HOPP_EXPORT_COLLECTION",

+ 3 - 3
packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue

@@ -21,7 +21,7 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
 
 import IconFolderPlus from "~icons/lucide/folder-plus"
 import IconUser from "~icons/lucide/user"
-import { initializeDownloadCollection } from "~/helpers/import-export/export"
+import { initializeDownloadFile } from "~/helpers/import-export/export"
 import { useReadonlyStream } from "~/composables/stream"
 
 import { platform } from "~/platform"
@@ -133,12 +133,12 @@ const GqlCollectionsHoppExporter: ImporterOrExporter = {
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace"],
   },
-  action: () => {
+  action: async () => {
     if (!gqlCollections.value.length) {
       return toast.error(t("error.no_collections_to_export"))
     }
 
-    const message = initializeDownloadCollection(
+    const message = await initializeDownloadFile(
       gqlCollectionsExporter(gqlCollections.value),
       "GQLCollections"
     )

+ 3 - 3
packages/hoppscotch-common/src/components/environments/ImportExport.vue

@@ -37,7 +37,7 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
 import IconPostman from "~icons/hopp/postman"
 import IconInsomnia from "~icons/hopp/insomnia"
 import IconUser from "~icons/lucide/user"
-import { initializeDownloadCollection } from "~/helpers/import-export/export"
+import { initializeDownloadFile } from "~/helpers/import-export/export"
 import { computed } from "vue"
 import { useReadonlyStream } from "~/composables/stream"
 import { environmentsExporter } from "~/helpers/import-export/export/environments"
@@ -230,12 +230,12 @@ const HoppEnvironmentsExport: ImporterOrExporter = {
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace"],
   },
-  action: () => {
+  action: async () => {
     if (!environmentJson.value.length) {
       return toast.error(t("error.no_environments_to_export"))
     }
 
-    const message = initializeDownloadCollection(
+    const message = await initializeDownloadFile(
       environmentsExporter(environmentJson.value),
       "Environments"
     )

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

@@ -110,7 +110,8 @@
                     :shortcut="['X']"
                     @click="
                       () => {
-                        emit('export-data'), hide()
+                        emit('export-collection', collectionView.collectionID)
+                        hide()
                       }
                     "
                   />
@@ -189,7 +190,7 @@ const emit = defineEmits<{
     event: "edit-root-collection",
     payload: { collectionIndexPath: string; collectionName: string }
   ): void
-  (event: "export-data"): void
+  (event: "export-collection", collectionIndexPath: string): void
   (event: "remove-child-collection", collectionIndexPath: string): void
   (event: "remove-root-collection", collectionIndexPath: string): void
   (event: "toggle-children"): void

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

@@ -27,7 +27,7 @@
           v-tippy="{ theme: 'tooltip' }"
           :icon="IconImport"
           :title="t('modal.import_export')"
-          @click="() => {}"
+          @click="displayModalImportExport(true)"
         />
       </span>
     </div>
@@ -50,6 +50,7 @@
             @edit-child-collection="editChildCollection"
             @edit-root-collection="editRootCollection"
             @edit-collection-properties="editCollectionProperties"
+            @export-collection="exportCollection"
             @remove-child-collection="removeChildCollection"
             @remove-root-collection="removeRootCollection"
             @select-pick="onSelectPick"
@@ -143,6 +144,13 @@
       @resolve="resolveConfirmModal"
     />
 
+    <!-- TODO: Supply `collectionsType` once teams implementation is in place -->
+    <!-- Defaults to `my-collections` -->
+    <CollectionsImportExport
+      v-if="showImportExportModal"
+      @hide-modal="displayModalImportExport(false)"
+    />
+
     <!-- TODO: Remove the `emitWithFullCollection` prop after porting all usages of the below component -->
     <CollectionsProperties
       :show="showModalEditProperties"
@@ -220,6 +228,7 @@ const showModalAddChildColl = ref(false)
 const showModalEditRootColl = ref(false)
 const showModalEditChildColl = ref(false)
 const showModalEditRequest = ref(false)
+const showImportExportModal = ref(false)
 const showModalEditProperties = ref(false)
 const showConfirmModal = ref(false)
 
@@ -309,6 +318,12 @@ const displayModalEditRequest = (show: boolean) => {
   if (!show) resetSelectedData()
 }
 
+const displayModalImportExport = (show: boolean) => {
+  showImportExportModal.value = show
+
+  if (!show) resetSelectedData()
+}
+
 const displayModalEditProperties = (show: boolean) => {
   showModalEditProperties.value = show
 
@@ -1102,6 +1117,40 @@ const setCollectionProperties = async (updatedCollectionProps: {
   displayModalEditProperties(false)
 }
 
+const exportCollection = async (collectionIndexPath: string) => {
+  const collectionHandleResult = await workspaceService.getCollectionHandle(
+    props.workspaceHandle,
+    collectionIndexPath
+  )
+
+  if (E.isLeft(collectionHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE | INVALID_COLLECTION_ID | INVALID_PATH
+    return
+  }
+
+  const collectionHandle = collectionHandleResult.right
+
+  if (collectionHandle.value.type === "invalid") {
+    // WORKSPACE_INVALIDATED
+    return
+  }
+
+  const collection = navigateToFolderWithIndexPath(
+    restCollectionState.value,
+    collectionIndexPath.split("/").map((id) => parseInt(id))
+  ) as HoppCollection
+
+  const result = await workspaceService.exportRESTCollection(
+    collectionHandle,
+    collection
+  )
+
+  if (E.isLeft(result)) {
+    // INVALID_COLLECTION_HANDLE
+    return
+  }
+}
+
 const shareRequest = (request: HoppRESTRequest) => {
   if (currentUser.value) {
     // Opens the share request modal if the user is logged in

+ 17 - 18
packages/hoppscotch-common/src/helpers/import-export/export/index.ts

@@ -1,32 +1,31 @@
 import * as E from "fp-ts/Either"
 
+import { platform } from "~/platform"
+
 /**
  * Create a downloadable file from a collection and prompts the user to download it.
  * @param collectionJSON - JSON string of the collection
  * @param name - Name of the collection set as the file name
  */
-export const initializeDownloadCollection = (
+export const initializeDownloadFile = async (
   collectionJSON: string,
   name: string | null
 ) => {
-  const file = new Blob([collectionJSON], { type: "application/json" })
-  const a = document.createElement("a")
-  const url = URL.createObjectURL(file)
-  a.href = url
+  const result = await platform.io.saveFileWithDialog({
+    data: collectionJSON,
+    contentType: "application/json",
+    suggestedFilename: `${name ?? "collection"}.json`,
+    filters: [
+      {
+        name: "Hoppscotch Collection/Environment JSON file",
+        extensions: ["json"],
+      },
+    ],
+  })
 
-  if (name) {
-    a.download = `${name}.json`
-  } else {
-    a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
+  if (result.type === "unknown" || result.type === "saved") {
+    return E.right("state.download_started")
   }
 
-  document.body.appendChild(a)
-  a.click()
-
-  setTimeout(() => {
-    document.body.removeChild(a)
-    URL.revokeObjectURL(url)
-  }, 1000)
-
-  return E.right("state.download_started")
+  return E.left("state.download_failed")
 }

+ 0 - 5
packages/hoppscotch-common/src/helpers/import-export/export/myCollections.ts

@@ -1,5 +0,0 @@
-import { HoppCollection } from "@hoppscotch/data"
-
-export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
-  return JSON.stringify(myCollections, null, 2)
-}

+ 11 - 9
packages/hoppscotch-common/src/platform/std/io.ts

@@ -13,15 +13,17 @@ export const browserIODef: IOPlatformDef = {
     const url = URL.createObjectURL(file)
 
     a.href = url
-    a.download = pipe(
-      url,
-      S.split("/"),
-      RNEA.last,
-      S.split("#"),
-      RNEA.head,
-      S.split("?"),
-      RNEA.head
-    )
+    a.download =
+      opts.suggestedFilename ??
+      pipe(
+        url,
+        S.split("/"),
+        RNEA.last,
+        S.split("#"),
+        RNEA.head,
+        S.split("?"),
+        RNEA.head
+      )
 
     document.body.appendChild(a)
     a.click()

+ 93 - 0
packages/hoppscotch-common/src/services/new-workspace/index.ts

@@ -383,6 +383,99 @@ export class NewWorkspaceService extends Service {
     return E.right(result.right)
   }
 
+  public async importRESTCollections(
+    workspaceHandle: HandleRef<Workspace>,
+    collections: HoppCollection[]
+  ): Promise<
+    E.Either<
+      WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
+      HandleRef<WorkspaceCollection>
+    >
+  > {
+    if (workspaceHandle.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      workspaceHandle.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.importRESTCollections(
+      workspaceHandle,
+      collections
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
+  public async exportRESTCollections(
+    workspaceHandle: HandleRef<Workspace>,
+    collections: HoppCollection[]
+  ): Promise<
+    E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
+  > {
+    if (workspaceHandle.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      workspaceHandle.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.exportRESTCollections(
+      workspaceHandle,
+      collections
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
+  public async exportRESTCollection(
+    collectionHandle: HandleRef<WorkspaceCollection>,
+    collection: HoppCollection
+  ): Promise<
+    E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
+  > {
+    if (collectionHandle.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      collectionHandle.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.exportRESTCollection(
+      collectionHandle,
+      collection
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
   public async getRESTCollectionChildrenView(
     collectionHandle: HandleRef<WorkspaceCollection>
   ): Promise<

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

@@ -67,4 +67,17 @@ export interface WorkspaceProvider {
   removeRESTRequest(
     requestHandle: HandleRef<WorkspaceRequest>
   ): Promise<E.Either<unknown, void>>
+
+  importRESTCollections(
+    workspaceHandle: HandleRef<Workspace>,
+    collections: HoppCollection[]
+  ): Promise<E.Either<unknown, HandleRef<WorkspaceCollection>>>
+  exportRESTCollections(
+    workspaceHandle: HandleRef<Workspace>,
+    collections: HoppCollection[]
+  ): Promise<E.Either<unknown, void>>
+  exportRESTCollection(
+    collectionHandle: HandleRef<WorkspaceCollection>,
+    collection: HoppCollection
+  ): Promise<E.Either<unknown, void>>
 }

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