diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue
index 714311dd8..a6d6afc90 100644
--- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue
+++ b/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",
diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue
index a9dc667b8..add0f3365 100644
--- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue
+++ b/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"
)
diff --git a/packages/hoppscotch-common/src/components/environments/ImportExport.vue b/packages/hoppscotch-common/src/components/environments/ImportExport.vue
index fc97315f4..3a510247e 100644
--- a/packages/hoppscotch-common/src/components/environments/ImportExport.vue
+++ b/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"
)
diff --git a/packages/hoppscotch-common/src/components/new-collections/rest/Collection.vue b/packages/hoppscotch-common/src/components/new-collections/rest/Collection.vue
index 149efed62..f8ed08884 100644
--- a/packages/hoppscotch-common/src/components/new-collections/rest/Collection.vue
+++ b/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
diff --git a/packages/hoppscotch-common/src/components/new-collections/rest/index.vue b/packages/hoppscotch-common/src/components/new-collections/rest/index.vue
index 11ed484b4..4f8604ab7 100644
--- a/packages/hoppscotch-common/src/components/new-collections/rest/index.vue
+++ b/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)"
/>
@@ -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"
/>
+
+
+
+
{
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
diff --git a/packages/hoppscotch-common/src/helpers/import-export/export/index.ts b/packages/hoppscotch-common/src/helpers/import-export/export/index.ts
index 23e7fe5b2..f25c5cadb 100644
--- a/packages/hoppscotch-common/src/helpers/import-export/export/index.ts
+++ b/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")
}
diff --git a/packages/hoppscotch-common/src/helpers/import-export/export/myCollections.ts b/packages/hoppscotch-common/src/helpers/import-export/export/myCollections.ts
deleted file mode 100644
index 9a167e4ee..000000000
--- a/packages/hoppscotch-common/src/helpers/import-export/export/myCollections.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { HoppCollection } from "@hoppscotch/data"
-
-export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
- return JSON.stringify(myCollections, null, 2)
-}
diff --git a/packages/hoppscotch-common/src/platform/std/io.ts b/packages/hoppscotch-common/src/platform/std/io.ts
index b8fd50ffc..70e55399b 100644
--- a/packages/hoppscotch-common/src/platform/std/io.ts
+++ b/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()
diff --git a/packages/hoppscotch-common/src/services/new-workspace/index.ts b/packages/hoppscotch-common/src/services/new-workspace/index.ts
index 2d2a93dd8..497f8bea1 100644
--- a/packages/hoppscotch-common/src/services/new-workspace/index.ts
+++ b/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,
+ collections: HoppCollection[]
+ ): Promise<
+ E.Either<
+ WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
+ HandleRef
+ >
+ > {
+ 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,
+ collections: HoppCollection[]
+ ): Promise<
+ E.Either, 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,
+ collection: HoppCollection
+ ): Promise<
+ E.Either, 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
): Promise<
diff --git a/packages/hoppscotch-common/src/services/new-workspace/provider.ts b/packages/hoppscotch-common/src/services/new-workspace/provider.ts
index 41b2597ee..5886d7422 100644
--- a/packages/hoppscotch-common/src/services/new-workspace/provider.ts
+++ b/packages/hoppscotch-common/src/services/new-workspace/provider.ts
@@ -67,4 +67,17 @@ export interface WorkspaceProvider {
removeRESTRequest(
requestHandle: HandleRef
): Promise>
+
+ importRESTCollections(
+ workspaceHandle: HandleRef,
+ collections: HoppCollection[]
+ ): Promise>>
+ exportRESTCollections(
+ workspaceHandle: HandleRef,
+ collections: HoppCollection[]
+ ): Promise>
+ exportRESTCollection(
+ collectionHandle: HandleRef,
+ collection: HoppCollection
+ ): Promise>
}
diff --git a/packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts b/packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts
index 03d960306..4c9de8f60 100644
--- a/packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts
+++ b/packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts
@@ -15,6 +15,7 @@ import { useStreamStatic } from "~/composables/stream"
import {
addRESTCollection,
addRESTFolder,
+ appendRESTCollections,
editRESTCollection,
editRESTFolder,
editRESTRequest,
@@ -49,6 +50,7 @@ import { HoppGQLHeader } from "~/helpers/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import IconUser from "~icons/lucide/user"
import { NewWorkspaceService } from ".."
+import { initializeDownloadFile } from "~/helpers/import-export/export"
export class PersonalWorkspaceProviderService
extends Service
@@ -217,7 +219,7 @@ export class PersonalWorkspaceProviderService
collectionHandle.value.data.providerID !== this.providerID ||
collectionHandle.value.data.workspaceID !== "personal"
) {
- return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
+ return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
}
const { collectionID } = collectionHandle.value.data
@@ -393,6 +395,86 @@ export class PersonalWorkspaceProviderService
return Promise.resolve(E.right(undefined))
}
+ public importRESTCollections(
+ workspaceHandle: HandleRef,
+ collections: HoppCollection[]
+ ): Promise>> {
+ if (
+ workspaceHandle.value.type !== "ok" ||
+ workspaceHandle.value.data.providerID !== this.providerID ||
+ workspaceHandle.value.data.workspaceID !== "personal"
+ ) {
+ return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
+ }
+
+ appendRESTCollections(collections)
+
+ const newCollectionName = collections[0].name
+ const newCollectionID =
+ this.restCollectionState.value.state.length.toString()
+
+ return Promise.resolve(
+ E.right(
+ computed(() => {
+ if (
+ workspaceHandle.value.type !== "ok" ||
+ workspaceHandle.value.data.providerID !== this.providerID ||
+ workspaceHandle.value.data.workspaceID !== "personal"
+ ) {
+ return {
+ type: "invalid" as const,
+ reason: "WORKSPACE_INVALIDATED" as const,
+ }
+ }
+
+ return {
+ type: "ok",
+ data: {
+ providerID: this.providerID,
+ workspaceID: workspaceHandle.value.data.workspaceID,
+ collectionID: newCollectionID,
+ name: newCollectionName,
+ },
+ }
+ })
+ )
+ )
+ }
+
+ public exportRESTCollections(
+ workspaceHandle: HandleRef,
+ collections: HoppCollection[]
+ ): Promise> {
+ if (
+ workspaceHandle.value.type !== "ok" ||
+ workspaceHandle.value.data.providerID !== this.providerID ||
+ workspaceHandle.value.data.workspaceID !== "personal"
+ ) {
+ return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
+ }
+
+ initializeDownloadFile(JSON.stringify(collections, null, 2), "Collections")
+
+ return Promise.resolve(E.right(undefined))
+ }
+
+ public exportRESTCollection(
+ collectionHandle: HandleRef,
+ collection: HoppCollection
+ ): Promise> {
+ if (
+ collectionHandle.value.type !== "ok" ||
+ collectionHandle.value.data.providerID !== this.providerID ||
+ collectionHandle.value.data.workspaceID !== "personal"
+ ) {
+ return Promise.resolve(E.left("INVALID_COLLECTION_HANDLE" as const))
+ }
+
+ initializeDownloadFile(JSON.stringify(collection, null, 2), collection.name)
+
+ return Promise.resolve(E.right(undefined))
+ }
+
public getCollectionHandle(
workspaceHandle: HandleRef,
collectionID: string