refactor: revamp the importers & exporters systems to be reused (#3425)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Akash K
2023-12-06 21:24:29 +05:30
committed by GitHub
parent d9c75ed79e
commit ab7c29d228
90 changed files with 2399 additions and 1892 deletions

View File

@@ -69,5 +69,7 @@ module.exports = {
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
eqeqeq: 1,
"no-else-return": 1,
},
}

View File

@@ -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",

View File

@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
return { severity: 0 }
})
const severityColor = (severity: number) => {

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -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: [],
}
})
}

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -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: [],
}
})
}
}

View File

@@ -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 = () => {

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -137,7 +137,7 @@
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsGraphqlImportExport
:show="showModalImportExport"
v-if="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</div>

View File

@@ -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")
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = () => {

View File

@@ -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(

View File

@@ -68,7 +68,7 @@
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
:show="showModalImportExport"
v-if="showModalImportExport"
environment-type="MY_ENV"
@hide-modal="displayModalImportExport(false)"
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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(() => {

View File

@@ -339,7 +339,7 @@ const deleteBodyParam = (index: number) => {
}
workingParams.value = workingParams.value.filter(
(_, arrIndex) => arrIndex != index
(_, arrIndex) => arrIndex !== index
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
: []
},

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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(() =>

View File

@@ -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+$/, "")
},
},
}

View File

@@ -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>

View File

@@ -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(

View File

@@ -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) => {

View File

@@ -276,7 +276,8 @@ const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({
},
},
]
} else return []
}
return []
}),
})

View File

@@ -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")
}
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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)
)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -51,7 +51,7 @@ const getMethodByDeduction = (parsedArguments: parser.Arguments) => {
)(parsedArguments)
)
return O.some("POST")
else return O.none
return O.none
}
/**

View File

@@ -16,9 +16,8 @@ const linter: LinterDefinition = (text) => {
severity: "error",
},
])
} else {
return Promise.resolve([])
}
return Promise.resolve([])
}
export default linter

View File

@@ -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,
},
},
},

View File

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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -0,0 +1,7 @@
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
export const gqlCollectionsExporter = (
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
return JSON.stringify(gqlCollections, null, 2)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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),
},
]

View File

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

View File

@@ -0,0 +1,5 @@
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
export const teamCollectionsExporter = (teamID: string) => {
return getTeamCollectionJSON(teamID)
}

View File

@@ -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)
),
})

View File

@@ -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))
}
/**

View File

@@ -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)
}

View File

@@ -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"
)
}

View File

@@ -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,
}))
}

View File

@@ -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",
},
})

View File

@@ -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)
}
},
}))
}

View File

@@ -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"

View File

@@ -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)
)

View File

@@ -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)
}

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
}

View File

@@ -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,
}

View File

@@ -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 => {

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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
)

View File

@@ -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: [],
}
}

View File

@@ -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)

View File

@@ -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))
)
)
)

View File

@@ -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")
}
},
},

View File

@@ -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 || "")

View File

@@ -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,
})
}
}

View File

@@ -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"
)}`
})
}

View File

@@ -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,
})
}
}

View File

@@ -117,9 +117,8 @@ export class CollectionsSpotlightSearcherService
return "graphql"
} else if (url.pathname === "/") {
return "rest"
} else {
return "other"
}
return "other"
} catch (e) {
return "other"
}

View File

@@ -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,
},
}
},
}
})
},