refactor: revamp the importers & exporters systems to be reused (#3425)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -69,5 +69,7 @@ module.exports = {
|
||||
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||
},
|
||||
],
|
||||
eqeqeq: 1,
|
||||
"no-else-return": 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -273,6 +273,9 @@
|
||||
"variable": "Variable",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"error": {
|
||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||
"check_console_details": "Check console log for details.",
|
||||
@@ -308,7 +311,8 @@
|
||||
"create_secret_gist": "Create secret Gist",
|
||||
"gist_created": "Gist created",
|
||||
"require_github": "Login with GitHub to create secret gist",
|
||||
"title": "Export"
|
||||
"title": "Export",
|
||||
"failed": "Something went wrong while exporting"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
@@ -375,6 +379,7 @@
|
||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_file": "Import from File",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Enter Gist URL",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
@@ -382,7 +387,14 @@
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import"
|
||||
"title": "Import",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment JSON file",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
|
||||
@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
|
||||
},
|
||||
{ severity: 0 }
|
||||
)
|
||||
} else {
|
||||
return { severity: 0 }
|
||||
}
|
||||
return { severity: 0 }
|
||||
})
|
||||
|
||||
const severityColor = (severity: number) => {
|
||||
|
||||
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
|
||||
if (props.isSelected) return IconCheckCircle
|
||||
else if (!props.isOpen) return IconFolder
|
||||
else if (props.isOpen) return IconFolderOpen
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const collectionName = computed(() => {
|
||||
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||
else return (props.data as TeamCollection).title
|
||||
return (props.data as TeamCollection).title
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -424,9 +424,8 @@ const isCollLoading = computed(() => {
|
||||
props.data.id
|
||||
) {
|
||||
return collectionMoveLoading.includes(props.data.id)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
|
||||
@@ -1,361 +1,568 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.collections')"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<HoppButtonSecondary
|
||||
v-if="importerType !== null"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.go_back')"
|
||||
:icon="IconArrowLeft"
|
||||
@click="resetImport"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="importerType !== null" class="flex flex-col">
|
||||
<div class="flex flex-col pb-4">
|
||||
<div
|
||||
v-for="(step, index) in importerSteps"
|
||||
:key="`step-${index}`"
|
||||
class="flex flex-col space-y-8"
|
||||
>
|
||||
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasFile,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${step.metadata.caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ml-10 flex flex-col rounded border border-dashed border-dividerDark"
|
||||
>
|
||||
<input
|
||||
id="inputChooseFileToImportFrom"
|
||||
ref="inputChooseFileToImportFrom"
|
||||
name="inputChooseFileToImportFrom"
|
||||
type="file"
|
||||
class="cursor-pointer p-4 text-secondary transition file:mr-2 file:cursor-pointer file:rounded file:border-0 file:bg-primaryLight file:px-4 file:py-2 file:text-secondary file:transition hover:text-secondaryDark hover:file:bg-primaryDark hover:file:text-secondaryDark"
|
||||
:accept="step.metadata.acceptedFileTypes"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasGist,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${step.metadata.caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="ml-10 flex flex-col">
|
||||
<input
|
||||
v-model="inputChooseGistToImportFrom"
|
||||
type="url"
|
||||
class="input"
|
||||
:placeholder="`${t('import.gist_url')}`"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<select
|
||||
v-model="mySelectedCollectionID"
|
||||
autocomplete="off"
|
||||
class="select"
|
||||
autofocus
|
||||
>
|
||||
<option :key="undefined" :value="undefined" disabled selected>
|
||||
{{ t("collection.select") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(collection, collectionIndex) in myCollections"
|
||||
:key="`collection-${collectionIndex}`"
|
||||
:value="collectionIndex"
|
||||
class="bg-primary"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</option>
|
||||
</select>
|
||||
</HoppSmartSelectWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoppButtonPrimary
|
||||
:label="t('import.title')"
|
||||
:disabled="enableImportButton"
|
||||
:loading="importingMyCollections"
|
||||
@click="finishImport"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<HoppSmartExpand>
|
||||
<template #body>
|
||||
<HoppSmartItem
|
||||
v-for="(importer, index) in importerModules"
|
||||
:key="`importer-${index}`"
|
||||
:icon="importer.icon"
|
||||
:label="t(`${importer.name}`)"
|
||||
@click="importerType = index"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartExpand>
|
||||
<hr />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
:loading="exportingTeamCollections"
|
||||
:label="t('export.as_json')"
|
||||
@click="emit('export-json-collection')"
|
||||
/>
|
||||
<span
|
||||
v-if="platform.platformFeatureFlags.exportAsGIST"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
!currentUser
|
||||
? `${t('export.require_github')}`
|
||||
: currentUser.provider !== 'github.com'
|
||||
? `${t('export.require_github')}`
|
||||
: undefined
|
||||
"
|
||||
class="flex"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
: currentUser.provider !== 'github.com'
|
||||
? true
|
||||
: false
|
||||
"
|
||||
:icon="IconGithub"
|
||||
:loading="creatingGistCollection"
|
||||
:label="t('export.create_secret_gist')"
|
||||
@click="emit('create-collection-gist')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="modal.collections"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, PropType, ref, watch } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { platform } from "~/platform"
|
||||
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
|
||||
|
||||
import IconFile from "~icons/lucide/file"
|
||||
|
||||
import {
|
||||
hoppRESTImporter,
|
||||
hoppInsomniaImporter,
|
||||
hoppPostmanImporter,
|
||||
toTeamsImporter,
|
||||
hoppOpenAPIImporter,
|
||||
} from "~/helpers/import-export/import/importers"
|
||||
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
import { PropType, computed, ref } from "vue"
|
||||
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconOpenAPI from "~icons/lucide/file"
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import IconLink from "~icons/lucide/link"
|
||||
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
|
||||
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"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
type CollectionType = "team-collections" | "my-collections"
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
type CollectionType =
|
||||
| {
|
||||
type: "team-collections"
|
||||
selectedTeam: SelectedTeam
|
||||
}
|
||||
| { type: "my-collections" }
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
collectionsType: {
|
||||
type: String as PropType<CollectionType>,
|
||||
default: "my-collections",
|
||||
type: Object as PropType<CollectionType>,
|
||||
default: () => ({
|
||||
type: "my-collections",
|
||||
selectedTeam: undefined,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
exportingTeamCollections: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
creatingGistCollection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
importingMyCollections: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(e: "update-team-collections"): void
|
||||
(e: "export-json-collection"): void
|
||||
(e: "create-collection-gist"): void
|
||||
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
|
||||
}>()
|
||||
|
||||
const hasFile = ref(false)
|
||||
const hasGist = ref(false)
|
||||
|
||||
const importerType = ref<number | null>(null)
|
||||
|
||||
const stepResults = ref<StepReturnValue[]>([])
|
||||
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
||||
const inputChooseGistToImportFrom = ref<string>("")
|
||||
|
||||
const importerModules = computed(() =>
|
||||
RESTCollectionImporters.filter(
|
||||
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
|
||||
)
|
||||
)
|
||||
|
||||
const importerModule = computed(() => {
|
||||
if (importerType.value === null) return null
|
||||
return importerModules.value[importerType.value]
|
||||
})
|
||||
|
||||
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
||||
|
||||
const enableImportButton = computed(
|
||||
() => !(stepResults.value.length === importerSteps.value?.length)
|
||||
)
|
||||
|
||||
watch(mySelectedCollectionID, (newValue) => {
|
||||
if (newValue === undefined) return
|
||||
stepResults.value = []
|
||||
stepResults.value.push(newValue)
|
||||
})
|
||||
|
||||
watch(inputChooseGistToImportFrom, (url) => {
|
||||
stepResults.value = []
|
||||
if (url === "") {
|
||||
hasGist.value = false
|
||||
} else {
|
||||
hasGist.value = true
|
||||
stepResults.value.push(inputChooseGistToImportFrom.value)
|
||||
}
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||
if (!importerModule.value) return
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
pipe(
|
||||
await importerModule.value.importer(stepResults as any)(),
|
||||
E.match(
|
||||
(err) => {
|
||||
failedImport()
|
||||
console.error("error", err)
|
||||
},
|
||||
(result) => {
|
||||
if (props.collectionsType === "team-collections") {
|
||||
emit("import-to-teams", result)
|
||||
} else {
|
||||
appendRESTCollections(result)
|
||||
const handleImportToStore = async (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
const importResult =
|
||||
props.collectionsType.type === "my-collections"
|
||||
? await importToPersonalWorkspace(collections)
|
||||
: await importToTeamsWorkspace(collections)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: importerModule.value!.name,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
if (E.isRight(importResult)) {
|
||||
toast.success(t("state.file_imported"))
|
||||
emit("hide-modal")
|
||||
} else {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
}
|
||||
|
||||
fileImported()
|
||||
}
|
||||
const importToPersonalWorkspace = (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
appendRESTCollections(collections)
|
||||
return E.right({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
const importToTeamsWorkspace = async (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
|
||||
return E.left({
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
|
||||
const res = await toTeamsImporter(
|
||||
JSON.stringify(collections),
|
||||
selectedTeamID.value
|
||||
)()
|
||||
|
||||
return E.isRight(res)
|
||||
? E.right({ success: true })
|
||||
: E.left({
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
|
||||
const isHoppMyCollectionExporterInProgress = ref(false)
|
||||
const isHoppTeamCollectionExporterInProgress = ref(false)
|
||||
const isHoppGistCollectionExporterInProgress = ref(false)
|
||||
|
||||
const isTeamWorkspace = computed(() => {
|
||||
return props.collectionsType.type === "team-collections"
|
||||
})
|
||||
|
||||
const HoppRESTImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_rest",
|
||||
name: "import.from_json",
|
||||
title: "import.from_json_description",
|
||||
icon: IconFolderPlus,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||
},
|
||||
component: FileSource({
|
||||
caption: "import.from_file",
|
||||
acceptedFileTypes: ".json",
|
||||
onImportFromFile: async (content) => {
|
||||
const res = await hoppRESTImporter(content)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_json",
|
||||
platform: "rest",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const HoppMyCollectionImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_my_collection",
|
||||
name: "import.from_my_collections",
|
||||
title: "import.from_my_collections_description",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["team-workspace"],
|
||||
},
|
||||
component: defineStep("my_collection_import", MyCollectionImport, () => ({
|
||||
async onImportFromMyCollection(content) {
|
||||
handleImportToStore([content])
|
||||
|
||||
// our analytics consider this as an export event, so keeping compatibility with that
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "import_to_teams",
|
||||
platform: "rest",
|
||||
})
|
||||
},
|
||||
})),
|
||||
}
|
||||
|
||||
const HoppOpenAPIImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_openapi",
|
||||
name: "import.from_openapi",
|
||||
title: "import.from_openapi_description",
|
||||
icon: IconOpenAPI,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||
},
|
||||
supported_sources: [
|
||||
{
|
||||
id: "file_import",
|
||||
name: "import.from_file",
|
||||
icon: IconFile,
|
||||
step: FileSource({
|
||||
caption: "import.from_file",
|
||||
acceptedFileTypes: ".json, .yaml, .yml",
|
||||
onImportFromFile: async (content) => {
|
||||
const res = await hoppOpenAPIImporter(content)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
platform: "rest",
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_openapi",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "url_import",
|
||||
name: "import.from_url",
|
||||
icon: IconLink,
|
||||
step: UrlSource({
|
||||
caption: "import.from_url",
|
||||
onImportFromURL: async (content) => {
|
||||
const res = await hoppOpenAPIImporter(content)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
platform: "rest",
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_openapi",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const HoppPostmanImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_postman",
|
||||
name: "import.from_postman",
|
||||
title: "import.from_postman_description",
|
||||
icon: IconPostman,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||
},
|
||||
component: FileSource({
|
||||
caption: "import.from_file",
|
||||
acceptedFileTypes: ".json",
|
||||
onImportFromFile: async (content) => {
|
||||
const res = await hoppPostmanImporter(content)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
platform: "rest",
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_postman",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const HoppInsomniaImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_insomnia",
|
||||
name: "import.from_insomnia",
|
||||
title: "import.from_insomnia_description",
|
||||
icon: IconInsomnia,
|
||||
disabled: true,
|
||||
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||
},
|
||||
component: FileSource({
|
||||
caption: "import.from_file",
|
||||
acceptedFileTypes: ".json",
|
||||
onImportFromFile: async (content) => {
|
||||
const res = await hoppInsomniaImporter(content)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
platform: "rest",
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_insomnia",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const HoppGistImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_gist",
|
||||
name: "import.from_gist",
|
||||
title: "import.from_gist_description",
|
||||
icon: IconGithub,
|
||||
disabled: true,
|
||||
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||
},
|
||||
component: GistSource({
|
||||
caption: "import.from_url",
|
||||
onImportFromGist: async (content) => {
|
||||
if (E.isLeft(content)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
const res = await hoppRESTImporter(content.right)()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
platform: "rest",
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "import.from_gist",
|
||||
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||
})
|
||||
} else {
|
||||
showImportFailedError()
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const HoppMyCollectionsExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_my_collections",
|
||||
name: "export.as_json",
|
||||
title: "action.download_file",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace"],
|
||||
isLoading: isHoppMyCollectionExporterInProgress,
|
||||
},
|
||||
action: () => {
|
||||
if (!myCollections.value.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
isHoppMyCollectionExporterInProgress.value = true
|
||||
|
||||
const message = initializeDownloadCollection(
|
||||
myCollectionsExporter(myCollections.value),
|
||||
"Collections"
|
||||
)
|
||||
)
|
||||
|
||||
if (E.isRight(message)) {
|
||||
toast.success(t(message.right))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
}
|
||||
|
||||
isHoppMyCollectionExporterInProgress.value = false
|
||||
},
|
||||
}
|
||||
|
||||
const finishImport = async () => {
|
||||
await importerAction(stepResults.value)
|
||||
const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_team_collections",
|
||||
name: "export.as_json",
|
||||
title: "export.as_json_description",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["team-workspace"],
|
||||
isLoading: isHoppTeamCollectionExporterInProgress,
|
||||
},
|
||||
action: async () => {
|
||||
isHoppTeamCollectionExporterInProgress.value = true
|
||||
|
||||
if (
|
||||
props.collectionsType.type === "team-collections" &&
|
||||
props.collectionsType.selectedTeam
|
||||
) {
|
||||
const res = await teamCollectionsExporter(
|
||||
props.collectionsType.selectedTeam.id
|
||||
)
|
||||
|
||||
if (E.isRight(res)) {
|
||||
const { exportCollectionsToJSON } = res.right
|
||||
|
||||
if (!JSON.parse(exportCollectionsToJSON).length) {
|
||||
isHoppTeamCollectionExporterInProgress.value = false
|
||||
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
initializeDownloadCollection(
|
||||
exportCollectionsToJSON,
|
||||
"team-collections"
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
} else {
|
||||
toast.error(res.left.error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
isHoppTeamCollectionExporterInProgress.value = false
|
||||
},
|
||||
}
|
||||
|
||||
const onFileChange = () => {
|
||||
stepResults.value = []
|
||||
const HoppGistCollectionsExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "create_secret_gist",
|
||||
name: "export.create_secret_gist",
|
||||
icon: IconGithub,
|
||||
disabled: !currentUser.value
|
||||
? true
|
||||
: currentUser.value.provider !== "github.com",
|
||||
title: t("export.create_secret_gist"),
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
isLoading: isHoppGistCollectionExporterInProgress,
|
||||
},
|
||||
action: async () => {
|
||||
isHoppGistCollectionExporterInProgress.value = true
|
||||
|
||||
const inputFileToImport = inputChooseFileToImportFrom.value[0]
|
||||
const collectionJSON = await getCollectionJSON()
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
if (!inputFileToImport) {
|
||||
hasFile.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||
inputChooseFileToImportFrom.value[0].value = ""
|
||||
hasFile.value = false
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
const content = target!.result as string | null
|
||||
if (!content) {
|
||||
hasFile.value = false
|
||||
toast.show(t("action.choose_file").toString())
|
||||
if (!accessToken) {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
isHoppGistCollectionExporterInProgress.value = false
|
||||
return
|
||||
}
|
||||
|
||||
stepResults.value.push(content)
|
||||
hasFile.value = !!content?.length
|
||||
if (E.isRight(collectionJSON)) {
|
||||
collectionsGistExporter(collectionJSON.right, accessToken)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "gist",
|
||||
platform: "rest",
|
||||
})
|
||||
}
|
||||
|
||||
isHoppGistCollectionExporterInProgress.value = false
|
||||
},
|
||||
}
|
||||
|
||||
const importerModules = computed(() => {
|
||||
const enabledImporters = [
|
||||
HoppRESTImporter,
|
||||
HoppMyCollectionImporter,
|
||||
HoppOpenAPIImporter,
|
||||
HoppPostmanImporter,
|
||||
HoppInsomniaImporter,
|
||||
HoppGistImporter,
|
||||
]
|
||||
|
||||
const isTeams = props.collectionsType.type === "team-collections"
|
||||
|
||||
return enabledImporters.filter((importer) => {
|
||||
return isTeams
|
||||
? importer.metadata.applicableTo.includes("team-workspace")
|
||||
: importer.metadata.applicableTo.includes("personal-workspace")
|
||||
})
|
||||
})
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
const enabledExporters = [
|
||||
HoppMyCollectionsExporter,
|
||||
HoppTeamCollectionsExporter,
|
||||
]
|
||||
|
||||
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||
enabledExporters.push(HoppGistCollectionsExporter)
|
||||
}
|
||||
|
||||
reader.readAsText(inputFileToImport.files[0])
|
||||
}
|
||||
return enabledExporters.filter((exporter) => {
|
||||
return exporter.metadata.applicableTo.includes(
|
||||
props.collectionsType.type === "my-collections"
|
||||
? "personal-workspace"
|
||||
: "team-workspace"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const fileImported = () => {
|
||||
toast.success(t("state.file_imported").toString())
|
||||
hideModal()
|
||||
}
|
||||
const failedImport = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
const hideModal = () => {
|
||||
resetImport()
|
||||
emit("hide-modal")
|
||||
}
|
||||
const hasTeamWriteAccess = computed(() => {
|
||||
const { collectionsType } = props
|
||||
|
||||
const resetImport = () => {
|
||||
importerType.value = null
|
||||
hasFile.value = false
|
||||
hasGist.value = false
|
||||
stepResults.value = []
|
||||
inputChooseFileToImportFrom.value = ""
|
||||
inputChooseGistToImportFrom.value = ""
|
||||
mySelectedCollectionID.value = undefined
|
||||
const isTeamCollection = collectionsType.type === "team-collections"
|
||||
|
||||
if (!isTeamCollection || !collectionsType.selectedTeam) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
collectionsType.selectedTeam.myRole === "EDITOR" ||
|
||||
collectionsType.selectedTeam.myRole === "OWNER"
|
||||
)
|
||||
})
|
||||
|
||||
const selectedTeamID = computed(() => {
|
||||
const { collectionsType } = props
|
||||
|
||||
return collectionsType.type === "team-collections"
|
||||
? collectionsType.selectedTeam?.id
|
||||
: undefined
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
|
||||
const getCollectionJSON = async () => {
|
||||
if (
|
||||
props.collectionsType.type === "team-collections" &&
|
||||
props.collectionsType.selectedTeam?.id
|
||||
) {
|
||||
const res = await getTeamCollectionJSON(
|
||||
props.collectionsType.selectedTeam?.id
|
||||
)
|
||||
|
||||
return E.isRight(res)
|
||||
? E.right(res.right.exportCollectionsToJSON)
|
||||
: E.left(res.left)
|
||||
}
|
||||
|
||||
if (props.collectionsType.type === "my-collections") {
|
||||
return E.right(JSON.stringify(myCollections.value, null, 2))
|
||||
}
|
||||
|
||||
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -538,13 +538,12 @@ const isSelected = ({
|
||||
props.picked.folderPath === folderPath &&
|
||||
props.picked.requestIndex === requestIndex
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-folder" &&
|
||||
props.picked.folderPath === folderPath
|
||||
)
|
||||
}
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "my-folder" &&
|
||||
props.picked.folderPath === folderPath
|
||||
)
|
||||
}
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
@@ -741,11 +740,10 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
status: "loaded",
|
||||
data: data,
|
||||
} as ChildrenResult<Folder | Requests>
|
||||
} else {
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -374,9 +374,8 @@ const updateLastItemOrder = (e: DragEvent) => {
|
||||
const isRequestLoading = computed(() => {
|
||||
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
||||
return props.requestMoveLoading.includes(props.requestID)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
|
||||
@@ -141,9 +141,8 @@ const reqName = computed(() => {
|
||||
return props.request.name
|
||||
} else if (props.mode === "rest") {
|
||||
return restRequestName.value
|
||||
} else {
|
||||
return gqlRequestName.value
|
||||
}
|
||||
return gqlRequestName.value
|
||||
})
|
||||
|
||||
const requestName = ref(reqName.value)
|
||||
@@ -480,21 +479,20 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
return t("team.invalid_id")
|
||||
case "team/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
return t("team.invalid_id")
|
||||
case "team/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -554,13 +554,12 @@ const isSelected = ({
|
||||
props.picked.pickedType === "teams-request" &&
|
||||
props.picked.requestID === requestID
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-folder" &&
|
||||
props.picked.folderID === folderID
|
||||
)
|
||||
}
|
||||
return (
|
||||
props.picked &&
|
||||
props.picked.pickedType === "teams-folder" &&
|
||||
props.picked.folderID === folderID
|
||||
)
|
||||
}
|
||||
|
||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||
@@ -726,82 +725,78 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||
return {
|
||||
status: "loading",
|
||||
}
|
||||
} else {
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
}
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
data: {
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
type: "collections",
|
||||
data: {
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
type: "collections",
|
||||
data: {
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
},
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
},
|
||||
}))
|
||||
return {
|
||||
status: "loaded",
|
||||
data: cloneDeep(data),
|
||||
} as ChildrenResult<TeamCollections>
|
||||
}
|
||||
} else {
|
||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||
},
|
||||
}))
|
||||
return {
|
||||
status: "loaded",
|
||||
data: cloneDeep(data),
|
||||
} as ChildrenResult<TeamCollections>
|
||||
}
|
||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||
|
||||
!props.teamLoadingCollections.includes(parsedID) &&
|
||||
emit("expand-team-collection", parsedID)
|
||||
!props.teamLoadingCollections.includes(parsedID) &&
|
||||
emit("expand-team-collection", parsedID)
|
||||
|
||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||
return {
|
||||
status: "loading",
|
||||
}
|
||||
} else {
|
||||
const items = this.findCollInTree(this.data.value, parsedID)
|
||||
if (items) {
|
||||
const data = [
|
||||
...(items.children
|
||||
? items.children.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.children && items.children.length > 1
|
||||
? index === items.children.length - 1
|
||||
: false,
|
||||
type: "folders",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
data: item,
|
||||
},
|
||||
},
|
||||
}))
|
||||
: []),
|
||||
...(items.requests
|
||||
? items.requests.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.requests && items.requests.length > 1
|
||||
? index === items.requests.length - 1
|
||||
: false,
|
||||
type: "requests",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
data: item,
|
||||
},
|
||||
},
|
||||
}))
|
||||
: []),
|
||||
]
|
||||
return {
|
||||
status: "loaded",
|
||||
data: cloneDeep(data),
|
||||
} as ChildrenResult<TeamFolder | TeamRequests>
|
||||
} else {
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||
return {
|
||||
status: "loading",
|
||||
}
|
||||
}
|
||||
const items = this.findCollInTree(this.data.value, parsedID)
|
||||
if (items) {
|
||||
const data = [
|
||||
...(items.children
|
||||
? items.children.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.children && items.children.length > 1
|
||||
? index === items.children.length - 1
|
||||
: false,
|
||||
type: "folders",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
data: item,
|
||||
},
|
||||
},
|
||||
}))
|
||||
: []),
|
||||
...(items.requests
|
||||
? items.requests.map((item, index) => ({
|
||||
id: `${id}/${item.id}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
items.requests && items.requests.length > 1
|
||||
? index === items.requests.length - 1
|
||||
: false,
|
||||
type: "requests",
|
||||
data: {
|
||||
parentIndex: parsedID,
|
||||
data: item,
|
||||
},
|
||||
},
|
||||
}))
|
||||
: []),
|
||||
]
|
||||
return {
|
||||
status: "loaded",
|
||||
data: cloneDeep(data),
|
||||
} as ChildrenResult<TeamFolder | TeamRequests>
|
||||
}
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ const collectionIcon = computed(() => {
|
||||
if (isSelected.value) return IconCheckCircle
|
||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const pick = () => {
|
||||
|
||||
@@ -253,7 +253,7 @@ const collectionIcon = computed(() => {
|
||||
if (isSelected.value) return IconCheckCircle
|
||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const pick = () => {
|
||||
|
||||
@@ -1,299 +1,227 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('modal.collections')}`"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<span>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="t('import.from_gist')"
|
||||
@click="
|
||||
() => {
|
||||
readCollectionGist()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
!currentUser
|
||||
? `${t('export.require_github')}`
|
||||
: currentUser.provider !== 'github.com'
|
||||
? `${t('export.require_github')}`
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
: currentUser.provider !== 'github.com'
|
||||
? true
|
||||
: false
|
||||
"
|
||||
:icon="IconGithub"
|
||||
:label="t('export.create_secret_gist')"
|
||||
@click="
|
||||
() => {
|
||||
createCollectionGist()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('import.from_json')"
|
||||
@click="openDialogChooseFileToImportFrom"
|
||||
/>
|
||||
<input
|
||||
ref="inputChooseFileToImportFrom"
|
||||
class="input"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
@change="importFromJSON"
|
||||
/>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.as_json')"
|
||||
@click="exportJSON"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="graphql_collections.title"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
graphqlCollections$,
|
||||
setGraphqlCollections,
|
||||
appendGraphqlCollections,
|
||||
} from "~/newstore/collections"
|
||||
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
|
||||
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
|
||||
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
|
||||
import { computed } from "vue"
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
||||
const toast = useToast()
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
||||
const GqlCollectionsHoppImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.from_json",
|
||||
name: "import.from_json",
|
||||
icon: IconFolderPlus,
|
||||
title: "import.from_json",
|
||||
applicableTo: ["personal-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: FileSource({
|
||||
acceptedFileTypes: "application/json",
|
||||
caption: "import.from_json_description",
|
||||
onImportFromFile: async (gqlCollections) => {
|
||||
const res = await hoppGqlCollectionsImporter(gqlCollections)
|
||||
|
||||
const collectionJson = computed(() => {
|
||||
return JSON.stringify(collections.value, null, 2)
|
||||
})
|
||||
|
||||
const createCollectionGist = async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
"https://api.github.com/gists",
|
||||
{
|
||||
files: {
|
||||
"hoppscotch-collections.json": {
|
||||
content: collectionJson.value,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${currentUser.value.accessToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
importer: "json",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const GqlCollectionsGistImporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.from_gist",
|
||||
name: "import.from_gist",
|
||||
icon: IconFolderPlus,
|
||||
title: "import.from_gist",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: GistSource({
|
||||
caption: "import.gql_collections_from_gist_description",
|
||||
onImportFromGist: async (gqlCollections) => {
|
||||
if (E.isLeft(gqlCollections)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
const res = await hoppGqlCollectionsImporter(gqlCollections.right)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
importer: "gist",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
|
||||
|
||||
const GqlCollectionsHoppExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "export.as_json",
|
||||
name: "export.as_json",
|
||||
title: "action.download_file",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
},
|
||||
action: () => {
|
||||
if (!gqlCollections.value.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
const message = initializeDownloadCollection(
|
||||
gqlCollectionsExporter(gqlCollections.value),
|
||||
"GQLCollections"
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const fileImported = () => {
|
||||
toast.success(t("state.file_imported").toString())
|
||||
}
|
||||
|
||||
const failedImport = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
const readCollectionGist = async () => {
|
||||
const gist = prompt(t("import.gist_url").toString())
|
||||
if (!gist) return
|
||||
|
||||
try {
|
||||
const { files } = (await axios.get(
|
||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)) as {
|
||||
files: {
|
||||
[fileName: string]: {
|
||||
content: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const collections = JSON.parse(Object.values(files)[0].content)
|
||||
setGraphqlCollections(collections)
|
||||
fileImported()
|
||||
} catch (e) {
|
||||
failedImport()
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const openDialogChooseFileToImportFrom = () => {
|
||||
if (inputChooseFileToImportFrom.value)
|
||||
inputChooseFileToImportFrom.value.click()
|
||||
}
|
||||
|
||||
const importFromJSON = () => {
|
||||
if (!inputChooseFileToImportFrom.value) return
|
||||
|
||||
if (
|
||||
!inputChooseFileToImportFrom.value.files ||
|
||||
inputChooseFileToImportFrom.value.files.length === 0
|
||||
) {
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
const content = target!.result as string | null
|
||||
|
||||
if (!content) {
|
||||
toast.show(t("action.choose_file").toString())
|
||||
if (E.isLeft(message)) {
|
||||
toast.error(t("export.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
const collections = JSON.parse(content)
|
||||
if (collections[0]) {
|
||||
const [name, folders, requests] = Object.keys(collections[0])
|
||||
if (name === "name" && folders === "folders" && requests === "requests") {
|
||||
// Do nothing
|
||||
}
|
||||
} else {
|
||||
failedImport()
|
||||
return
|
||||
}
|
||||
appendGraphqlCollections(collections)
|
||||
toast.success(message.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "json",
|
||||
workspaceType: "personal",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
}
|
||||
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = collectionJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
exporter: "json",
|
||||
})
|
||||
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const GqlCollectionsGistExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "export.as_gist",
|
||||
name: "export.create_secret_gist",
|
||||
title: !currentUser
|
||||
? "export.require_github"
|
||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
currentUser.provider !== "github.com"
|
||||
? `export.require_github`
|
||||
: "export.create_secret_gist",
|
||||
icon: IconUser,
|
||||
disabled: !currentUser.value
|
||||
? true
|
||||
: currentUser.value.provider !== "github.com",
|
||||
applicableTo: ["personal-workspace"],
|
||||
},
|
||||
action: async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission"))
|
||||
return
|
||||
}
|
||||
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
if (accessToken) {
|
||||
const res = await gqlCollectionsGistExporter(
|
||||
JSON.stringify(gqlCollections.value),
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("export.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(t("export.success"))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
exporter: "gist",
|
||||
})
|
||||
|
||||
window.open(res.right, "_blank")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const importerModules = [GqlCollectionsHoppImporter, GqlCollectionsGistImporter]
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
const modules = [GqlCollectionsHoppExporter]
|
||||
|
||||
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||
modules.push(GqlCollectionsGistExporter)
|
||||
}
|
||||
|
||||
return modules
|
||||
})
|
||||
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
const handleImportToStore = async (
|
||||
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||
) => {
|
||||
setGraphqlCollections(gqlCollections)
|
||||
toast.success(t("import.success"))
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
@hide-modal="displayModalEditRequest(false)"
|
||||
/>
|
||||
<CollectionsGraphqlImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -140,17 +140,13 @@
|
||||
@hide-modal="showConfirmModal = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
|
||||
<CollectionsImportExport
|
||||
:show="showModalImportExport"
|
||||
:collections-type="collectionsType.type"
|
||||
:exporting-team-collections="exportingTeamCollections"
|
||||
:creating-gist-collection="creatingGistCollection"
|
||||
:importing-my-collections="importingMyCollections"
|
||||
@export-json-collection="exportJSONCollection"
|
||||
@create-collection-gist="createCollectionGist"
|
||||
@import-to-teams="importToTeams"
|
||||
v-if="showModalImportExport"
|
||||
:collections-type="collectionsType"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
|
||||
<TeamsAdd
|
||||
:show="showTeamModalAdd"
|
||||
@hide-modal="displayTeamModalAdd(false)"
|
||||
@@ -199,7 +195,6 @@ import {
|
||||
createChildCollection,
|
||||
renameCollection,
|
||||
deleteCollection,
|
||||
importJSONToTeam,
|
||||
moveRESTTeamCollection,
|
||||
updateOrderRESTTeamCollection,
|
||||
} from "~/helpers/backend/mutations/TeamCollection"
|
||||
@@ -214,12 +209,9 @@ import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import { Collection as NodeCollection } from "./MyCollections.vue"
|
||||
import {
|
||||
getCompleteCollectionTree,
|
||||
getTeamCollectionJSON,
|
||||
teamCollToHoppRESTColl,
|
||||
} from "~/helpers/backend/helpers"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { platform } from "~/platform"
|
||||
import { createCollectionGists } from "~/helpers/gist"
|
||||
import {
|
||||
getRequestsByPath,
|
||||
resolveSaveContextOnRequestReorder,
|
||||
@@ -305,12 +297,6 @@ const draggingToRoot = ref(false)
|
||||
const collectionMoveLoading = ref<string[]>([])
|
||||
const requestMoveLoading = ref<string[]>([])
|
||||
|
||||
// Export - Import refs
|
||||
const collectionJSON = ref("")
|
||||
const exportingTeamCollections = ref(false)
|
||||
const creatingGistCollection = ref(false)
|
||||
const importingMyCollections = ref(false)
|
||||
|
||||
// TeamList-Adapter
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
@@ -414,14 +400,12 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
})
|
||||
|
||||
const hasTeamWriteAccess = computed(() => {
|
||||
if (!collectionsType.value.selectedTeam) return false
|
||||
if (collectionsType.value.type !== "team-collections") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
collectionsType.value.type === "team-collections" &&
|
||||
collectionsType.value.selectedTeam.myRole !== "VIEWER"
|
||||
)
|
||||
return true
|
||||
else return false
|
||||
const role = collectionsType.value.selectedTeam?.myRole
|
||||
return role === "OWNER" || role === "EDITOR"
|
||||
})
|
||||
|
||||
const filteredCollections = computed(() => {
|
||||
@@ -1071,7 +1055,7 @@ const onRemoveCollection = () => {
|
||||
const collectionIndex = editingCollectionIndex.value
|
||||
|
||||
const collectionToRemove =
|
||||
collectionIndex || collectionIndex == 0
|
||||
collectionIndex || collectionIndex === 0
|
||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||
collectionIndex,
|
||||
])
|
||||
@@ -1470,9 +1454,8 @@ const checkIfCollectionIsAParentOfTheChildren = (
|
||||
)
|
||||
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -1493,9 +1476,8 @@ const isMoveToSameLocation = (
|
||||
|
||||
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1675,25 +1657,22 @@ const isSameSameParent = (
|
||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||
|
||||
return dragedItemParent.join("/") === destinationCollectionIndex
|
||||
} else {
|
||||
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
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1835,33 +1814,6 @@ const updateCollectionOrder = (payload: {
|
||||
}
|
||||
}
|
||||
// Import - Export Collection functions
|
||||
/**
|
||||
* Export the whole my collection or specific team collection to JSON
|
||||
*/
|
||||
const getJSONCollection = async () => {
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
collectionJSON.value = JSON.stringify(myCollections.value, null, 2)
|
||||
} else {
|
||||
if (!collectionsType.value.selectedTeam) return
|
||||
exportingTeamCollections.value = true
|
||||
pipe(
|
||||
await getTeamCollectionJSON(collectionsType.value.selectedTeam.id),
|
||||
E.match(
|
||||
(err) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
exportingTeamCollections.value = false
|
||||
},
|
||||
(result) => {
|
||||
const { exportCollectionsToJSON } = result
|
||||
collectionJSON.value = exportCollectionsToJSON
|
||||
exportingTeamCollections.value = false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return collectionJSON.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a downloadable file from a collection and prompts the user to download it.
|
||||
@@ -1930,90 +1882,6 @@ const exportData = async (
|
||||
}
|
||||
}
|
||||
|
||||
const exportJSONCollection = async () => {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
await getJSONCollection()
|
||||
|
||||
const parsedCollections = JSON.parse(collectionJSON.value)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
initializeDownloadCollection(collectionJSON.value, null)
|
||||
}
|
||||
|
||||
const createCollectionGist = async () => {
|
||||
if (!currentUser.value || !currentUser.value.accessToken) {
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "gist",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
creatingGistCollection.value = true
|
||||
await getJSONCollection()
|
||||
|
||||
pipe(
|
||||
createCollectionGists(collectionJSON.value, currentUser.value.accessToken),
|
||||
TE.match(
|
||||
(err) => {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
console.error(err)
|
||||
creatingGistCollection.value = false
|
||||
},
|
||||
(result) => {
|
||||
toast.success(t("export.gist_created").toString())
|
||||
creatingGistCollection.value = false
|
||||
window.open(result.data.html_url)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
||||
if (!hasTeamWriteAccess.value) {
|
||||
toast.error(t("team.no_access").toString())
|
||||
return
|
||||
}
|
||||
|
||||
if (!collectionsType.value.selectedTeam) return
|
||||
|
||||
importingMyCollections.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "import-to-teams",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
pipe(
|
||||
importJSONToTeam(
|
||||
JSON.stringify(collection),
|
||||
collectionsType.value.selectedTeam.id
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
importingMyCollections.value = false
|
||||
},
|
||||
() => {
|
||||
importingMyCollections.value = false
|
||||
displayModalImportExport(false)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
|
||||
if (currentUser.value) {
|
||||
// opens the share request modal
|
||||
@@ -2054,37 +1922,36 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
case "bug/team_coll/no_coll_id":
|
||||
case "team_req/invalid_target_id":
|
||||
return t("team.invalid_coll_id")
|
||||
case "team/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_found":
|
||||
return t("team.no_request_found")
|
||||
case "bug/team_req/no_req_id":
|
||||
return t("team.no_request_found")
|
||||
case "team/collection_is_parent_coll":
|
||||
return t("team.parent_coll_move")
|
||||
case "team/target_and_destination_collection_are_same":
|
||||
return t("team.same_target_destination")
|
||||
case "team/target_collection_is_already_root_collection":
|
||||
return t("collection.invalid_root_move")
|
||||
case "team_req/requests_not_from_same_collection":
|
||||
return t("request.different_collection")
|
||||
case "team/team_collections_have_different_parents":
|
||||
return t("collection.different_parent")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
case "bug/team_coll/no_coll_id":
|
||||
case "team_req/invalid_target_id":
|
||||
return t("team.invalid_coll_id")
|
||||
case "team/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_found":
|
||||
return t("team.no_request_found")
|
||||
case "bug/team_req/no_req_id":
|
||||
return t("team.no_request_found")
|
||||
case "team/collection_is_parent_coll":
|
||||
return t("team.parent_coll_move")
|
||||
case "team/target_and_destination_collection_are_same":
|
||||
return t("team.same_target_destination")
|
||||
case "team/target_collection_is_already_root_collection":
|
||||
return t("collection.invalid_root_move")
|
||||
case "team_req/requests_not_from_same_collection":
|
||||
return t("request.different_collection")
|
||||
case "team/team_collections_have_different_parents":
|
||||
return t("collection.different_parent")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,15 +205,14 @@ const addEnvironment = async () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,154 +1,60 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('environment.title')}`"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<span>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:icon="IconGithub"
|
||||
:label="t('import.from_gist')"
|
||||
@click="
|
||||
() => {
|
||||
readEnvironmentGist()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
!currentUser
|
||||
? `${t('export.require_github')}`
|
||||
: currentUser.provider !== 'github.com'
|
||||
? `${t('export.require_github')}`
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:disabled="
|
||||
!currentUser
|
||||
? true
|
||||
: currentUser.provider !== 'github.com'
|
||||
? true
|
||||
: false
|
||||
"
|
||||
:icon="IconGithub"
|
||||
:label="t('export.create_secret_gist')"
|
||||
@click="
|
||||
() => {
|
||||
createEnvironmentGist()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('import.from_json')"
|
||||
@click="openDialogChooseFileToImportFrom"
|
||||
/>
|
||||
<input
|
||||
ref="inputChooseFileToImportFrom"
|
||||
class="input"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
@change="importFromJSON"
|
||||
/>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.as_json')"
|
||||
@click="exportJSON"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="environment.title"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
import axios from "axios"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
environments$,
|
||||
replaceEnvironments,
|
||||
appendEnvironments,
|
||||
} from "~/newstore/environments"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { appendEnvironments, environments$ } from "~/newstore/environments"
|
||||
|
||||
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
|
||||
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
|
||||
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||
import { computed } from "vue"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { environmentsExporter } from "~/helpers/import-export/export/environments"
|
||||
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
teamEnvironments?: TeamEnvironment[]
|
||||
teamId?: string | undefined
|
||||
environmentType: "MY_ENV" | "TEAM_ENV"
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
||||
const isTeamEnvironment = computed(() => {
|
||||
return props.environmentType === "TEAM_ENV"
|
||||
})
|
||||
|
||||
const environmentJson = computed(() => {
|
||||
if (
|
||||
@@ -158,266 +64,249 @@ const environmentJson = computed(() => {
|
||||
const teamEnvironments = props.teamEnvironments.map(
|
||||
(x) => x.environment as Environment
|
||||
)
|
||||
return JSON.stringify(teamEnvironments, null, 2)
|
||||
} else {
|
||||
return JSON.stringify(myEnvironments.value, null, 2)
|
||||
return teamEnvironments
|
||||
}
|
||||
|
||||
return myEnvironments.value
|
||||
})
|
||||
|
||||
const createEnvironmentGist = async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
const HoppEnvironmentsImport: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.from_json",
|
||||
name: "import.from_json",
|
||||
icon: IconFolderPlus,
|
||||
title: "import.from_json",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: FileSource({
|
||||
acceptedFileTypes: "application/json",
|
||||
caption: "import.hoppscotch_environment_description",
|
||||
onImportFromFile: async (environments) => {
|
||||
const res = await hoppEnvImporter(environments)()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
"https://api.github.com/gists",
|
||||
{
|
||||
files: {
|
||||
"hoppscotch-environments.json": {
|
||||
content: environmentJson.value,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${currentUser.value.accessToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const PostmanEnvironmentsImport: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.from_postman",
|
||||
name: "import.from_postman",
|
||||
icon: IconPostman,
|
||||
title: "import.from_json",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: FileSource({
|
||||
acceptedFileTypes: "application/json",
|
||||
caption: "import.postman_environment_description",
|
||||
onImportFromFile: async (environments) => {
|
||||
const res = await postmanEnvImporter(environments)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
handleImportToStore([res.right])
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const EnvironmentsImportFromGIST: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.environments_from_gist",
|
||||
name: "import.environments_from_gist",
|
||||
icon: IconFolderPlus,
|
||||
title: "import.environments_from_gist",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: GistSource({
|
||||
caption: "import.environments_from_gist_description",
|
||||
onImportFromGist: async (environments) => {
|
||||
if (E.isLeft(environments)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
const res = await hoppEnvImporter(environments.right)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
handleImportToStore(res.right)
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||
})
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const HoppEnvironmentsExport: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "export.as_json",
|
||||
name: "export.as_json",
|
||||
title: "action.download_file",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
},
|
||||
action: () => {
|
||||
if (!environmentJson.value.length) {
|
||||
return toast.error(t("error.no_environments_to_export"))
|
||||
}
|
||||
|
||||
const message = initializeDownloadCollection(
|
||||
environmentsExporter(environmentJson.value),
|
||||
"Environments"
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
if (E.isLeft(message)) {
|
||||
toast.error(t(message.left))
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(t(message.right))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const fileImported = () => {
|
||||
toast.success(t("state.file_imported").toString())
|
||||
}
|
||||
|
||||
const failedImport = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
const readEnvironmentGist = async () => {
|
||||
const gist = prompt(t("import.gist_url").toString())
|
||||
if (!gist) return
|
||||
|
||||
try {
|
||||
const { files } = (await axios.get(
|
||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)) as {
|
||||
files: {
|
||||
[fileName: string]: {
|
||||
content: any
|
||||
}
|
||||
}
|
||||
}
|
||||
const environments = JSON.parse(Object.values(files)[0].content)
|
||||
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
replaceEnvironments(environments)
|
||||
fileImported()
|
||||
} else {
|
||||
importToTeams(environments)
|
||||
}
|
||||
} catch (e) {
|
||||
failedImport()
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const openDialogChooseFileToImportFrom = () => {
|
||||
if (inputChooseFileToImportFrom.value)
|
||||
inputChooseFileToImportFrom.value.click()
|
||||
}
|
||||
|
||||
const importToTeams = async (content: Environment[]) => {
|
||||
loading.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
for (const [i, env] of content.entries()) {
|
||||
if (i === content.length - 1) {
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(env.variables),
|
||||
props.teamId as string,
|
||||
env.name
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
hideModal()
|
||||
fileImported()
|
||||
}
|
||||
)
|
||||
)()
|
||||
} else {
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(env.variables),
|
||||
props.teamId as string,
|
||||
env.name
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
// wait for all the environments to be created then fire the toast
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importFromJSON = () => {
|
||||
if (!inputChooseFileToImportFrom.value) return
|
||||
|
||||
if (
|
||||
!inputChooseFileToImportFrom.value.files ||
|
||||
inputChooseFileToImportFrom.value.files.length === 0
|
||||
) {
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
const content = target!.result as string | null
|
||||
|
||||
if (!content) {
|
||||
toast.show(t("action.choose_file").toString())
|
||||
const HoppEnvironmentsGistExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "export.as_gist",
|
||||
name: "export.create_secret_gist",
|
||||
title:
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
currentUser?.provider === "github.com"
|
||||
? "export.create_secret_gist"
|
||||
: "export.require_github",
|
||||
icon: IconUser,
|
||||
disabled: !currentUser.value
|
||||
? true
|
||||
: currentUser.value.provider !== "github.com",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
},
|
||||
action: async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission"))
|
||||
return
|
||||
}
|
||||
|
||||
const environments = JSON.parse(content)
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
if (
|
||||
environments._postman_variable_scope === "environment" ||
|
||||
environments._postman_variable_scope === "globals"
|
||||
) {
|
||||
importFromPostman(environments)
|
||||
} else if (environments[0]) {
|
||||
const [name, variables] = Object.keys(environments[0])
|
||||
if (name === "name" && variables === "variables") {
|
||||
// Do nothing
|
||||
if (accessToken) {
|
||||
const res = await environmentsGistExporter(
|
||||
JSON.stringify(environmentJson.value),
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("export.failed"))
|
||||
return
|
||||
}
|
||||
importFromHoppscotch(environments)
|
||||
} else {
|
||||
failedImport()
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
toast.success(t("export.success"))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.right, "_blank")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const importFromHoppscotch = (environments: Environment[]) => {
|
||||
const importerModules = [
|
||||
HoppEnvironmentsImport,
|
||||
EnvironmentsImportFromGIST,
|
||||
PostmanEnvironmentsImport,
|
||||
]
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
const enabledExporters = [HoppEnvironmentsExport]
|
||||
|
||||
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||
enabledExporters.push(HoppEnvironmentsGistExporter)
|
||||
}
|
||||
|
||||
return enabledExporters
|
||||
})
|
||||
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
const handleImportToStore = async (environments: Environment[]) => {
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
appendEnvironments(environments)
|
||||
fileImported()
|
||||
toast.success(t("state.file_imported"))
|
||||
} else {
|
||||
importToTeams(environments)
|
||||
await importToTeams(environments)
|
||||
}
|
||||
}
|
||||
|
||||
const importFromPostman = ({
|
||||
name,
|
||||
values,
|
||||
}: {
|
||||
name: string
|
||||
values: { key: string; value: string }[]
|
||||
}) => {
|
||||
const environment: Environment = { name, variables: [] }
|
||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
||||
const environments = [environment]
|
||||
const importToTeams = async (content: Environment[]) => {
|
||||
const envImportPromises: Promise<
|
||||
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
|
||||
>[] = []
|
||||
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
for (const [, env] of content.entries()) {
|
||||
const res = createTeamEnvironment(
|
||||
JSON.stringify(env.variables),
|
||||
props.teamId as string,
|
||||
env.name
|
||||
)()
|
||||
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = environmentJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_environments_to_export"))
|
||||
envImportPromises.push(res)
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const url = URL.createObjectURL(file)
|
||||
const res = await Promise.all(envImportPromises)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
const failedImports = res.some((r) => E.isLeft(r))
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
if (failedImports) {
|
||||
toast.error(t("import.failed"))
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
toast.success(t("import.success"))
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -453,12 +453,11 @@ const isEnvActive = (id: string | number) => {
|
||||
} else {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
return selectedEnv.value.index === id
|
||||
} else {
|
||||
return (
|
||||
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnv.value.teamEnvID === id
|
||||
)
|
||||
}
|
||||
return (
|
||||
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnv.value.teamEnvID === id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,40 +502,36 @@ const selectedEnv = computed(() => {
|
||||
name: props.modelValue.environment.environment.name,
|
||||
teamEnvID: props.modelValue.environment.id,
|
||||
}
|
||||
} else {
|
||||
return { type: "global", name: "Global" }
|
||||
}
|
||||
} else {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
const environment =
|
||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.value.index,
|
||||
name: environment.name,
|
||||
variables: environment.variables,
|
||||
}
|
||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find(
|
||||
(env) =>
|
||||
env.id ===
|
||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnvironmentIndex.value.teamEnvID)
|
||||
)
|
||||
if (teamEnv) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
variables: teamEnv.environment.variables,
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
return { type: "global", name: "Global" }
|
||||
}
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
const environment =
|
||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.value.index,
|
||||
name: environment.name,
|
||||
variables: environment.variables,
|
||||
}
|
||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find(
|
||||
(env) =>
|
||||
env.id ===
|
||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnvironmentIndex.value.teamEnvID)
|
||||
)
|
||||
if (teamEnv) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
variables: teamEnv.environment.variables,
|
||||
}
|
||||
}
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
})
|
||||
|
||||
// Set the selected environment as initial scope value
|
||||
@@ -584,13 +579,12 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,9 +593,8 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
|
||||
const environmentVariables = computed(() => {
|
||||
if (selectedEnv.value.variables) {
|
||||
return selectedEnv.value.variables
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const editGlobalEnv = () => {
|
||||
|
||||
@@ -198,9 +198,8 @@ const workingEnv = computed(() => {
|
||||
type: "MY_ENV",
|
||||
index: props.editingEnvironmentIndex,
|
||||
})
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||
@@ -226,12 +225,11 @@ const liveEnvs = computed(() => {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
}
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
environment-type="MY_ENV"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
|
||||
@@ -205,11 +205,8 @@ const evnExpandError = computed(() => {
|
||||
const liveEnvs = computed(() => {
|
||||
if (evnExpandError.value) {
|
||||
return []
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
}
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -338,13 +335,12 @@ const hideModal = () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -184,13 +184,12 @@ const duplicateEnvironments = () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
:team-environments="teamEnvironments"
|
||||
:team-id="team?.id"
|
||||
environment-type="TEAM_ENV"
|
||||
@@ -174,13 +174,12 @@ const resetSelectedData = () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const isScalar = computed(() => {
|
||||
|
||||
function resolveRootType(type: GraphQLType) {
|
||||
let t = type as any
|
||||
while (t.ofType != null) t = t.ofType
|
||||
while (t.ofType !== null) t = t.ofType
|
||||
return t
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,8 @@ const duration = computed(() => {
|
||||
return responseDuration > 0
|
||||
? `${t("request.duration")}: ${responseDuration}ms`
|
||||
: t("error.no_duration")
|
||||
} else return t("error.no_duration")
|
||||
}
|
||||
return t("error.no_duration")
|
||||
})
|
||||
|
||||
const entryStatus = computed(() => {
|
||||
|
||||
@@ -339,7 +339,7 @@ const deleteBodyParam = (index: number) => {
|
||||
}
|
||||
|
||||
workingParams.value = workingParams.value.filter(
|
||||
(_, arrIndex) => arrIndex != index
|
||||
(_, arrIndex) => arrIndex !== index
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -214,10 +214,9 @@ const requestCode = computed(() => {
|
||||
if (O.isSome(result)) {
|
||||
errorState.value = false
|
||||
return result.value
|
||||
} else {
|
||||
errorState.value = true
|
||||
return ""
|
||||
}
|
||||
errorState.value = true
|
||||
return ""
|
||||
})
|
||||
|
||||
// Template refs
|
||||
|
||||
@@ -126,19 +126,19 @@ const linewrapEnabled = ref(true)
|
||||
const rawBodyParameters = ref<any | null>(null)
|
||||
|
||||
const codemirrorValue: Ref<string | undefined> =
|
||||
typeof rawParamsBody.value == "string"
|
||||
typeof rawParamsBody.value === "string"
|
||||
? ref(rawParamsBody.value)
|
||||
: ref(undefined)
|
||||
|
||||
watch(rawParamsBody, (newVal) => {
|
||||
typeof newVal == "string"
|
||||
typeof newVal === "string"
|
||||
? (codemirrorValue.value = newVal)
|
||||
: (codemirrorValue.value = undefined)
|
||||
})
|
||||
|
||||
// propagate the edits from codemirror back to the body
|
||||
watch(codemirrorValue, (updatedValue) => {
|
||||
if (updatedValue && updatedValue != rawParamsBody.value) {
|
||||
if (updatedValue && updatedValue !== rawParamsBody.value) {
|
||||
rawParamsBody.value = updatedValue
|
||||
}
|
||||
})
|
||||
@@ -185,7 +185,7 @@ const prettifyRequestBody = () => {
|
||||
if (body.value.contentType.endsWith("json")) {
|
||||
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
||||
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
||||
} else if (body.value.contentType == "application/xml") {
|
||||
} else if (body.value.contentType === "application/xml") {
|
||||
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||
}
|
||||
rawParamsBody.value = prettifyBody
|
||||
|
||||
@@ -242,7 +242,7 @@ const urlEncodedParamsRaw = pluckRef(body, "body")
|
||||
|
||||
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
||||
get() {
|
||||
return typeof urlEncodedParamsRaw.value == "string"
|
||||
return typeof urlEncodedParamsRaw.value === "string"
|
||||
? parseRawKeyValueEntries(urlEncodedParamsRaw.value)
|
||||
: []
|
||||
},
|
||||
|
||||
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
dialog
|
||||
:title="t(modalTitle)"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasPreviousStep"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.go_back')"
|
||||
:icon="IconArrowLeft"
|
||||
@click="goToPreviousStep"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<component :is="currentStep.component" v-bind="currentStep.props()" />
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { PropType, ref } from "vue"
|
||||
|
||||
import { useSteps, defineStep } from "~/composables/step-components"
|
||||
import ImportExportList from "./ImportExportList.vue"
|
||||
|
||||
import ImportExportSourcesList from "./ImportExportSourcesList.vue"
|
||||
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
importerModules: {
|
||||
// type: Array as PropType<ReturnType<typeof defineImporter>[]>,
|
||||
type: Array as PropType<ImporterOrExporter[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
exporterModules: {
|
||||
type: Array as PropType<ImporterOrExporter[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
modalTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
addStep,
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
hasPreviousStep,
|
||||
} = useSteps()
|
||||
|
||||
const selectedImporterID = ref<string | null>(null)
|
||||
const selectedSourceID = ref<string | null>(null)
|
||||
|
||||
const chooseImporterOrExporter = defineStep(
|
||||
"choose_importer_or_exporter",
|
||||
ImportExportList,
|
||||
() => ({
|
||||
importers: props.importerModules.map((importer) => ({
|
||||
id: importer.metadata.id,
|
||||
name: importer.metadata.name,
|
||||
title: importer.metadata.title,
|
||||
icon: importer.metadata.icon,
|
||||
disabled: importer.metadata.disabled,
|
||||
})),
|
||||
exporters: props.exporterModules.map((exporter) => ({
|
||||
id: exporter.metadata.id,
|
||||
name: exporter.metadata.name,
|
||||
title: exporter.metadata.title,
|
||||
icon: exporter.metadata.icon,
|
||||
disabled: exporter.metadata.disabled,
|
||||
loading: exporter.metadata.isLoading?.value ?? false,
|
||||
})),
|
||||
"onImporter-selected": (id: string) => {
|
||||
selectedImporterID.value = id
|
||||
|
||||
const selectedImporter = props.importerModules.find(
|
||||
(i) => i.metadata.id === id
|
||||
)
|
||||
|
||||
if (selectedImporter?.supported_sources) goToNextStep()
|
||||
else if (selectedImporter?.component)
|
||||
goToStep(selectedImporter.component.id)
|
||||
},
|
||||
"onExporter-selected": (id: string) => {
|
||||
const selectedExporter = props.exporterModules.find(
|
||||
(i) => i.metadata.id === id
|
||||
)
|
||||
|
||||
if (selectedExporter && selectedExporter.action) {
|
||||
selectedExporter.action()
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const chooseImportSource = defineStep(
|
||||
"choose_import_source",
|
||||
ImportExportSourcesList,
|
||||
() => {
|
||||
const currentImporter = props.importerModules.find(
|
||||
(i) => i.metadata.id === selectedImporterID.value
|
||||
)
|
||||
|
||||
const sources = currentImporter?.supported_sources
|
||||
|
||||
if (!sources)
|
||||
return {
|
||||
sources: [],
|
||||
}
|
||||
|
||||
sources.forEach((source) => {
|
||||
addStep(source.step)
|
||||
})
|
||||
|
||||
return {
|
||||
sources: sources.map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
icon: source.icon,
|
||||
})),
|
||||
"onImport-source-selected": (sourceID) => {
|
||||
selectedSourceID.value = sourceID
|
||||
|
||||
const sourceStep = sources.find((s) => s.id === sourceID)?.step
|
||||
|
||||
if (sourceStep) {
|
||||
goToStep(sourceStep.id)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
addStep(chooseImporterOrExporter)
|
||||
addStep(chooseImportSource)
|
||||
|
||||
props.importerModules.forEach((importer) => {
|
||||
if (importer.component) {
|
||||
addStep(importer.component)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const hideModal = () => {
|
||||
// resetImport()
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartExpand>
|
||||
<template #body>
|
||||
<HoppSmartItem
|
||||
v-for="importer in importers"
|
||||
:key="importer.id"
|
||||
:icon="importer.icon"
|
||||
:label="t(`${importer.name}`)"
|
||||
@click="emit('importer-selected', importer.id)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartExpand>
|
||||
<hr />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<template v-for="exporter in exporters" :key="exporter.id">
|
||||
<!-- adding the title to a span if the item is visible, otherwise the title won't be shown -->
|
||||
|
||||
<span
|
||||
v-if="exporter.disabled && exporter.title"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t(`${exporter.title}`)"
|
||||
class="flex"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="exporter.icon"
|
||||
:label="t(`${exporter.name}`)"
|
||||
:disabled="exporter.disabled"
|
||||
:loading="exporter.loading"
|
||||
@click="emit('exporter-selected', exporter.id)"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<HoppSmartItem
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="exporter.icon"
|
||||
:title="t(`${exporter.title}`)"
|
||||
:label="t(`${exporter.name}`)"
|
||||
:loading="exporter.loading"
|
||||
:disabled="exporter.disabled"
|
||||
@click="emit('exporter-selected', exporter.id)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Component } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type ImportExportEntryMeta = {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
disabled: boolean
|
||||
title?: string
|
||||
loading?: boolean
|
||||
isVisible?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
importers: ImportExportEntryMeta[]
|
||||
exporters: ImportExportEntryMeta[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importer-selected", importerID: string): void
|
||||
(e: "exporter-selected", exporterID: string): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="source in sources"
|
||||
:key="source.id"
|
||||
:icon="source.icon"
|
||||
:label="t(`${source.name}`)"
|
||||
@click="emit('import-source-selected', source.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Component } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type ListItemMeta = {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
title?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
sources: ListItemMeta[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "import-source-selected", sourceID: string): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasFile,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
||||
>
|
||||
<input
|
||||
id="inputChooseFileToImportFrom"
|
||||
ref="inputChooseFileToImportFrom"
|
||||
name="inputChooseFileToImportFrom"
|
||||
type="file"
|
||||
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
||||
:accept="acceptedFileTypes"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasFile"
|
||||
@click="emit('importFromFile', fileContent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
defineProps<{
|
||||
caption: string
|
||||
acceptedFileTypes: string
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const hasFile = ref(false)
|
||||
const fileContent = ref("")
|
||||
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromFile", content: string): void
|
||||
}>()
|
||||
|
||||
const onFileChange = () => {
|
||||
const inputFileToImport = inputChooseFileToImportFrom.value
|
||||
|
||||
if (!inputFileToImport) {
|
||||
hasFile.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||
inputChooseFileToImportFrom.value[0].value = ""
|
||||
hasFile.value = false
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
const content = target!.result as string | null
|
||||
if (!content) {
|
||||
hasFile.value = false
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
fileContent.value = content
|
||||
|
||||
hasFile.value = !!content?.length
|
||||
}
|
||||
|
||||
reader.readAsText(inputFileToImport.files[0])
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
v-model="mySelectedCollectionID"
|
||||
autocomplete="off"
|
||||
class="select"
|
||||
autofocus
|
||||
>
|
||||
<option :key="undefined" :value="undefined" disabled selected>
|
||||
{{ t("collection.select") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(collection, collectionIndex) in myCollections"
|
||||
:key="`collection-${collectionIndex}`"
|
||||
:value="collectionIndex"
|
||||
class="bg-primary"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasSelectedCollectionID"
|
||||
@click="fetchCollectionFromMyCollections"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { getRESTCollection, restCollections$ } from "~/newstore/collections"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
||||
|
||||
const hasSelectedCollectionID = computed(() => {
|
||||
return mySelectedCollectionID.value !== undefined
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromMyCollection", content: HoppCollection<HoppRESTRequest>): void
|
||||
}>()
|
||||
|
||||
const fetchCollectionFromMyCollections = async () => {
|
||||
if (mySelectedCollectionID.value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const collection = getRESTCollection(mySelectedCollectionID.value)
|
||||
|
||||
if (collection) {
|
||||
emit("importFromMyCollection", collection)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasURL,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(caption) }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="flex flex-col ml-10">
|
||||
<input
|
||||
v-model="inputChooseGistToImportFrom"
|
||||
type="url"
|
||||
class="input"
|
||||
:placeholder="`${t('import.from_url')}`"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasURL"
|
||||
:loading="isFetchingUrl"
|
||||
@click="fetchUrlData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
caption: string
|
||||
fetchLogic?: (url: string) => Promise<AxiosResponse<any>>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromURL", content: unknown): void
|
||||
}>()
|
||||
|
||||
const inputChooseGistToImportFrom = ref<string>("")
|
||||
const hasURL = ref(false)
|
||||
|
||||
const isFetchingUrl = ref(false)
|
||||
|
||||
watch(inputChooseGistToImportFrom, (url) => {
|
||||
hasURL.value = !!url
|
||||
})
|
||||
|
||||
const urlFetchLogic =
|
||||
props.fetchLogic ??
|
||||
async function (url: string) {
|
||||
const res = await axios.get(url, {
|
||||
transitional: {
|
||||
forcedJSONParsing: false,
|
||||
silentJSONParsing: false,
|
||||
clarifyTimeoutError: true,
|
||||
},
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function fetchUrlData() {
|
||||
isFetchingUrl.value = true
|
||||
|
||||
try {
|
||||
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
|
||||
|
||||
if (res.status === 200) {
|
||||
emit("importFromURL", res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(t("import.failed"))
|
||||
console.log(e)
|
||||
} finally {
|
||||
isFetchingUrl.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, Ref } from "vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
// TODO: move the metadata except disabled and isLoading to importers.ts
|
||||
export type ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: string
|
||||
name: string
|
||||
icon: any
|
||||
title: string
|
||||
disabled: boolean
|
||||
applicableTo: Array<"personal-workspace" | "team-workspace" | "url-import">
|
||||
isLoading?: Ref<boolean>
|
||||
}
|
||||
supported_sources?: {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
step: ReturnType<typeof defineStep>
|
||||
}[]
|
||||
component?: ReturnType<typeof defineStep>
|
||||
action?: (...args: any[]) => any
|
||||
}
|
||||
@@ -313,9 +313,8 @@ const jsonResponseBodyText = computed(() => {
|
||||
),
|
||||
E.map(JSON.stringify)
|
||||
)
|
||||
} else {
|
||||
return E.right(responseBodyText.value)
|
||||
}
|
||||
return E.right(responseBodyText.value)
|
||||
})
|
||||
|
||||
const jsonBodyText = computed(() =>
|
||||
|
||||
@@ -5,12 +5,11 @@ export default {
|
||||
computed: {
|
||||
responseBodyText() {
|
||||
if (typeof this.response.body === "string") return this.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(this.response.body)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
const res = new TextDecoder("utf-8").decode(this.response.body)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="myTeams.length"
|
||||
class="flex flex-col space-y-2 rounded-lg border border-red-500 bg-info p-4 text-secondaryDark"
|
||||
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
class="mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 bg-info p-4 text-secondaryDark"
|
||||
class="bg-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
@@ -173,8 +173,8 @@ const deleteUserAccount = async () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -269,12 +269,12 @@ const ast = computed(() =>
|
||||
|
||||
const editorText = computed(() => {
|
||||
if (selectedTab.value === "json") return jsonBodyText.value
|
||||
else return logPayload.value
|
||||
return logPayload.value
|
||||
})
|
||||
|
||||
const editorMode = computed(() => {
|
||||
if (selectedTab.value === "json") return "application/ld+json"
|
||||
else return "text/plain"
|
||||
return "text/plain"
|
||||
})
|
||||
|
||||
const { cursor } = useCodemirror(
|
||||
|
||||
@@ -124,9 +124,8 @@ onClickOutside(autoCompleteWrapper, () => {
|
||||
const uniqueAutoCompleteSource = computed(() => {
|
||||
if (props.autoCompleteSource) {
|
||||
return [...new Set(props.autoCompleteSource)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const suggestions = computed(() => {
|
||||
@@ -139,9 +138,8 @@ const suggestions = computed(() => {
|
||||
return uniqueAutoCompleteSource.value.filter((suggestion) =>
|
||||
suggestion.toLowerCase().includes(props.modelValue.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
return uniqueAutoCompleteSource.value ?? []
|
||||
}
|
||||
return uniqueAutoCompleteSource.value ?? []
|
||||
})
|
||||
|
||||
const updateModelValue = (value: string) => {
|
||||
|
||||
@@ -276,7 +276,8 @@ const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({
|
||||
},
|
||||
},
|
||||
]
|
||||
} else return []
|
||||
}
|
||||
return []
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -570,17 +570,16 @@ const sendInvites = async () => {
|
||||
const getErrorMessage = (error: SendInvitesErrorType) => {
|
||||
if (error.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (error.error) {
|
||||
case "team/invalid_id":
|
||||
return t("team.invalid_id")
|
||||
case "team/member_not_found":
|
||||
return t("team.member_not_found")
|
||||
case "team_invite/already_member":
|
||||
return t("team.already_member")
|
||||
case "team_invite/member_has_invite":
|
||||
return t("team.member_has_invite")
|
||||
}
|
||||
}
|
||||
switch (error.error) {
|
||||
case "team/invalid_id":
|
||||
return t("team.invalid_id")
|
||||
case "team/member_not_found":
|
||||
return t("team.member_not_found")
|
||||
case "team_invite/already_member":
|
||||
return t("team.already_member")
|
||||
case "team_invite/member_has_invite":
|
||||
return t("team.member_has_invite")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,8 @@ const maxMembersHardLimit = 6
|
||||
const slicedTeamMembers = computed(() => {
|
||||
if (props.showCount && props.teamMembers.length > maxMembersSoftLimit) {
|
||||
return props.teamMembers.slice(0, maxMembersSoftLimit)
|
||||
} else {
|
||||
return props.teamMembers
|
||||
}
|
||||
return props.teamMembers
|
||||
})
|
||||
|
||||
const remainingSlicedMembers = computed(
|
||||
|
||||
@@ -155,11 +155,10 @@ export function useResponseBody(response: HoppRESTResponse): {
|
||||
)
|
||||
return ""
|
||||
if (typeof response.body === "string") return response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
|
||||
const res = new TextDecoder("utf-8").decode(response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
})
|
||||
return {
|
||||
responseBodyText,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { computed, defineComponent, ref } from "vue"
|
||||
|
||||
export function useSteps() {
|
||||
type Step = ReturnType<typeof defineStep>
|
||||
|
||||
const steps: Step[] = []
|
||||
|
||||
const currentStepIndex = ref(0)
|
||||
|
||||
const currentStep = computed(() => {
|
||||
return steps[currentStepIndex.value]
|
||||
})
|
||||
|
||||
const backHistoryIndexes = ref([0])
|
||||
|
||||
const hasPreviousStep = computed(() => {
|
||||
return currentStepIndex.value > 0
|
||||
})
|
||||
|
||||
const addStep = (step: Step) => {
|
||||
steps.push(step)
|
||||
}
|
||||
|
||||
const goToNextStep = () => {
|
||||
currentStepIndex.value++
|
||||
backHistoryIndexes.value.push(currentStepIndex.value)
|
||||
}
|
||||
|
||||
const goToStep = (stepId: string) => {
|
||||
currentStepIndex.value = steps.findIndex((step) => step.id === stepId)
|
||||
backHistoryIndexes.value.push(currentStepIndex.value)
|
||||
}
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
if (backHistoryIndexes.value.length !== 1) {
|
||||
backHistoryIndexes.value.pop()
|
||||
currentStepIndex.value =
|
||||
backHistoryIndexes.value[backHistoryIndexes.value.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
steps,
|
||||
currentStep,
|
||||
addStep,
|
||||
goToPreviousStep,
|
||||
goToNextStep,
|
||||
goToStep,
|
||||
hasPreviousStep,
|
||||
}
|
||||
}
|
||||
|
||||
export function defineStep<
|
||||
StepComponent extends ReturnType<typeof defineComponent>,
|
||||
>(
|
||||
id: string,
|
||||
component: StepComponent,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
props: () => InstanceType<StepComponent>["$props"]
|
||||
) {
|
||||
const step = {
|
||||
id,
|
||||
component,
|
||||
props,
|
||||
}
|
||||
|
||||
return step
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export function translateExtURLParams(
|
||||
initialReq?: HoppRESTRequest
|
||||
): HoppRESTRequest {
|
||||
if (urlParams.v) return parseV1ExtURL(urlParams, initialReq)
|
||||
else return parseV0ExtURL(urlParams, initialReq)
|
||||
return parseV0ExtURL(urlParams, initialReq)
|
||||
}
|
||||
|
||||
function parseV0ExtURL(
|
||||
|
||||
@@ -325,7 +325,8 @@ export const runAuthOnlyGQLSubscription = flow(
|
||||
) {
|
||||
sub.unsubscribe()
|
||||
return null
|
||||
} else return res
|
||||
}
|
||||
return res
|
||||
}),
|
||||
filter((res): res is Exclude<typeof res, null> => res !== null)
|
||||
)
|
||||
|
||||
@@ -132,17 +132,10 @@ export const teamCollToHoppRESTColl = (
|
||||
* @param teamID - ID of the team
|
||||
* @returns Either of the JSON string of the collection or the error
|
||||
*/
|
||||
export const getTeamCollectionJSON = async (teamID: string) => {
|
||||
const data = await runGQLQuery({
|
||||
export const getTeamCollectionJSON = async (teamID: string) =>
|
||||
await runGQLQuery({
|
||||
query: ExportAsJsonDocument,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
return E.left(data.left)
|
||||
}
|
||||
|
||||
return E.right(data.right)
|
||||
}
|
||||
|
||||
@@ -164,11 +164,10 @@ export function getFoldersByPath(
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.folders
|
||||
} else {
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
|
||||
return currentCollection.folders
|
||||
|
||||
@@ -63,11 +63,10 @@ export function getRequestsByPath(
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.requests
|
||||
} else {
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
|
||||
return currentCollection.requests
|
||||
|
||||
@@ -51,7 +51,7 @@ const getMethodByDeduction = (parsedArguments: parser.Arguments) => {
|
||||
)(parsedArguments)
|
||||
)
|
||||
return O.some("POST")
|
||||
else return O.none
|
||||
return O.none
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,8 @@ const linter: LinterDefinition = (text) => {
|
||||
severity: "error",
|
||||
},
|
||||
])
|
||||
} else {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
export default linter
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import axios from "axios"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
/**
|
||||
* Create an gist on GitHub with the collection JSON
|
||||
* @param collectionJSON - JSON string of the collection
|
||||
* @param accessToken - GitHub access token
|
||||
* @returns Either of the response of the GitHub Gist API or the error
|
||||
*/
|
||||
export const createCollectionGists = (
|
||||
collectionJSON: string,
|
||||
export const createGist = (
|
||||
content: string,
|
||||
filename: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
return TE.tryCatch(
|
||||
@@ -17,8 +12,8 @@ export const createCollectionGists = (
|
||||
"https://api.github.com/gists",
|
||||
{
|
||||
files: {
|
||||
"hoppscotch-collections.json": {
|
||||
content: collectionJSON,
|
||||
[filename]: {
|
||||
content: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
export const environmentsExporter = (myEnvironments: Environment[]) => {
|
||||
return JSON.stringify(myEnvironments, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import { createGist } from "~/helpers/gist"
|
||||
|
||||
export const environmentsGistExporter = async (
|
||||
environmentsJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
const res = await createGist(
|
||||
environmentsJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(res.right.data.html_url as string)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createGist } from "~/helpers/gist"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export const collectionsGistExporter = async (
|
||||
collectionJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
if (!accessToken) {
|
||||
return E.left("Invalid User")
|
||||
}
|
||||
|
||||
const res = await createGist(
|
||||
collectionJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(true)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
|
||||
export const gqlCollectionsExporter = (
|
||||
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||
) => {
|
||||
return JSON.stringify(gqlCollections, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import { createGist } from "~/helpers/gist"
|
||||
|
||||
export const gqlCollectionsGistExporter = async (
|
||||
gqlCollectionsJSON: string,
|
||||
accessToken: string
|
||||
) => {
|
||||
const res = await createGist(
|
||||
gqlCollectionsJSON,
|
||||
"hoppscotch-collections.json",
|
||||
accessToken
|
||||
)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left(res.left)
|
||||
}
|
||||
return E.right(res.right.data.html_url as string)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppExporter } from "."
|
||||
|
||||
const exporter: HoppExporter<HoppCollection<HoppRESTRequest>> = (content) =>
|
||||
pipe(content, JSON.stringify, TE.right)
|
||||
|
||||
export default exporter
|
||||
@@ -1,18 +1,32 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export type HoppExporter<T> = (content: T) => TE.TaskEither<string, string>
|
||||
/**
|
||||
* 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 = (
|
||||
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
|
||||
|
||||
export type HoppExporterDefinition<T> = {
|
||||
name: string
|
||||
exporter: () => Promise<HoppExporter<T>>
|
||||
if (name) {
|
||||
a.download = `${name}.json`
|
||||
} else {
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
}
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
return E.right("state.download_started")
|
||||
}
|
||||
|
||||
export const RESTCollectionExporters: HoppExporterDefinition<
|
||||
HoppCollection<HoppRESTRequest>
|
||||
>[] = [
|
||||
{
|
||||
name: "Hoppscotch REST Collection JSON",
|
||||
exporter: () => import("./hopp").then((m) => m.default),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
|
||||
export const myCollectionsExporter = (
|
||||
myCollections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
return JSON.stringify(myCollections, null, 2)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||
|
||||
export const teamCollectionsExporter = (teamID: string) => {
|
||||
return getTeamCollectionJSON(teamID)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import * as O from "fp-ts/Option"
|
||||
import axios from "axios"
|
||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
// TODO: Add validation to output
|
||||
const fetchGist = (
|
||||
url: string
|
||||
): TO.TaskOption<HoppCollection<HoppRESTRequest>[]> =>
|
||||
pipe(
|
||||
TO.tryCatch(() =>
|
||||
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
})
|
||||
),
|
||||
TO.chain((res) =>
|
||||
pipe(
|
||||
O.tryCatch(() =>
|
||||
JSON.parse((Object.values(res.data.files)[0] as any).content)
|
||||
),
|
||||
TO.fromOption
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
id: "gist",
|
||||
name: "import.from_gist",
|
||||
icon: IconGithub,
|
||||
applicableTo: ["my-collections", "team-collections"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "URL_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_gist_description",
|
||||
placeholder: "import.gist_url",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) =>
|
||||
pipe(
|
||||
fetchGist(content),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
@@ -9,38 +8,23 @@ import {
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { isPlainObject as _isPlainObject } from "lodash-es"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
export default defineImporter({
|
||||
id: "hoppscotch",
|
||||
name: "import.from_json",
|
||||
icon: IconFolderPlus,
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_json_description",
|
||||
acceptedFileTypes: "application/json",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) =>
|
||||
pipe(
|
||||
safeParseJSON(content),
|
||||
O.chain(
|
||||
flow(
|
||||
makeCollectionsArray,
|
||||
RA.map(validateCollection),
|
||||
O.sequenceArray,
|
||||
O.map(RA.toArray)
|
||||
)
|
||||
),
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
export const hoppRESTImporter = (content: string) =>
|
||||
pipe(
|
||||
safeParseJSON(content),
|
||||
O.chain(
|
||||
flow(
|
||||
makeCollectionsArray,
|
||||
RA.map(validateCollection),
|
||||
O.sequenceArray,
|
||||
O.map(RA.toArray)
|
||||
)
|
||||
),
|
||||
})
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
/**
|
||||
* checks if a value is a plain object
|
||||
@@ -63,9 +47,8 @@ const isValidCollection = (
|
||||
const validateCollection = (collection: unknown) => {
|
||||
if (isValidCollection(collection)) {
|
||||
return O.some(collection)
|
||||
} else {
|
||||
return O.some(translateToNewRESTCollection(collection))
|
||||
}
|
||||
return O.some(translateToNewRESTCollection(collection))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
const hoppEnvSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const hoppEnvImporter = (content: string) => {
|
||||
const parsedContent = safeParseJSON(content)
|
||||
|
||||
// parse json from the environments string
|
||||
if (O.isNone(parsedContent)) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const environments = validationResult.data
|
||||
|
||||
return TE.right(environments)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
// TODO: add zod validation
|
||||
export const hoppGqlCollectionsImporter = (
|
||||
content: string
|
||||
): E.Either<"INVALID_JSON", HoppCollection<HoppGQLRequest>[]> => {
|
||||
return E.tryCatch(
|
||||
() => JSON.parse(content) as HoppCollection<HoppGQLRequest>[],
|
||||
() => "INVALID_JSON"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function FileSource(metadata: {
|
||||
acceptedFileTypes: string
|
||||
caption: string
|
||||
onImportFromFile: (content: string) => any | Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, FileImportVue, () => ({
|
||||
acceptedFileTypes: metadata.acceptedFileTypes,
|
||||
caption: metadata.caption,
|
||||
onImportFromFile: metadata.onImportFromFile,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import axios from "axios"
|
||||
import UrlImport from "~/components/importExport/ImportExportSteps/UrlImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { z } from "zod"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function GistSource(metadata: {
|
||||
caption: string
|
||||
onImportFromGist: (
|
||||
importResult: E.Either<string, string>
|
||||
) => any | Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, UrlImport, () => ({
|
||||
caption: metadata.caption,
|
||||
onImportFromURL: (gistResponse) => {
|
||||
const fileSchema = z.object({
|
||||
files: z.record(z.object({ content: z.string() })),
|
||||
})
|
||||
|
||||
const parseResult = fileSchema.safeParse(gistResponse)
|
||||
|
||||
if (!parseResult.success) {
|
||||
metadata.onImportFromGist(E.left("INVALID_GIST"))
|
||||
return
|
||||
}
|
||||
|
||||
const content = Object.values(parseResult.data.files)[0].content
|
||||
|
||||
metadata.onImportFromGist(E.right(content))
|
||||
},
|
||||
fetchLogic: fetchGistFromUrl,
|
||||
}))
|
||||
}
|
||||
|
||||
const fetchGistFromUrl = (url: string) =>
|
||||
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import UrlImport from "~/components/importExport/ImportExportSteps/UrlImport.vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
export function UrlSource(metadata: {
|
||||
caption: string
|
||||
onImportFromURL: (content: string) => any | Promise<any>
|
||||
fetchLogic?: (url: string) => Promise<any>
|
||||
}) {
|
||||
const stepID = uuidv4()
|
||||
|
||||
return defineStep(stepID, UrlImport, () => ({
|
||||
caption: metadata.caption,
|
||||
onImportFromURL: (content: unknown) => {
|
||||
if (typeof content === "string") {
|
||||
metadata.onImportFromURL(content)
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -1,22 +1,5 @@
|
||||
import HoppRESTCollImporter from "./hopp"
|
||||
import OpenAPIImporter from "./openapi"
|
||||
import PostmanImporter from "./postman"
|
||||
import InsomniaImporter from "./insomnia"
|
||||
import GistImporter from "./gist"
|
||||
import MyCollectionsImporter from "./myCollections"
|
||||
|
||||
export const RESTCollectionImporters = [
|
||||
HoppRESTCollImporter,
|
||||
OpenAPIImporter,
|
||||
PostmanImporter,
|
||||
InsomniaImporter,
|
||||
GistImporter,
|
||||
MyCollectionsImporter,
|
||||
] as const
|
||||
|
||||
export const URLImporters = [
|
||||
HoppRESTCollImporter,
|
||||
OpenAPIImporter,
|
||||
PostmanImporter,
|
||||
InsomniaImporter,
|
||||
] as const
|
||||
export { hoppRESTImporter } from "./hopp"
|
||||
export { hoppOpenAPIImporter } from "./openapi"
|
||||
export { hoppPostmanImporter } from "./postman"
|
||||
export { hoppInsomniaImporter } from "./insomnia"
|
||||
export { toTeamsImporter } from "./myCollections"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
import { convert, ImportRequest } from "insomnia-importers"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
@@ -16,8 +15,7 @@ import * as A from "fp-ts/Array"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
|
||||
|
||||
@@ -210,27 +208,10 @@ const getHoppCollections = (doc: InsomniaDoc) =>
|
||||
getHoppFolder(f, doc.data.resources)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
id: "insomnia",
|
||||
name: "import.from_insomnia",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconInsomnia,
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_insomnia_description",
|
||||
acceptedFileTypes: ".json, .yaml",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([fileContent]) =>
|
||||
pipe(
|
||||
fileContent,
|
||||
parseInsomniaDoc,
|
||||
|
||||
TO.map(getHoppCollections),
|
||||
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
export const hoppInsomniaImporter = (fileContent: string) =>
|
||||
pipe(
|
||||
fileContent,
|
||||
parseInsomniaDoc,
|
||||
TO.map(getHoppCollections),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter } from "."
|
||||
import { getRESTCollection } from "~/newstore/collections"
|
||||
import { importJSONToTeam } from "~/helpers/backend/mutations/TeamCollection"
|
||||
|
||||
export default defineImporter({
|
||||
id: "myCollections",
|
||||
name: "import.from_my_collections",
|
||||
icon: IconUser,
|
||||
applicableTo: ["team-collections"],
|
||||
steps: [
|
||||
step({
|
||||
stepName: "TARGET_MY_COLLECTION",
|
||||
metadata: {
|
||||
caption: "import.from_my_collections_description",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) => pipe(content, getRESTCollection, A.of, TE.of),
|
||||
})
|
||||
export function toTeamsImporter(content: string, teamID: string) {
|
||||
return importJSONToTeam(content, teamID)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconOpenAPI from "~icons/lucide/file"
|
||||
import {
|
||||
OpenAPI,
|
||||
OpenAPIV2,
|
||||
@@ -25,8 +24,7 @@ import * as S from "fp-ts/string"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||
|
||||
@@ -167,14 +165,12 @@ const parseOpenAPIV3BodyFormData = (
|
||||
contentType,
|
||||
body: keys.map((key) => `${key}: `).join("\n"),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) =>
|
||||
<FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) => <FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +229,9 @@ const resolveOpenAPIV3SecurityObj = (
|
||||
} else if (scheme.scheme === "bearer") {
|
||||
// Bearer
|
||||
return { authType: "bearer", authActive: true, token: "" }
|
||||
} else {
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
} else if (scheme.type === "apiKey") {
|
||||
if (scheme.in === "header") {
|
||||
return {
|
||||
@@ -301,17 +296,16 @@ const resolveOpenAPIV3SecurityObj = (
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.type === "openIdConnect") {
|
||||
return {
|
||||
@@ -339,7 +333,7 @@ const resolveOpenAPIV3SecurityScheme = (
|
||||
| undefined
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
else return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3Security = (
|
||||
@@ -439,17 +433,16 @@ const resolveOpenAPIV2SecurityScheme = (
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,44 +607,29 @@ const parseOpenAPIDocContent = (str: string) =>
|
||||
)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
id: "openapi",
|
||||
name: "import.from_openapi",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconOpenAPI,
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_openapi_description",
|
||||
acceptedFileTypes: ".json, .yaml, .yml",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([fileContent]) =>
|
||||
pipe(
|
||||
// See if we can parse JSON properly
|
||||
fileContent,
|
||||
parseOpenAPIDocContent,
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||
// Try validating, else the importer is invalid file format
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.validate(obj),
|
||||
() => IMPORTER_INVALID_FILE_FORMAT
|
||||
)
|
||||
export const hoppOpenAPIImporter = (fileContent: string) =>
|
||||
pipe(
|
||||
// See if we can parse JSON properly
|
||||
fileContent,
|
||||
parseOpenAPIDocContent,
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||
// Try validating, else the importer is invalid file format
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.validate(obj),
|
||||
() => IMPORTER_INVALID_FILE_FORMAT
|
||||
)
|
||||
),
|
||||
// Deference the references
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.dereference(obj),
|
||||
() => OPENAPI_DEREF_ERROR
|
||||
)
|
||||
)
|
||||
),
|
||||
TE.chainW(convertOpenApiDocToHopp)
|
||||
)
|
||||
),
|
||||
})
|
||||
// Deference the references
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.dereference(obj),
|
||||
() => OPENAPI_DEREF_ERROR
|
||||
)
|
||||
)
|
||||
),
|
||||
TE.chainW(convertOpenApiDocToHopp)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import {
|
||||
Collection as PMCollection,
|
||||
Item,
|
||||
@@ -25,8 +24,7 @@ import * as S from "fp-ts/string"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { PMRawLanguage } from "~/types/pm-coll-exts"
|
||||
import { stringArrayJoin } from "~/helpers/functional/array"
|
||||
|
||||
@@ -298,28 +296,13 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
|
||||
|
||||
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
|
||||
|
||||
export default defineImporter({
|
||||
id: "postman",
|
||||
name: "import.from_postman",
|
||||
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||
icon: IconPostman,
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_postman_description",
|
||||
acceptedFileTypes: ".json",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([fileContent]) =>
|
||||
pipe(
|
||||
// Try reading
|
||||
fileContent,
|
||||
readPMCollection,
|
||||
export const hoppPostmanImporter = (fileContent: string) =>
|
||||
pipe(
|
||||
// Try reading
|
||||
fileContent,
|
||||
readPMCollection,
|
||||
|
||||
O.map(flow(getHoppCollection, A.of)),
|
||||
O.map(flow(getHoppCollection, A.of)),
|
||||
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
const postmanEnvSchema = z.object({
|
||||
name: z.string(),
|
||||
values: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const postmanEnvImporter = (content: string) => {
|
||||
const parsedContent = safeParseJSON(content)
|
||||
|
||||
// parse json from the environments string
|
||||
if (O.isNone(parsedContent)) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = postmanEnvSchema.safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const postmanEnv = validationResult.data
|
||||
|
||||
const environment: Environment = {
|
||||
name: postmanEnv.name,
|
||||
variables: [],
|
||||
}
|
||||
|
||||
postmanEnv.values.forEach(({ key, value }) =>
|
||||
environment.variables.push({ key, value })
|
||||
)
|
||||
|
||||
return TE.right(environment)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Defines which type of content a Step returns.
|
||||
* Add an entry here when you define a step
|
||||
*/
|
||||
export type StepDefinition = {
|
||||
FILE_IMPORT: {
|
||||
returnType: string
|
||||
metadata: {
|
||||
caption: string
|
||||
acceptedFileTypes: string
|
||||
}
|
||||
} // String content of the file
|
||||
TARGET_MY_COLLECTION: {
|
||||
returnType: number
|
||||
metadata: {
|
||||
caption: string
|
||||
}
|
||||
} // folderPath
|
||||
URL_IMPORT: {
|
||||
returnType: string
|
||||
metadata: {
|
||||
caption: string
|
||||
placeholder: string
|
||||
}
|
||||
} // String content of the url
|
||||
}
|
||||
|
||||
export type StepReturnValue = StepDefinition[keyof StepDefinition]["returnType"]
|
||||
|
||||
/**
|
||||
* Defines what the data structure of a step
|
||||
*/
|
||||
export type Step<T extends keyof StepDefinition> =
|
||||
StepDefinition[T]["metadata"] extends never
|
||||
? {
|
||||
name: T
|
||||
caption?: string
|
||||
metadata: undefined
|
||||
}
|
||||
: {
|
||||
name: T
|
||||
caption?: string
|
||||
metadata: StepDefinition[T]["metadata"]
|
||||
}
|
||||
|
||||
/**
|
||||
* The return output value of an individual step
|
||||
*/
|
||||
export type StepReturnType<T> = T extends Step<infer U>
|
||||
? StepDefinition[U]["returnType"]
|
||||
: never
|
||||
|
||||
export type StepMetadata<T> = T extends Step<infer U>
|
||||
? StepDefinition[U]["metadata"]
|
||||
: never
|
||||
|
||||
/**
|
||||
* Defines the value of the output list generated by a step
|
||||
*/
|
||||
export type StepsOutputList<T> = {
|
||||
[K in keyof T]: StepReturnType<T[K]>
|
||||
}
|
||||
|
||||
type StepFuncInput<T extends keyof StepDefinition> =
|
||||
StepDefinition[T]["metadata"] extends never
|
||||
? {
|
||||
stepName: T
|
||||
caption?: string
|
||||
}
|
||||
: {
|
||||
stepName: T
|
||||
caption?: string
|
||||
metadata: StepDefinition[T]["metadata"]
|
||||
}
|
||||
|
||||
/** Use this function to define a step */
|
||||
export const step = <T extends keyof StepDefinition>(input: StepFuncInput<T>) =>
|
||||
<Step<T>>{
|
||||
name: input.stepName,
|
||||
metadata: (input as any).metadata ?? undefined,
|
||||
caption: input.caption,
|
||||
}
|
||||
@@ -62,27 +62,25 @@ const buildHarPostParams = (
|
||||
),
|
||||
RA.toArray
|
||||
)
|
||||
} else {
|
||||
// FormData has its own format
|
||||
return req.body.body.flatMap((entry) => {
|
||||
if (entry.isFile) {
|
||||
// We support multiple files
|
||||
return entry.value.map(
|
||||
(file) =>
|
||||
<Har.Param>{
|
||||
name: entry.key,
|
||||
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
|
||||
contentType: file.type,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
name: entry.key,
|
||||
value: entry.value,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// FormData has its own format
|
||||
return req.body.body.flatMap((entry) => {
|
||||
if (entry.isFile) {
|
||||
// We support multiple files
|
||||
return entry.value.map(
|
||||
(file) =>
|
||||
<Har.Param>{
|
||||
name: entry.key,
|
||||
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
|
||||
contentType: file.type,
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
name: entry.key,
|
||||
value: entry.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const buildHarPostData = (req: HoppRESTRequest): Har.PostData | undefined => {
|
||||
|
||||
@@ -319,7 +319,7 @@ export default class NewTeamCollectionAdapter {
|
||||
|
||||
if (!parentCollection) return
|
||||
|
||||
if (parentCollection.children != null) {
|
||||
if (parentCollection.children !== null) {
|
||||
parentCollection.children.push(collection)
|
||||
} else {
|
||||
parentCollection.children = [collection]
|
||||
@@ -997,7 +997,7 @@ export default class NewTeamCollectionAdapter {
|
||||
|
||||
if (!collection) return
|
||||
|
||||
if (collection.children != null) return
|
||||
if (collection.children !== null) return
|
||||
|
||||
this.loadingCollections$.next([
|
||||
...this.loadingCollections$.getValue(),
|
||||
|
||||
@@ -31,7 +31,8 @@ const applyColorMode = (app: App) => {
|
||||
const selection = computed<Exclude<HoppBgColor, "system">>(() => {
|
||||
if (currentLocalPreference.value === "system") {
|
||||
return systemPrefersDark.value ? "dark" : "light"
|
||||
} else return currentLocalPreference.value
|
||||
}
|
||||
return currentLocalPreference.value
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -1342,7 +1342,7 @@ function removeDuplicateCollectionsFromPath<
|
||||
: undefined
|
||||
|
||||
if (collectionPath && parentCollection) {
|
||||
if (type == "collection") {
|
||||
if (type === "collection") {
|
||||
parentCollection.folders = removeDuplicatesFromAnArrayById(
|
||||
idToRemove,
|
||||
parentCollection.folders
|
||||
@@ -1367,7 +1367,7 @@ function removeDuplicateCollectionsFromPath<
|
||||
(entry) => entry.id === idToRemove
|
||||
)
|
||||
|
||||
if (duplicateEntries.length == 2) {
|
||||
if (duplicateEntries.length === 2) {
|
||||
const duplicateEntryIndex = arrayWithID.findIndex(
|
||||
(entry) => entry.id === idToRemove
|
||||
)
|
||||
|
||||
@@ -47,18 +47,16 @@ const dispatchers = defineDispatchers({
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selectedEnvironmentIndex: {
|
||||
type: "NO_ENV_SELECTED",
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
selectedEnvironmentIndex: {
|
||||
type: "NO_ENV_SELECTED",
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
}
|
||||
},
|
||||
appendEnvironments(
|
||||
{ environments }: EnvironmentStore,
|
||||
@@ -309,7 +307,7 @@ const dispatchers = defineDispatchers({
|
||||
|
||||
const newEnvironments = [...environments]
|
||||
|
||||
if (entries.length == 2) {
|
||||
if (entries.length === 2) {
|
||||
const indexToRemove = environments.findIndex((e) => e.id === id)
|
||||
newEnvironments.splice(indexToRemove, 1)
|
||||
}
|
||||
@@ -350,9 +348,8 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
||||
return env
|
||||
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
return environments[selectedEnvironmentIndex.index]
|
||||
} else {
|
||||
return selectedEnvironmentIndex.environment
|
||||
}
|
||||
return selectedEnvironmentIndex.environment
|
||||
})
|
||||
)
|
||||
|
||||
@@ -422,9 +419,8 @@ export function getCurrentEnvironment(): Environment {
|
||||
return environmentsStore.value.environments[
|
||||
environmentsStore.value.selectedEnvironmentIndex.index
|
||||
]
|
||||
} else {
|
||||
return environmentsStore.value.selectedEnvironmentIndex.environment
|
||||
}
|
||||
return environmentsStore.value.selectedEnvironmentIndex.environment
|
||||
}
|
||||
|
||||
export function getSelectedEnvironmentIndex() {
|
||||
@@ -469,7 +465,7 @@ export function getLocalIndexByEnvironmentID(id: string) {
|
||||
(env) => env.id === id
|
||||
)
|
||||
|
||||
return envIndex != -1 ? envIndex : null
|
||||
return envIndex !== -1 ? envIndex : null
|
||||
}
|
||||
|
||||
export function addGlobalEnvVariable(entry: Environment["variables"][number]) {
|
||||
@@ -678,10 +674,9 @@ export function getEnvironment(selectedEnv: SelectedEnv) {
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV"
|
||||
) {
|
||||
return environmentsStore.value.selectedEnvironmentIndex.environment
|
||||
} else {
|
||||
return {
|
||||
name: "N0_ENV",
|
||||
variables: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: "N0_ENV",
|
||||
variables: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ const RESTHistoryDispatchers = defineDispatchers({
|
||||
removeDuplicateEntry(currentVal: RESTHistoryType, { id }: { id: string }) {
|
||||
const entries = currentVal.state.filter((e) => e.id === id)
|
||||
|
||||
if (entries.length == 2) {
|
||||
if (entries.length === 2) {
|
||||
const indexToRemove = currentVal.state.findIndex((e) => e.id === id)
|
||||
|
||||
currentVal.state.splice(indexToRemove, 1)
|
||||
@@ -230,7 +230,7 @@ const GQLHistoryDispatchers = defineDispatchers({
|
||||
removeDuplicateEntry(currentVal: GraphqlHistoryType, { id }: { id: string }) {
|
||||
const entries = currentVal.state.filter((e) => e.id === id)
|
||||
|
||||
if (entries.length == 2) {
|
||||
if (entries.length === 2) {
|
||||
const indexToRemove = currentVal.state.findIndex((e) => e.id === id)
|
||||
|
||||
currentVal.state.splice(indexToRemove, 1)
|
||||
|
||||
@@ -16,12 +16,18 @@ import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { appendRESTCollections } from "~/newstore/collections"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { URLImporters } from "~/helpers/import-export/import/importers"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "~/helpers/import-export/import"
|
||||
import { OPENAPI_DEREF_ERROR } from "~/helpers/import-export/import/openapi"
|
||||
import { isOfType } from "~/helpers/functional/primtive"
|
||||
import { TELeftType } from "~/helpers/functional/taskEither"
|
||||
|
||||
import {
|
||||
hoppRESTImporter,
|
||||
hoppPostmanImporter,
|
||||
hoppInsomniaImporter,
|
||||
hoppOpenAPIImporter,
|
||||
} from "~/helpers/import-export/import/importers"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
@@ -30,16 +36,33 @@ const t = useI18n()
|
||||
const IMPORTER_INVALID_TYPE = "importer_invalid_type" as const
|
||||
const IMPORTER_INVALID_FETCH = "importer_invalid_fetch" as const
|
||||
|
||||
// TODO: move this to importers after moving the importer metadatas from respective components to imports/*.ts file
|
||||
const URLImporters = [
|
||||
{
|
||||
id: "hoppscotch",
|
||||
importer: hoppRESTImporter,
|
||||
},
|
||||
{
|
||||
id: "postman",
|
||||
importer: hoppPostmanImporter,
|
||||
},
|
||||
{
|
||||
id: "insomnia",
|
||||
importer: hoppInsomniaImporter,
|
||||
},
|
||||
{
|
||||
id: "openapi",
|
||||
importer: hoppOpenAPIImporter,
|
||||
},
|
||||
]
|
||||
|
||||
const importCollections = (url: unknown, type: unknown) =>
|
||||
pipe(
|
||||
TE.Do,
|
||||
TE.bind("importer", () =>
|
||||
pipe(
|
||||
URLImporters,
|
||||
RA.findFirst(
|
||||
(importer) =>
|
||||
importer.applicableTo.includes("url-import") && importer.id === type
|
||||
),
|
||||
RA.findFirst((importer) => importer.id === type),
|
||||
TE.fromOption(() => IMPORTER_INVALID_TYPE)
|
||||
)
|
||||
),
|
||||
@@ -56,7 +79,7 @@ const importCollections = (url: unknown, type: unknown) =>
|
||||
content.data,
|
||||
TO.fromPredicate(isOfType("string")),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||
TE.chain((data) => importer.importer([data]))
|
||||
TE.chain((data) => importer.importer(data))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -210,7 +210,7 @@ export default defineComponent({
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
else if (!currentUser.value) return true
|
||||
else return false
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -264,21 +264,20 @@ export default defineComponent({
|
||||
getErrorMessage(error: GQLError<GetInviteDetailsError>) {
|
||||
if (error.type === "network_error") {
|
||||
return this.t("error.network_error")
|
||||
} else {
|
||||
switch (error.error) {
|
||||
case "team_invite/not_valid_viewer":
|
||||
return this.t("team.not_valid_viewer")
|
||||
case "team_invite/not_found":
|
||||
return this.t("team.not_found")
|
||||
case "team_invite/no_invite_found":
|
||||
return this.t("team.no_invite_found")
|
||||
case "team_invite/already_member":
|
||||
return this.t("team.already_member")
|
||||
case "team_invite/email_do_not_match":
|
||||
return this.t("team.email_do_not_match")
|
||||
default:
|
||||
return this.t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
switch (error.error) {
|
||||
case "team_invite/not_valid_viewer":
|
||||
return this.t("team.not_valid_viewer")
|
||||
case "team_invite/not_found":
|
||||
return this.t("team.not_found")
|
||||
case "team_invite/no_invite_found":
|
||||
return this.t("team.no_invite_found")
|
||||
case "team_invite/already_member":
|
||||
return this.t("team.already_member")
|
||||
case "team_invite/email_do_not_match":
|
||||
return this.t("team.email_do_not_match")
|
||||
default:
|
||||
return this.t("error.something_went_wrong")
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -235,7 +235,7 @@ const probableUser = useReadonlyStream(
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
else if (!currentUser.value) return true
|
||||
else return false
|
||||
return false
|
||||
})
|
||||
|
||||
const displayName = ref(currentUser.value?.displayName || "")
|
||||
|
||||
@@ -76,15 +76,14 @@ async function runRequest(
|
||||
})
|
||||
} else if (axios.isCancel(e)) {
|
||||
return E.left("cancellation")
|
||||
} else {
|
||||
return E.left(<InterceptorError>{
|
||||
humanMessage: {
|
||||
heading: (t) => t("error.network_fail"),
|
||||
description: (t) => t("helpers.network_fail"),
|
||||
},
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
return E.left(<InterceptorError>{
|
||||
humanMessage: {
|
||||
heading: (t) => t("error.network_fail"),
|
||||
description: (t) => t("helpers.network_fail"),
|
||||
},
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,8 @@ export class ExtensionInterceptorService
|
||||
public extensionVersion = computed(() => {
|
||||
if (this.extensionStatus.value === "available") {
|
||||
return window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -197,11 +196,10 @@ export class ExtensionInterceptorService
|
||||
if (this.extensionStatus.value === "available" && version) {
|
||||
const { major, minor } = version
|
||||
return `${t("settings.extensions")}: v${major}.${minor}`
|
||||
} else {
|
||||
return `${t("settings.extensions")}: ${t(
|
||||
"settings.extension_ver_not_reported"
|
||||
)}`
|
||||
}
|
||||
return `${t("settings.extensions")}: ${t(
|
||||
"settings.extension_ver_not_reported"
|
||||
)}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -80,15 +80,14 @@ async function runRequest(
|
||||
} catch (e) {
|
||||
if (axios.isCancel(e)) {
|
||||
return E.left("cancellation")
|
||||
} else {
|
||||
return E.left({
|
||||
humanMessage: {
|
||||
heading: (t) => t("error.network_fail"),
|
||||
description: (t) => t("helpers.network_fail"),
|
||||
},
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
return E.left({
|
||||
humanMessage: {
|
||||
heading: (t) => t("error.network_fail"),
|
||||
description: (t) => t("helpers.network_fail"),
|
||||
},
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,8 @@ export class CollectionsSpotlightSearcherService
|
||||
return "graphql"
|
||||
} else if (url.pathname === "/") {
|
||||
return "rest"
|
||||
} else {
|
||||
return "other"
|
||||
}
|
||||
return "other"
|
||||
} catch (e) {
|
||||
return "other"
|
||||
}
|
||||
|
||||
@@ -190,23 +190,22 @@ export class HistorySpotlightSearcherService
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Assume gql
|
||||
const entry =
|
||||
graphqlHistoryStore.value.state[parseInt(x.id.split("-")[1])]
|
||||
}
|
||||
// Assume gql
|
||||
const entry =
|
||||
graphqlHistoryStore.value.state[parseInt(x.id.split("-")[1])]
|
||||
|
||||
return {
|
||||
id: x.id,
|
||||
icon: markRaw(IconHistory),
|
||||
score: x.score,
|
||||
text: {
|
||||
type: "custom",
|
||||
component: markRaw(SpotlightGQLHistoryEntry),
|
||||
componentProps: {
|
||||
historyEntry: entry,
|
||||
},
|
||||
return {
|
||||
id: x.id,
|
||||
icon: markRaw(IconHistory),
|
||||
score: x.score,
|
||||
text: {
|
||||
type: "custom",
|
||||
component: markRaw(SpotlightGQLHistoryEntry),
|
||||
componentProps: {
|
||||
historyEntry: entry,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user