fix: ensure cross-platform compatibility with file exports (#4336)

This commit is contained in:
James George
2024-09-10 00:23:33 -07:00
committed by GitHub
parent 3b29a56ba0
commit a395643387
8 changed files with 114 additions and 107 deletions

View File

@@ -48,7 +48,7 @@ import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
import { platform } from "~/platform" 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 { gistExporter } from "~/helpers/import-export/export/gist"
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections" import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections" import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
@@ -389,27 +389,27 @@ const HoppMyCollectionsExporter: ImporterOrExporter = {
applicableTo: ["personal-workspace"], applicableTo: ["personal-workspace"],
isLoading: isHoppMyCollectionExporterInProgress, isLoading: isHoppMyCollectionExporterInProgress,
}, },
action: () => { action: async () => {
if (!myCollections.value.length) { if (!myCollections.value.length) {
return toast.error(t("error.no_collections_to_export")) return toast.error(t("error.no_collections_to_export"))
} }
isHoppMyCollectionExporterInProgress.value = true isHoppMyCollectionExporterInProgress.value = true
const message = initializeDownloadCollection( const message = await initializeDownloadFile(
myCollectionsExporter(myCollections.value), myCollectionsExporter(myCollections.value),
"Collections" "hoppscotch-personal-collections"
) )
if (E.isRight(message)) { E.isRight(message)
toast.success(t(message.right)) ? toast.success(t(message.right))
: toast.error(t(message.left))
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION", type: "HOPP_EXPORT_COLLECTION",
exporter: "json", exporter: "json",
platform: "rest", platform: "rest",
}) })
}
isHoppMyCollectionExporterInProgress.value = false isHoppMyCollectionExporterInProgress.value = false
}, },
@@ -436,7 +436,14 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
) )
if (E.isRight(res)) { if (E.isRight(res)) {
initializeDownloadCollection(res.right, "team-collections") const message = await initializeDownloadFile(
res.right,
"hoppscotch-team-collections"
)
E.isRight(message)
? toast.success(t(message.right))
: toast.error(t(message.left))
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION", type: "HOPP_EXPORT_COLLECTION",

View File

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

View File

@@ -13,36 +13,36 @@ import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { ref } from "vue" import { ref } from "vue"
import { ImporterOrExporter } from "~/components/importExport/types"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { ImporterOrExporter } from "~/components/importExport/types" import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
import { import {
appendEnvironments,
addGlobalEnvVariable, addGlobalEnvVariable,
appendEnvironments,
environments$, environments$,
} from "~/newstore/environments" } from "~/newstore/environments"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql" import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv" import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv" import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv"
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
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 { computed } from "vue" import { computed } from "vue"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import { initializeDownloadFile } from "~/helpers/import-export/export"
import { environmentsExporter } from "~/helpers/import-export/export/environments" import { environmentsExporter } from "~/helpers/import-export/export/environments"
import { gistExporter } from "~/helpers/import-export/export/gist" import { gistExporter } from "~/helpers/import-export/export/gist"
import { platform } from "~/platform" import { platform } from "~/platform"
import IconInsomnia from "~icons/hopp/insomnia"
import IconPostman from "~icons/hopp/postman"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -67,19 +67,17 @@ const isTeamEnvironment = computed(() => {
}) })
const environmentJson = computed(() => { const environmentJson = computed(() => {
if ( if (isTeamEnvironment.value && props.teamEnvironments) {
props.environmentType === "TEAM_ENV" && return props.teamEnvironments.map((x) => x.environment)
props.teamEnvironments !== undefined
) {
const teamEnvironments = props.teamEnvironments.map(
(x) => x.environment as Environment
)
return teamEnvironments
} }
return myEnvironments.value return myEnvironments.value
}) })
const workspaceType = computed(() =>
isTeamEnvironment.value ? "team" : "personal"
)
const HoppEnvironmentsImport: ImporterOrExporter = { const HoppEnvironmentsImport: ImporterOrExporter = {
metadata: { metadata: {
id: "import.from_json", id: "import.from_json",
@@ -105,7 +103,7 @@ const HoppEnvironmentsImport: ImporterOrExporter = {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT", type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest", platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal", workspaceType: workspaceType.value,
}) })
emit("hide-modal") emit("hide-modal")
@@ -138,7 +136,7 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT", type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest", platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal", workspaceType: workspaceType.value,
}) })
emit("hide-modal") emit("hide-modal")
@@ -178,7 +176,7 @@ const insomniaEnvironmentsImport: ImporterOrExporter = {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT", type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest", platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal", workspaceType: workspaceType.value,
}) })
emit("hide-modal") emit("hide-modal")
@@ -214,7 +212,7 @@ const EnvironmentsImportFromGIST: ImporterOrExporter = {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT", type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest", platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal", workspaceType: workspaceType.value,
}) })
emit("hide-modal") emit("hide-modal")
}, },
@@ -230,14 +228,14 @@ const HoppEnvironmentsExport: ImporterOrExporter = {
disabled: false, disabled: false,
applicableTo: ["personal-workspace", "team-workspace"], applicableTo: ["personal-workspace", "team-workspace"],
}, },
action: () => { action: async () => {
if (!environmentJson.value.length) { if (!environmentJson.value.length) {
return toast.error(t("error.no_environments_to_export")) return toast.error(t("error.no_environments_to_export"))
} }
const message = initializeDownloadCollection( const message = await initializeDownloadFile(
environmentsExporter(environmentJson.value), environmentsExporter(environmentJson.value),
"Environments" `hoppscotch-${workspaceType.value}-environments`
) )
if (E.isLeft(message)) { if (E.isLeft(message)) {

View File

@@ -126,6 +126,8 @@ import { useService } from "dioc/vue"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import * as E from "fp-ts/Either"
import { exportAsJSON } from "~/helpers/import-export/export/environment" import { exportAsJSON } from "~/helpers/import-export/export/environment"
import { import {
createEnvironment, createEnvironment,
@@ -163,11 +165,13 @@ const secretEnvironmentService = useService(SecretEnvironmentService)
const isGlobalEnvironment = computed(() => props.environmentIndex === "Global") const isGlobalEnvironment = computed(() => props.environmentIndex === "Global")
const exportEnvironmentAsJSON = () => { const exportEnvironmentAsJSON = async () => {
const { environment, environmentIndex } = props const { environment, environmentIndex } = props
exportAsJSON(environment, environmentIndex)
? toast.success(t("state.download_started")) const message = await exportAsJSON(environment, environmentIndex)
: toast.error(t("state.download_failed")) E.isRight(message)
? toast.success(t(message.right))
: toast.error(t(message.left))
} }
const tippyActions = ref<HTMLDivElement | null>(null) const tippyActions = ref<HTMLDivElement | null>(null)

View File

@@ -128,6 +128,7 @@ import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { ref } from "vue" import { ref } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import * as E from "fp-ts/Either"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
@@ -161,10 +162,12 @@ const secretEnvironmentService = useService(SecretEnvironmentService)
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () => const exportEnvironmentAsJSON = async () => {
exportAsJSON(props.environment) const message = await exportAsJSON(props.environment)
? toast.success(t("state.download_started")) E.isRight(message)
: toast.error(t("state.download_failed")) ? toast.success(t(message.right))
: toast.error(t(message.left))
}
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)

View File

@@ -1,9 +1,11 @@
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { platform } from "~/platform"
const getEnvironmentJson = ( import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { initializeDownloadFile } from "."
const getEnvironmentJSON = (
environmentObj: TeamEnvironment | Environment, environmentObj: TeamEnvironment | Environment,
environmentIndex?: number | "Global" | null environmentIndex?: number | "Global" | null
) => { ) => {
@@ -22,33 +24,20 @@ const getEnvironmentJson = (
: undefined : undefined
} }
export const exportAsJSON = ( export const exportAsJSON = async (
environmentObj: Environment | TeamEnvironment, environmentObj: Environment | TeamEnvironment,
environmentIndex?: number | "Global" | null environmentIndex?: number | "Global" | null
): boolean => { ): Promise<E.Right<string> | E.Left<string>> => {
const dataToWrite = getEnvironmentJson(environmentObj, environmentIndex) const environmentJSON = getEnvironmentJSON(environmentObj, environmentIndex)
if (!dataToWrite) return false if (!environmentJSON) {
return E.left("state.download_failed")
}
const file = new Blob([dataToWrite], { type: "application/json" }) const message = await initializeDownloadFile(
const url = URL.createObjectURL(file) environmentJSON,
JSON.parse(environmentJSON).name
)
URL.revokeObjectURL(url) return message
platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
suggestedFilename: `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.json`,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
return true
} }

View File

@@ -1,32 +1,36 @@
import * as E from "fp-ts/Either" 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. * Create a downloadable file from a collection/environment and prompts the user to download it.
* @param collectionJSON - JSON string of the collection * @param contentsJSON - JSON string of the collection
* @param name - Name of the collection set as the file name * @param name - Name of the collection set as the file name
* @returns {Promise<E.Right<string> | E.Left<string>>} - Returns a promise that resolves to an `Either` with `i18n` key for the status message
*/ */
export const initializeDownloadCollection = ( export const initializeDownloadFile = async (
collectionJSON: string, contentsJSON: string,
name: string | null name: string | null
) => { ) => {
const file = new Blob([collectionJSON], { type: "application/json" }) const file = new Blob([contentsJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url
if (name) { const fileName = name ?? url.split("/").pop()!.split("#")[0].split("?")[0]
a.download = `${name}.json`
} else { const result = await platform.io.saveFileWithDialog({
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json` data: contentsJSON,
contentType: "application/json",
suggestedFilename: `${fileName}.json`,
filters: [
{
name: "Hoppscotch Collection/Environment JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
return E.right("state.download_started")
} }
document.body.appendChild(a) return E.left("state.download_failed")
a.click()
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
return E.right("state.download_started")
} }

View File

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