Merge branch 'refactor/import-export'
This commit is contained in:
1
packages/hoppscotch-app/assets/icons/insomnia.svg
Normal file
1
packages/hoppscotch-app/assets/icons/insomnia.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M12 0C5.417 0 0 5.417 0 12s5.417 12 12 12 12-5.417 12-12S18.583 0 12 0zm0 2.478c5.256 0 9.522 4.266 9.522 9.522S17.256 21.522 12 21.522 2.478 17.256 2.478 12c0-.885.12-1.741.347-2.554a4.76 4.76 0 0 0 3.925 2.066 4.764 4.764 0 0 0 4.762-4.762 4.758 4.758 0 0 0-2.067-3.925A9.526 9.526 0 0 1 12 2.478Z"/></svg>
|
||||
|
After Width: | Height: | Size: 398 B |
1
packages/hoppscotch-app/assets/icons/postman.svg
Normal file
1
packages/hoppscotch-app/assets/icons/postman.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M13.527.099C6.955-.744.942 3.9.099 10.473c-.843 6.572 3.8 12.584 10.373 13.428 6.573.843 12.587-3.801 13.428-10.374C24.744 6.955 20.101.943 13.527.099zm2.471 7.485a.855.855 0 0 0-.593.25l-4.453 4.453-.307-.307-.643-.643c4.389-4.376 5.18-4.418 5.996-3.753zm-4.863 4.861 4.44-4.44a.62.62 0 1 1 .847.903l-4.699 4.125-.588-.588zm.33.694-1.1.238a.06.06 0 0 1-.067-.032.06.06 0 0 1 .01-.073l.645-.645.512.512zm-2.803-.459 1.172-1.172.879.878-1.979.426a.074.074 0 0 1-.085-.039.072.072 0 0 1 .013-.093zm-3.646 6.058a.076.076 0 0 1-.069-.083.077.077 0 0 1 .022-.046h.002l.946-.946 1.222 1.222-2.123-.147zm2.425-1.256a.228.228 0 0 0-.117.256l.203.865a.125.125 0 0 1-.211.117h-.003l-.934-.934-.294-.295 3.762-3.758 1.82-.393.874.874c-1.255 1.102-2.971 2.201-5.1 3.268zm5.279-3.428h-.002l-.839-.839 4.699-4.125a.952.952 0 0 0 .119-.127c-.148 1.345-2.029 3.245-3.977 5.091zm3.657-6.46-.003-.002a1.822 1.822 0 0 1 2.459-2.684l-1.61 1.613a.119.119 0 0 0 0 .169l1.247 1.247a1.817 1.817 0 0 1-2.093-.343zm2.578 0a1.714 1.714 0 0 1-.271.218h-.001l-1.207-1.207 1.533-1.533c.661.72.637 1.832-.054 2.522zm-.1-1.544a.143.143 0 0 0-.053.157.416.416 0 0 1-.053.45.14.14 0 0 0 .023.197.141.141 0 0 0 .084.03.14.14 0 0 0 .106-.05.691.691 0 0 0 .087-.751.138.138 0 0 0-.194-.033z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
packages/hoppscotch-app/assets/icons/upload.svg
Normal file
5
packages/hoppscotch-app/assets/icons/upload.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -1,50 +1,131 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
:title="`${t('modal.import_export')} ${t('modal.collections')}`"
|
||||
:title="`${t('modal.collections')}`"
|
||||
max-width="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonSecondary
|
||||
v-if="mode == 'import_from_my_collections'"
|
||||
v-if="importerType !== null"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.go_back')"
|
||||
svg="arrow-left"
|
||||
@click.native="
|
||||
() => {
|
||||
mode = 'import_export'
|
||||
mySelectedCollectionID = undefined
|
||||
}
|
||||
"
|
||||
@click.native="resetImport"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="
|
||||
mode == 'import_export' && collectionsType.type == 'my-collections'
|
||||
"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
svg="more-vertical"
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="importerType !== null" class="flex flex-col">
|
||||
<div class="flex pb-6 flex-col px-2">
|
||||
<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="inline-flex border-4 border-primary items-center justify-center flex-shrink-0 mr-4 rounded-full text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasFile,
|
||||
}"
|
||||
>
|
||||
<i class="material-icons">check_circle</i>
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${step.metadata.caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="flex flex-col ml-10">
|
||||
<input
|
||||
id="inputChooseFileToImportFrom"
|
||||
ref="inputChooseFileToImportFrom"
|
||||
name="inputChooseFileToImportFrom"
|
||||
type="file"
|
||||
class="transition cursor-pointer 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="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="inline-flex border-4 border-primary items-center justify-center flex-shrink-0 mr-4 rounded-full text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasGist,
|
||||
}"
|
||||
>
|
||||
<i class="material-icons">check_circle</i>
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${step.metadata.caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="flex flex-col ml-10">
|
||||
<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 px-2"
|
||||
>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
v-model="mySelectedCollectionID"
|
||||
type="text"
|
||||
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"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
:label="t('import.title')"
|
||||
:disabled="enableImportButton"
|
||||
class="mx-2"
|
||||
:loading="importingMyCollections"
|
||||
@click.native="finishImport"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col px-2">
|
||||
<SmartExpand>
|
||||
<template #body>
|
||||
<SmartItem
|
||||
v-for="(importer, index) in importerModules"
|
||||
:key="`importer-${index}`"
|
||||
:svg="importer.icon"
|
||||
:label="t(`${importer.name}`)"
|
||||
@click.native="importerType = index"
|
||||
/>
|
||||
</template>
|
||||
</SmartExpand>
|
||||
<hr />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<SmartItem
|
||||
icon="assignment_returned"
|
||||
:label="t('import.from_gist')"
|
||||
@click.native="
|
||||
() => {
|
||||
readCollectionGist()
|
||||
options.tippy().hide()
|
||||
}
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
svg="download"
|
||||
:label="t('export.as_json')"
|
||||
@click.native="exportJSON"
|
||||
/>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -55,6 +136,7 @@
|
||||
? `${t('export.require_github')}`
|
||||
: undefined
|
||||
"
|
||||
class="flex"
|
||||
>
|
||||
<SmartItem
|
||||
:disabled="
|
||||
@@ -64,120 +146,24 @@
|
||||
? true
|
||||
: false
|
||||
"
|
||||
icon="assignment_turned_in"
|
||||
svg="github"
|
||||
:label="t('export.create_secret_gist')"
|
||||
@click.native="
|
||||
() => {
|
||||
createCollectionGist()
|
||||
options.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</tippy>
|
||||
</span>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="mode == 'import_export'" class="flex flex-col space-y-2 px-2">
|
||||
<SmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.preserve_current')"
|
||||
svg="folder-plus"
|
||||
:label="t('import.json')"
|
||||
@click.native="openDialogChooseFileToImportFrom"
|
||||
/>
|
||||
<input
|
||||
ref="inputChooseFileToImportFrom"
|
||||
class="input"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
@change="importFromJSON"
|
||||
/>
|
||||
<SmartItem
|
||||
v-if="collectionsType.type == 'team-collections'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.preserve_current')"
|
||||
svg="user"
|
||||
:label="t('import.from_my_collections')"
|
||||
@click.native="mode = 'import_from_my_collections'"
|
||||
/>
|
||||
<SmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.replace_current')"
|
||||
svg="file"
|
||||
:label="t('action.replace_json')"
|
||||
@click.native="openDialogChooseFileToReplaceWith"
|
||||
/>
|
||||
<input
|
||||
ref="inputChooseFileToReplaceWith"
|
||||
class="input"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
@change="replaceWithJSON"
|
||||
/>
|
||||
<hr />
|
||||
<SmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.download_file')"
|
||||
svg="download"
|
||||
:label="t('export.as_json')"
|
||||
@click.native="exportJSON"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="mode == 'import_from_my_collections'"
|
||||
class="flex flex-col px-2"
|
||||
>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="select"
|
||||
autofocus
|
||||
@change="
|
||||
($event) => {
|
||||
mySelectedCollectionID = $event.target.value
|
||||
}
|
||||
"
|
||||
>
|
||||
<option
|
||||
:key="undefined"
|
||||
:value="undefined"
|
||||
hidden
|
||||
disabled
|
||||
selected
|
||||
>
|
||||
Select Collection
|
||||
</option>
|
||||
<option
|
||||
v-for="(collection, index) in myCollections"
|
||||
:key="`collection-${index}`"
|
||||
:value="index"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div v-if="mode == 'import_from_my_collections'">
|
||||
<span>
|
||||
<ButtonPrimary
|
||||
:disabled="mySelectedCollectionID == undefined"
|
||||
svg="folder-plus"
|
||||
:label="t('import.title')"
|
||||
@click.native="importFromMyCollections"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { computed, ref, watch } from "@nuxtjs/composition-api"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { apolloClient } from "~/helpers/apollo"
|
||||
import {
|
||||
useAxios,
|
||||
@@ -187,14 +173,14 @@ import {
|
||||
} from "~/helpers/utils/composables"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import * as teamUtils from "~/helpers/teams/utils"
|
||||
import { parseInsomniaCollection } from "~/helpers/utils/parseInsomniaCollection"
|
||||
import {
|
||||
restCollections$,
|
||||
setRESTCollections,
|
||||
appendRESTCollections,
|
||||
Collection,
|
||||
makeCollection,
|
||||
restCollections$,
|
||||
} from "~/newstore/collections"
|
||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||
import { HoppRESTRequest } from "~/../hoppscotch-data/dist"
|
||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
@@ -219,14 +205,12 @@ const t = useI18n()
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
|
||||
const mode = ref("import_export")
|
||||
const mySelectedCollectionID = ref(undefined)
|
||||
const collectionJson = ref("")
|
||||
|
||||
// Template refs
|
||||
const options = ref<any>()
|
||||
const inputChooseFileToReplaceWith = ref<HTMLInputElement>()
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
||||
const mode = ref("import_export")
|
||||
const mySelectedCollectionID = ref<undefined | number>(undefined)
|
||||
const collectionJson = ref("")
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||
const inputChooseGistToImportFrom = ref<string>("")
|
||||
|
||||
const getJSONCollection = async () => {
|
||||
if (props.collectionsType.type === "my-collections") {
|
||||
@@ -284,376 +268,44 @@ 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)
|
||||
setRESTCollections(collections)
|
||||
fileImported()
|
||||
} catch (e) {
|
||||
failedImport()
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
mode.value = "import_export"
|
||||
mySelectedCollectionID.value = undefined
|
||||
resetImport()
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const openDialogChooseFileToReplaceWith = () => {
|
||||
if (inputChooseFileToReplaceWith.value)
|
||||
inputChooseFileToReplaceWith.value.click()
|
||||
}
|
||||
const stepResults = ref<StepReturnValue[]>([])
|
||||
|
||||
const openDialogChooseFileToImportFrom = () => {
|
||||
if (inputChooseFileToImportFrom.value)
|
||||
inputChooseFileToImportFrom.value.click()
|
||||
}
|
||||
watch(mySelectedCollectionID, (newValue) => {
|
||||
if (newValue === undefined) return
|
||||
stepResults.value = []
|
||||
stepResults.value.push(newValue)
|
||||
})
|
||||
|
||||
const hasFolder = (item: { item?: any }) => {
|
||||
return Object.prototype.hasOwnProperty.call(item, "item")
|
||||
}
|
||||
const importingMyCollections = ref(false)
|
||||
|
||||
// TODO: I don't even know what is going on here :/
|
||||
type PostmanCollection = {
|
||||
info?: {
|
||||
name: string
|
||||
}
|
||||
name: string
|
||||
item: {
|
||||
name: string
|
||||
request: any
|
||||
item?: any
|
||||
}[]
|
||||
folders?: any
|
||||
}
|
||||
|
||||
const parsePostmanCollection = ({ info, name, item }: PostmanCollection) => {
|
||||
const hoppscotchCollection: Collection<HoppRESTRequest> = makeCollection({
|
||||
name: "",
|
||||
folders: [],
|
||||
requests: [],
|
||||
})
|
||||
|
||||
hoppscotchCollection.name = info ? info.name : name
|
||||
|
||||
if (item && item.length > 0) {
|
||||
for (const collectionItem of item) {
|
||||
if (collectionItem.request) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(hoppscotchCollection, "folders")
|
||||
) {
|
||||
hoppscotchCollection.name = info ? info.name : name
|
||||
hoppscotchCollection.requests.push(
|
||||
parsePostmanRequest(collectionItem)
|
||||
)
|
||||
} else {
|
||||
hoppscotchCollection.name = name || ""
|
||||
hoppscotchCollection.requests.push(
|
||||
parsePostmanRequest(collectionItem)
|
||||
)
|
||||
}
|
||||
} else if (hasFolder(collectionItem)) {
|
||||
hoppscotchCollection.folders.push(
|
||||
parsePostmanCollection(collectionItem as any)
|
||||
)
|
||||
} else {
|
||||
hoppscotchCollection.requests.push(parsePostmanRequest(collectionItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
return hoppscotchCollection
|
||||
}
|
||||
|
||||
// TODO: Rewrite
|
||||
const parsePostmanRequest = ({
|
||||
name,
|
||||
request,
|
||||
}: {
|
||||
name: string
|
||||
request: any
|
||||
}) => {
|
||||
const pwRequest = {
|
||||
url: "",
|
||||
path: "",
|
||||
method: "",
|
||||
auth: "",
|
||||
httpUser: "",
|
||||
httpPassword: "",
|
||||
passwordFieldType: "password",
|
||||
bearerToken: "",
|
||||
headers: [] as { name?: string; type?: string }[],
|
||||
params: [] as { disabled?: boolean }[],
|
||||
bodyParams: [] as { type?: string }[],
|
||||
body: {
|
||||
body: "",
|
||||
contentType: "application/json",
|
||||
},
|
||||
rawParams: "",
|
||||
rawInput: false,
|
||||
contentType: "",
|
||||
requestType: "",
|
||||
name: "",
|
||||
}
|
||||
|
||||
pwRequest.name = name
|
||||
if (request.url) {
|
||||
const requestObjectUrl = request.url.raw.match(
|
||||
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
|
||||
)
|
||||
if (requestObjectUrl) {
|
||||
pwRequest.url = requestObjectUrl[1]
|
||||
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
|
||||
} else {
|
||||
pwRequest.url = request.url.raw
|
||||
}
|
||||
}
|
||||
|
||||
pwRequest.method = request.method
|
||||
const itemAuth = request.auth ? request.auth : ""
|
||||
const authType = itemAuth ? itemAuth.type : ""
|
||||
|
||||
try {
|
||||
if (authType === "basic") {
|
||||
pwRequest.auth = "Basic Auth"
|
||||
pwRequest.httpUser =
|
||||
itemAuth.basic[0].key === "username"
|
||||
? itemAuth.basic[0].value
|
||||
: itemAuth.basic[1].value
|
||||
pwRequest.httpPassword =
|
||||
itemAuth.basic[0].key === "password"
|
||||
? itemAuth.basic[0].value
|
||||
: itemAuth.basic[1].value
|
||||
} else if (authType === "oauth2") {
|
||||
pwRequest.auth = "OAuth 2.0"
|
||||
pwRequest.bearerToken =
|
||||
itemAuth.oauth2[0].key === "accessToken"
|
||||
? itemAuth.oauth2[0].value
|
||||
: itemAuth.oauth2[1].value
|
||||
} else if (authType === "bearer") {
|
||||
pwRequest.auth = "Bearer Token"
|
||||
pwRequest.bearerToken = itemAuth.bearer[0].value
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const requestObjectHeaders = request.header
|
||||
if (requestObjectHeaders) {
|
||||
pwRequest.headers = requestObjectHeaders
|
||||
for (const header of pwRequest.headers) {
|
||||
delete header.name
|
||||
delete header.type
|
||||
}
|
||||
}
|
||||
if (request.url) {
|
||||
const requestObjectParams = request.url.query
|
||||
if (requestObjectParams) {
|
||||
pwRequest.params = requestObjectParams
|
||||
for (const param of pwRequest.params) {
|
||||
delete param.disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request.body) {
|
||||
if (request.body.mode === "urlencoded") {
|
||||
const params = request.body.urlencoded
|
||||
pwRequest.bodyParams = params || []
|
||||
for (const param of pwRequest.bodyParams) {
|
||||
delete param.type
|
||||
}
|
||||
} else if (request.body.mode === "raw") {
|
||||
pwRequest.rawInput = true
|
||||
pwRequest.rawParams = request.body.raw
|
||||
try {
|
||||
const body = JSON.parse(request.body.raw)
|
||||
pwRequest.body.body = JSON.stringify(body, null, 2)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return translateToNewRequest(pwRequest)
|
||||
}
|
||||
|
||||
const replaceWithJSON = () => {
|
||||
if (!inputChooseFileToReplaceWith.value) return
|
||||
|
||||
if (
|
||||
!inputChooseFileToReplaceWith.value.files ||
|
||||
inputChooseFileToReplaceWith.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())
|
||||
return
|
||||
}
|
||||
|
||||
let collections = JSON.parse(content)
|
||||
|
||||
// TODO: File validation
|
||||
if (collections[0]) {
|
||||
const [name, folders, requests] = Object.keys(collections[0])
|
||||
if (name === "name" && folders === "folders" && requests === "requests") {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
|
||||
collections = [parsePostmanCollection(collections)]
|
||||
} else {
|
||||
failedImport()
|
||||
}
|
||||
if (props.collectionsType.type === "team-collections") {
|
||||
teamUtils
|
||||
.replaceWithJSON(
|
||||
apolloClient,
|
||||
collections,
|
||||
props.collectionsType.selectedTeam.id
|
||||
)
|
||||
.then((status) => {
|
||||
if (status) {
|
||||
fileImported()
|
||||
} else {
|
||||
failedImport()
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
failedImport()
|
||||
})
|
||||
} else {
|
||||
setRESTCollections(collections)
|
||||
fileImported()
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(inputChooseFileToReplaceWith.value.files[0])
|
||||
inputChooseFileToReplaceWith.value.value = ""
|
||||
}
|
||||
|
||||
const isInsomniaCollection = (collection: any) => {
|
||||
if (typeof collection === "object") {
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(collection, "__export_source") &&
|
||||
collection.__export_source.includes("insomnia")
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
let content = target!.result as string | null
|
||||
|
||||
if (!content) {
|
||||
toast.show(t("action.choose_file").toString())
|
||||
return
|
||||
}
|
||||
|
||||
let collections = JSON.parse(content)
|
||||
if (isInsomniaCollection(collections)) {
|
||||
collections = parseInsomniaCollection(content)
|
||||
content = JSON.stringify(collections)
|
||||
}
|
||||
if (collections[0]) {
|
||||
const [name, folders, requests] = Object.keys(collections[0])
|
||||
if (name === "name" && folders === "folders" && requests === "requests") {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (collections.info && collections.info.schema.includes("v2.1.0")) {
|
||||
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
|
||||
collections = JSON.parse(content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>"))
|
||||
collections = [parsePostmanCollection(collections)]
|
||||
} else {
|
||||
failedImport()
|
||||
return
|
||||
}
|
||||
if (props.collectionsType.type === "team-collections") {
|
||||
teamUtils
|
||||
.importFromJSON(
|
||||
apolloClient,
|
||||
collections,
|
||||
props.collectionsType.selectedTeam.id
|
||||
)
|
||||
.then((status) => {
|
||||
if (status) {
|
||||
emit("update-team-collections")
|
||||
fileImported()
|
||||
} else {
|
||||
failedImport()
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
failedImport()
|
||||
})
|
||||
} else {
|
||||
appendRESTCollections(collections)
|
||||
fileImported()
|
||||
}
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
}
|
||||
|
||||
const importFromMyCollections = () => {
|
||||
const importToTeams = async (content: Collection<HoppRESTRequest>) => {
|
||||
importingMyCollections.value = true
|
||||
if (props.collectionsType.type !== "team-collections") return
|
||||
|
||||
teamUtils
|
||||
.importFromMyCollections(
|
||||
await teamUtils
|
||||
.importFromJSON(
|
||||
apolloClient,
|
||||
mySelectedCollectionID.value,
|
||||
content,
|
||||
props.collectionsType.selectedTeam.id
|
||||
)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
fileImported()
|
||||
.then((status) => {
|
||||
if (status) {
|
||||
emit("update-team-collections")
|
||||
} else {
|
||||
failedImport()
|
||||
console.error(status)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
failedImport()
|
||||
})
|
||||
.finally(() => {
|
||||
importingMyCollections.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -676,4 +328,98 @@ const exportJSON = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const importerModules = computed(() =>
|
||||
RESTCollectionImporters.filter(
|
||||
(i) => i.applicableTo?.includes(props.collectionsType.type) ?? true
|
||||
)
|
||||
)
|
||||
|
||||
const importerType = ref<number | null>(null)
|
||||
|
||||
const importerModule = computed(() =>
|
||||
importerType.value !== null ? importerModules.value[importerType.value] : null
|
||||
)
|
||||
|
||||
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
||||
|
||||
const finishImport = async () => {
|
||||
await importerAction(stepResults.value)
|
||||
}
|
||||
|
||||
const importerAction = async (stepResults: any[]) => {
|
||||
if (!importerModule.value) return
|
||||
const result = await importerModule.value?.importer(stepResults as any)()
|
||||
if (E.isLeft(result)) {
|
||||
failedImport()
|
||||
console.error("error", result.left)
|
||||
} else if (E.isRight(result)) {
|
||||
if (props.collectionsType.type === "team-collections") {
|
||||
importToTeams(result.right)
|
||||
fileImported()
|
||||
} else {
|
||||
appendRESTCollections(result.right)
|
||||
fileImported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasFile = ref(false)
|
||||
const hasGist = ref(false)
|
||||
|
||||
watch(inputChooseGistToImportFrom, (v) => {
|
||||
stepResults.value = []
|
||||
if (v === "") {
|
||||
hasGist.value = false
|
||||
} else {
|
||||
hasGist.value = true
|
||||
stepResults.value.push(inputChooseGistToImportFrom.value)
|
||||
}
|
||||
})
|
||||
|
||||
const onFileChange = () => {
|
||||
stepResults.value = []
|
||||
if (!inputChooseFileToImportFrom.value[0]) {
|
||||
hasFile.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!inputChooseFileToImportFrom.value[0].files ||
|
||||
inputChooseFileToImportFrom.value[0].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
|
||||
}
|
||||
|
||||
stepResults.value.push(content)
|
||||
hasFile.value = !!content?.length
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value[0].files[0])
|
||||
}
|
||||
|
||||
const enableImportButton = computed(
|
||||
() => !(stepResults.value.length === importerSteps.value?.length)
|
||||
)
|
||||
|
||||
const resetImport = () => {
|
||||
importerType.value = null
|
||||
stepResults.value = []
|
||||
inputChooseFileToImportFrom.value = ""
|
||||
hasFile.value = false
|
||||
inputChooseGistToImportFrom.value = ""
|
||||
hasGist.value = false
|
||||
mySelectedCollectionID.value = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
:title="`${t('modal.import_export')} ${t('modal.collections')}`"
|
||||
:title="`${t('modal.collections')}`"
|
||||
max-width="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
:title="`${t('modal.import_export')} ${t('environment.title')}`"
|
||||
:title="`${t('environment.title')}`"
|
||||
max-width="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
|
||||
26
packages/hoppscotch-app/components/smart/Expand.vue
Normal file
26
packages/hoppscotch-app/components/smart/Expand.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col space-y-2 overflow-hidden"
|
||||
:class="expand ? 'h-full' : 'max-h-32'"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
<div class="flex sticky bottom-0 inset-x-0 items-center justify-center">
|
||||
<ButtonSecondary
|
||||
:icon="expand ? 'expand_less' : 'expand_more'"
|
||||
:label="expand ? t('action.less') : t('action.more')"
|
||||
filled
|
||||
rounded
|
||||
@click.native="expand = !expand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { useI18n } from "~/helpers/utils/composables"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const expand = ref(false)
|
||||
</script>
|
||||
8
packages/hoppscotch-app/globals.d.ts
vendored
Normal file
8
packages/hoppscotch-app/globals.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Some helpful type definitions to help with type checking
|
||||
*/
|
||||
|
||||
interface Object {
|
||||
// Allows for TypeScript to know this field now exist
|
||||
hasOwnProperty<K extends PropertyKey>(key: K): this is Record<K, unknown>
|
||||
}
|
||||
10
packages/hoppscotch-app/helpers/import-export/export/hopp.ts
Normal file
10
packages/hoppscotch-app/helpers/import-export/export/hopp.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppExporter } from "."
|
||||
import { Collection } from "~/newstore/collections"
|
||||
|
||||
const exporter: HoppExporter<Collection<HoppRESTRequest>> = (content) =>
|
||||
pipe(content, JSON.stringify, TE.right)
|
||||
|
||||
export default exporter
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { Collection } from "~/newstore/collections"
|
||||
|
||||
export type HoppExporter<T> = (content: T) => TE.TaskEither<string, string>
|
||||
|
||||
export type HoppExporterDefintion<T> = {
|
||||
name: string
|
||||
exporter: () => Promise<HoppExporter<T>>
|
||||
}
|
||||
|
||||
export const RESTCollectionExporters: HoppExporterDefintion<
|
||||
Collection<HoppRESTRequest>
|
||||
>[] = [
|
||||
{
|
||||
name: "Hoppscotch REST Collection JSON",
|
||||
exporter: () => import("./hopp").then((m) => m.default),
|
||||
},
|
||||
]
|
||||
48
packages/hoppscotch-app/helpers/import-export/import/gist.ts
Normal file
48
packages/hoppscotch-app/helpers/import-export/import/gist.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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 } from "@hoppscotch/data"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { Collection } from "~/newstore/collections"
|
||||
|
||||
// TODO: Add validation to output
|
||||
const fetchGist = (url: string): TO.TaskOption<Collection<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({
|
||||
name: "import.from_gist",
|
||||
icon: "github",
|
||||
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)
|
||||
),
|
||||
})
|
||||
31
packages/hoppscotch-app/helpers/import-export/import/hopp.ts
Normal file
31
packages/hoppscotch-app/helpers/import-export/import/hopp.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { translateToNewRESTCollection } from "~/newstore/collections"
|
||||
|
||||
export default defineImporter({
|
||||
name: "import.from_json",
|
||||
icon: "folder-plus",
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_json_description",
|
||||
acceptedFileTypes: "application/json",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([content]) =>
|
||||
pipe(
|
||||
E.tryCatch(
|
||||
() =>
|
||||
JSON.parse(content).map((coll: any) =>
|
||||
translateToNewRESTCollection(coll)
|
||||
),
|
||||
() => IMPORTER_INVALID_FILE_FORMAT
|
||||
),
|
||||
TE.fromEither
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { StepsOutputList } from "../steps"
|
||||
|
||||
/**
|
||||
* A common error state to be used when the file formats are not expected
|
||||
*/
|
||||
export const IMPORTER_INVALID_FILE_FORMAT =
|
||||
"importer_invalid_file_format" as const
|
||||
|
||||
export type HoppImporterError = typeof IMPORTER_INVALID_FILE_FORMAT
|
||||
|
||||
type HoppImporter<T, StepsType, Errors> = (
|
||||
stepValues: StepsOutputList<StepsType>
|
||||
) => TE.TaskEither<Errors, T>
|
||||
|
||||
/**
|
||||
* Definition for importers
|
||||
*/
|
||||
type HoppImporterDefintion<T, Y, E> = {
|
||||
/**
|
||||
* Name of the importer, shown on the Select Importer dropdown
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Icon for importer button
|
||||
*/
|
||||
icon: string
|
||||
|
||||
/**
|
||||
* Identifier for the importer
|
||||
*/
|
||||
applicableTo?: Array<"team-collections" | "my-collections">
|
||||
|
||||
/**
|
||||
* The importer function, It is a Promise because its supposed to be loaded in lazily (dynamic imports ?)
|
||||
*/
|
||||
importer: HoppImporter<T, Y, E>
|
||||
|
||||
/**
|
||||
* The steps to fetch information required to run an importer
|
||||
*/
|
||||
steps: Y
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a Hoppscotch importer
|
||||
*/
|
||||
export const defineImporter = <ReturnType, StepType, Errors>(input: {
|
||||
name: string
|
||||
icon: string
|
||||
importer: HoppImporter<ReturnType, StepType, Errors>
|
||||
applicableTo?: Array<"team-collections" | "my-collections">
|
||||
steps: StepType
|
||||
}) => {
|
||||
return <HoppImporterDefintion<ReturnType, StepType, Errors>>{
|
||||
...input,
|
||||
}
|
||||
}
|
||||
232
packages/hoppscotch-app/helpers/import-export/import/insomnia.ts
Normal file
232
packages/hoppscotch-app/helpers/import-export/import/insomnia.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { convert, ImportRequest } from "insomnia-importers"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
knownContentTypes,
|
||||
makeRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
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 { Collection, makeCollection } from "~/newstore/collections"
|
||||
|
||||
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
|
||||
|
||||
type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Y>
|
||||
? Y
|
||||
: never
|
||||
|
||||
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
|
||||
type InsomniaResource = ImportRequest
|
||||
|
||||
type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
|
||||
type InsomniaRequestResource = ImportRequest & { _type: "request" }
|
||||
|
||||
const parseInsomniaDoc = (content: string) =>
|
||||
TO.tryCatch(() => convert(content))
|
||||
|
||||
const replaceVarTemplating = flow(
|
||||
S.replace(/{{\s*/g, "<<"),
|
||||
S.replace(/\s*}}/g, ">>")
|
||||
)
|
||||
|
||||
const getFoldersIn = (
|
||||
folder: InsomniaFolderResource | null,
|
||||
resources: InsomniaResource[]
|
||||
) =>
|
||||
pipe(
|
||||
resources,
|
||||
A.filter(
|
||||
(x): x is InsomniaFolderResource =>
|
||||
(x._type === "request_group" || x._type === "workspace") &&
|
||||
x.parentId === (folder?._id ?? null)
|
||||
)
|
||||
)
|
||||
|
||||
const getRequestsIn = (
|
||||
folder: InsomniaFolderResource | null,
|
||||
resources: InsomniaResource[]
|
||||
) =>
|
||||
pipe(
|
||||
resources,
|
||||
A.filter(
|
||||
(x): x is InsomniaRequestResource =>
|
||||
x._type === "request" && x.parentId === (folder?._id ?? null)
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* The provided type by insomnia-importers, this type corrects it
|
||||
*/
|
||||
type InsoReqAuth =
|
||||
| { type: "basic"; disabled?: boolean; username?: string; password?: string }
|
||||
| {
|
||||
type: "oauth2"
|
||||
disabled?: boolean
|
||||
accessTokenUrl?: string
|
||||
authorizationUrl?: string
|
||||
clientId?: string
|
||||
scope?: string
|
||||
}
|
||||
| {
|
||||
type: "bearer"
|
||||
disabled?: boolean
|
||||
token?: string
|
||||
}
|
||||
|
||||
const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
|
||||
if (!req.authentication) return { authType: "none", authActive: true }
|
||||
|
||||
const auth = req.authentication as InsoReqAuth
|
||||
|
||||
if (auth.type === "basic")
|
||||
return {
|
||||
authType: "basic",
|
||||
authActive: true,
|
||||
username: replaceVarTemplating(auth.username ?? ""),
|
||||
password: replaceVarTemplating(auth.password ?? ""),
|
||||
}
|
||||
else if (auth.type === "oauth2")
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: !(auth.disabled ?? false),
|
||||
accessTokenURL: replaceVarTemplating(auth.accessTokenUrl ?? ""),
|
||||
authURL: replaceVarTemplating(auth.authorizationUrl ?? ""),
|
||||
clientID: replaceVarTemplating(auth.clientId ?? ""),
|
||||
oidcDiscoveryURL: "",
|
||||
scope: replaceVarTemplating(auth.scope ?? ""),
|
||||
token: "",
|
||||
}
|
||||
else if (auth.type === "bearer")
|
||||
return {
|
||||
authType: "bearer",
|
||||
authActive: true,
|
||||
token: replaceVarTemplating(auth.token ?? ""),
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const getHoppReqBody = (req: InsomniaRequestResource): HoppRESTReqBody => {
|
||||
if (!req.body) return { contentType: null, body: null }
|
||||
|
||||
if (typeof req.body === "string") {
|
||||
const contentType =
|
||||
req.headers?.find(
|
||||
(header) => header.name.toLowerCase() === "content-type"
|
||||
)?.value ?? "text/plain"
|
||||
|
||||
return { contentType, body: replaceVarTemplating(req.body) }
|
||||
}
|
||||
|
||||
if (req.body.mimeType === "multipart/form-data") {
|
||||
return {
|
||||
contentType: "multipart/form-data",
|
||||
body:
|
||||
req.body.params?.map((param) => ({
|
||||
key: replaceVarTemplating(param.name),
|
||||
value: replaceVarTemplating(param.value ?? ""),
|
||||
active: !(param.disabled ?? false),
|
||||
isFile: false,
|
||||
})) ?? [],
|
||||
}
|
||||
} else if (req.body.mimeType === "application/x-www-form-urlencoded") {
|
||||
return {
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body:
|
||||
req.body.params
|
||||
?.filter((param) => !(param.disabled ?? false))
|
||||
.map(
|
||||
(param) =>
|
||||
`${replaceVarTemplating(param.name)}: ${replaceVarTemplating(
|
||||
param.value ?? ""
|
||||
)}`
|
||||
)
|
||||
.join("\n") ?? "",
|
||||
}
|
||||
} else if (
|
||||
Object.keys(knownContentTypes).includes(req.body.mimeType ?? "text/plain")
|
||||
) {
|
||||
return {
|
||||
contentType: (req.body.mimeType ?? "text/plain") as any,
|
||||
body: replaceVarTemplating(req.body.text ?? "") as any,
|
||||
}
|
||||
}
|
||||
|
||||
return { contentType: null, body: null }
|
||||
}
|
||||
|
||||
const getHoppReqHeaders = (req: InsomniaRequestResource): HoppRESTHeader[] =>
|
||||
req.headers?.map((header) => ({
|
||||
key: replaceVarTemplating(header.name),
|
||||
value: replaceVarTemplating(header.value),
|
||||
active: !header.disabled,
|
||||
})) ?? []
|
||||
|
||||
const getHoppReqParams = (req: InsomniaRequestResource): HoppRESTParam[] =>
|
||||
req.parameters?.map((param) => ({
|
||||
key: replaceVarTemplating(param.name),
|
||||
value: replaceVarTemplating(param.value ?? ""),
|
||||
active: !(param.disabled ?? false),
|
||||
})) ?? []
|
||||
|
||||
const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
|
||||
makeRESTRequest({
|
||||
name: req.name ?? "Untitled Request",
|
||||
method: req.method?.toUpperCase() ?? "GET",
|
||||
endpoint: replaceVarTemplating(req.url ?? ""),
|
||||
auth: getHoppReqAuth(req),
|
||||
body: getHoppReqBody(req),
|
||||
headers: getHoppReqHeaders(req),
|
||||
params: getHoppReqParams(req),
|
||||
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
const getHoppFolder = (
|
||||
folderRes: InsomniaFolderResource,
|
||||
resources: InsomniaResource[]
|
||||
): Collection<HoppRESTRequest> =>
|
||||
makeCollection({
|
||||
name: folderRes.name ?? "",
|
||||
folders: getFoldersIn(folderRes, resources).map((f) =>
|
||||
getHoppFolder(f, resources)
|
||||
),
|
||||
requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
|
||||
})
|
||||
|
||||
const getHoppCollections = (doc: InsomniaDoc) =>
|
||||
getFoldersIn(null, doc.data.resources).map((f) =>
|
||||
getHoppFolder(f, doc.data.resources)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
name: "import.from_insomnia",
|
||||
icon: "insomnia",
|
||||
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)
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
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"
|
||||
|
||||
export default defineImporter({
|
||||
name: "import.from_my_collections",
|
||||
icon: "user",
|
||||
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),
|
||||
})
|
||||
628
packages/hoppscotch-app/helpers/import-export/import/openapi.ts
Normal file
628
packages/hoppscotch-app/helpers/import-export/import/openapi.ts
Normal file
@@ -0,0 +1,628 @@
|
||||
import {
|
||||
OpenAPI,
|
||||
OpenAPIV2,
|
||||
OpenAPIV3,
|
||||
OpenAPIV3_1 as OpenAPIV31,
|
||||
} from "openapi-types"
|
||||
import SwaggerParser from "@apidevtools/swagger-parser"
|
||||
import yaml from "js-yaml"
|
||||
import {
|
||||
FormDataKeyValue,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
knownContentTypes,
|
||||
makeRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
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 { Collection, makeCollection } from "~/newstore/collections"
|
||||
|
||||
const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||
|
||||
// TODO: URL Import Support
|
||||
|
||||
const safeParseJSON = (str: string) => O.tryCatch(() => JSON.parse(str))
|
||||
|
||||
const safeParseYAML = (str: string) => O.tryCatch(() => yaml.load(str))
|
||||
|
||||
const objectHasProperty = <T extends string>(
|
||||
obj: unknown,
|
||||
propName: T
|
||||
// eslint-disable-next-line
|
||||
): obj is { [propName in T]: unknown } =>
|
||||
!!obj &&
|
||||
typeof obj === "object" &&
|
||||
Object.prototype.hasOwnProperty.call(obj, propName)
|
||||
|
||||
type OpenAPIPathInfoType =
|
||||
| OpenAPIV2.PathItemObject<{}>
|
||||
| OpenAPIV3.PathItemObject<{}>
|
||||
| OpenAPIV31.PathItemObject<{}>
|
||||
|
||||
type OpenAPIParamsType =
|
||||
| OpenAPIV2.ParameterObject
|
||||
| OpenAPIV3.ParameterObject
|
||||
| OpenAPIV31.ParameterObject
|
||||
|
||||
type OpenAPIOperationType =
|
||||
| OpenAPIV2.OperationObject
|
||||
| OpenAPIV3.OperationObject
|
||||
| OpenAPIV31.OperationObject
|
||||
|
||||
// Removes the OpenAPI Path Templating to the Hoppscotch Templating (<< ? >>)
|
||||
const replaceOpenApiPathTemplating = flow(
|
||||
S.replace(/{/g, "<<"),
|
||||
S.replace(/}/g, ">>")
|
||||
)
|
||||
|
||||
const parseOpenAPIParams = (params: OpenAPIParamsType[]): HoppRESTParam[] =>
|
||||
pipe(
|
||||
params,
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "query"),
|
||||
O.map(
|
||||
(param) =>
|
||||
<HoppRESTParam>{
|
||||
key: param.name,
|
||||
value: "", // TODO: Can we do anything more ? (parse default values maybe)
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
|
||||
pipe(
|
||||
params,
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "header"),
|
||||
O.map(
|
||||
(header) =>
|
||||
<HoppRESTParam>{
|
||||
key: header.name,
|
||||
value: "", // TODO: Can we do anything more ? (parse default values maybe)
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => {
|
||||
const obj = (op.consumes ?? [])[0] as string | undefined
|
||||
|
||||
// Not a content-type Hoppscotch supports
|
||||
if (!obj || !(obj in knownContentTypes))
|
||||
return { contentType: null, body: null }
|
||||
|
||||
// Textual Content Types, so we just parse it and keep
|
||||
if (
|
||||
obj !== "multipart/form-data" &&
|
||||
obj !== "application/x-www-form-urlencoded"
|
||||
)
|
||||
return { contentType: obj as any, body: "" }
|
||||
|
||||
const formDataValues = pipe(
|
||||
(op.parameters ?? []) as OpenAPIV2.Parameter[],
|
||||
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((param) => param.in === "body"),
|
||||
O.map(
|
||||
(param) =>
|
||||
<FormDataKeyValue>{
|
||||
key: param.name,
|
||||
isFile: false,
|
||||
value: "",
|
||||
active: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return obj === "application/x-www-form-urlencoded"
|
||||
? {
|
||||
contentType: obj,
|
||||
body: formDataValues.map(({ key }) => `${key}: `).join("\n"),
|
||||
}
|
||||
: { contentType: obj, body: formDataValues }
|
||||
}
|
||||
|
||||
const parseOpenAPIV3BodyFormData = (
|
||||
contentType: "multipart/form-data" | "application/x-www-form-urlencoded",
|
||||
mediaObj: OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
|
||||
): HoppRESTReqBody => {
|
||||
const schema = mediaObj.schema as
|
||||
| OpenAPIV3.SchemaObject
|
||||
| OpenAPIV31.SchemaObject
|
||||
| undefined
|
||||
|
||||
if (!schema || schema.type !== "object") {
|
||||
return contentType === "application/x-www-form-urlencoded"
|
||||
? { contentType, body: "" }
|
||||
: { contentType, body: [] }
|
||||
}
|
||||
|
||||
const keys = Object.keys(schema.properties ?? {})
|
||||
|
||||
if (contentType === "application/x-www-form-urlencoded") {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map((key) => `${key}: `).join("\n"),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
contentType,
|
||||
body: keys.map(
|
||||
(key) =>
|
||||
<FormDataKeyValue>{ key, value: "", isFile: false, active: true }
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseOpenAPIV3Body = (
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
|
||||
): HoppRESTReqBody => {
|
||||
const objs = Object.entries(
|
||||
(
|
||||
op.requestBody as
|
||||
| OpenAPIV3.RequestBodyObject
|
||||
| OpenAPIV31.RequestBodyObject
|
||||
| undefined
|
||||
)?.content ?? {}
|
||||
)
|
||||
|
||||
if (objs.length === 0) return { contentType: null, body: null }
|
||||
|
||||
// We only take the first definition
|
||||
const [contentType, media]: [
|
||||
string,
|
||||
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
|
||||
] = objs[0]
|
||||
|
||||
return contentType in knownContentTypes
|
||||
? contentType === "multipart/form-data" ||
|
||||
contentType === "application/x-www-form-urlencoded"
|
||||
? parseOpenAPIV3BodyFormData(contentType, media)
|
||||
: { contentType: contentType as any, body: "" }
|
||||
: { contentType: null, body: null }
|
||||
}
|
||||
|
||||
const isOpenAPIV3Operation = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): op is OpenAPIV3.OperationObject | OpenAPIV31.OperationObject =>
|
||||
objectHasProperty(doc, "openapi") &&
|
||||
typeof doc.openapi === "string" &&
|
||||
doc.openapi.startsWith("3.")
|
||||
|
||||
const parseOpenAPIBody = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): HoppRESTReqBody =>
|
||||
isOpenAPIV3Operation(doc, op)
|
||||
? parseOpenAPIV3Body(op)
|
||||
: parseOpenAPIV2Body(op)
|
||||
|
||||
const resolveOpenAPIV3SecurityObj = (
|
||||
scheme: OpenAPIV3.SecuritySchemeObject | OpenAPIV31.SecuritySchemeObject,
|
||||
_schemeData: string[] // Used for OAuth to pass params
|
||||
): HoppRESTAuth => {
|
||||
if (scheme.type === "http") {
|
||||
if (scheme.scheme === "basic") {
|
||||
// Basic
|
||||
return { authType: "basic", authActive: true, username: "", password: "" }
|
||||
} else if (scheme.scheme === "bearer") {
|
||||
// Bearer
|
||||
return { authType: "bearer", authActive: true, token: "" }
|
||||
} else {
|
||||
// Unknown/Unsupported Scheme
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
} else if (scheme.type === "apiKey") {
|
||||
if (scheme.in === "header") {
|
||||
return {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
addTo: "Headers",
|
||||
key: scheme.name,
|
||||
value: "",
|
||||
}
|
||||
} else if (scheme.in === "query") {
|
||||
return {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
addTo: "Query params",
|
||||
key: scheme.in,
|
||||
value: "",
|
||||
}
|
||||
}
|
||||
} else if (scheme.type === "oauth2") {
|
||||
// NOTE: We select flow on a first come basis on this order, authorizationCode > implicit > password > clientCredentials
|
||||
if (scheme.flows.authorizationCode) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.authorizationCode.tokenUrl ?? "",
|
||||
authURL: scheme.flows.authorizationCode.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.implicit) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: scheme.flows.implicit.authorizationUrl ?? "",
|
||||
accessTokenURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.password) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: "",
|
||||
accessTokenURL: scheme.flows.password.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flows.clientCredentials) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.clientCredentials.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
} else if (scheme.type === "openIdConnect") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: scheme.openIdConnectUrl ?? "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3SecurityScheme = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
schemeName: string,
|
||||
schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
const scheme = doc.components?.securitySchemes?.[schemeName] as
|
||||
| OpenAPIV3.SecuritySchemeObject
|
||||
| undefined
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
else return resolveOpenAPIV3SecurityObj(scheme, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV3Security = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
security:
|
||||
| OpenAPIV3.SecurityRequirementObject[]
|
||||
| OpenAPIV31.SecurityRequirementObject[]
|
||||
): HoppRESTAuth => {
|
||||
// NOTE: Hoppscotch only considers the first security requirement
|
||||
const sec = security[0] as OpenAPIV3.SecurityRequirementObject | undefined
|
||||
|
||||
if (!sec) return { authType: "none", authActive: true }
|
||||
|
||||
// NOTE: We only consider the first security condition within the first condition
|
||||
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
|
||||
undefined,
|
||||
undefined,
|
||||
]) as [string | undefined, string[] | undefined]
|
||||
|
||||
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
|
||||
|
||||
return resolveOpenAPIV3SecurityScheme(doc, schemeName, schemeData)
|
||||
}
|
||||
|
||||
const parseOpenAPIV3Auth = (
|
||||
doc: OpenAPIV3.Document | OpenAPIV31.Document,
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
|
||||
): HoppRESTAuth => {
|
||||
const rootAuth = doc.security
|
||||
? resolveOpenAPIV3Security(doc, doc.security)
|
||||
: undefined
|
||||
const opAuth = op.security
|
||||
? resolveOpenAPIV3Security(doc, op.security)
|
||||
: undefined
|
||||
|
||||
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2SecurityScheme = (
|
||||
scheme: OpenAPIV2.SecuritySchemeObject,
|
||||
_schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
if (scheme.type === "basic") {
|
||||
return { authType: "basic", authActive: true, username: "", password: "" }
|
||||
} else if (scheme.type === "apiKey") {
|
||||
// V2 only supports in: header and in: query
|
||||
return {
|
||||
authType: "api-key",
|
||||
addTo: scheme.in === "header" ? "Headers" : "Query params",
|
||||
authActive: true,
|
||||
key: scheme.name,
|
||||
value: "",
|
||||
}
|
||||
} else if (scheme.type === "oauth2") {
|
||||
// NOTE: We select flow on a first come basis on this order, accessCode > implicit > password > application
|
||||
if (scheme.flow === "accessCode") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "implicit") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "application") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else if (scheme.flow === "password") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2SecurityDef = (
|
||||
doc: OpenAPIV2.Document,
|
||||
schemeName: string,
|
||||
schemeData: string[]
|
||||
): HoppRESTAuth => {
|
||||
const scheme = Object.entries(doc.securityDefinitions ?? {}).find(
|
||||
([name]) => schemeName === name
|
||||
)
|
||||
|
||||
if (!scheme) return { authType: "none", authActive: true }
|
||||
|
||||
const schemeObj = scheme[1]
|
||||
|
||||
return resolveOpenAPIV2SecurityScheme(schemeObj, schemeData)
|
||||
}
|
||||
|
||||
const resolveOpenAPIV2Security = (
|
||||
doc: OpenAPIV2.Document,
|
||||
security: OpenAPIV2.SecurityRequirementObject[]
|
||||
): HoppRESTAuth => {
|
||||
// NOTE: Hoppscotch only considers the first security requirement
|
||||
const sec = security[0] as OpenAPIV2.SecurityRequirementObject | undefined
|
||||
|
||||
if (!sec) return { authType: "none", authActive: true }
|
||||
|
||||
// NOTE: We only consider the first security condition within the first condition
|
||||
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
|
||||
undefined,
|
||||
undefined,
|
||||
]) as [string | undefined, string[] | undefined]
|
||||
|
||||
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
|
||||
|
||||
return resolveOpenAPIV2SecurityDef(doc, schemeName, schemeData)
|
||||
}
|
||||
|
||||
const parseOpenAPIV2Auth = (
|
||||
doc: OpenAPIV2.Document,
|
||||
op: OpenAPIV2.OperationObject
|
||||
): HoppRESTAuth => {
|
||||
const rootAuth = doc.security
|
||||
? resolveOpenAPIV2Security(doc, doc.security)
|
||||
: undefined
|
||||
const opAuth = op.security
|
||||
? resolveOpenAPIV2Security(doc, op.security)
|
||||
: undefined
|
||||
|
||||
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
const parseOpenAPIAuth = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): HoppRESTAuth =>
|
||||
isOpenAPIV3Operation(doc, op)
|
||||
? parseOpenAPIV3Auth(doc as OpenAPIV3.Document | OpenAPIV31.Document, op)
|
||||
: parseOpenAPIV2Auth(doc as OpenAPIV2.Document, op)
|
||||
|
||||
const convertPathToHoppReqs = (
|
||||
doc: OpenAPI.Document,
|
||||
pathName: string,
|
||||
pathObj: OpenAPIPathInfoType
|
||||
) =>
|
||||
pipe(
|
||||
["get", "head", "post", "put", "delete", "options"] as const,
|
||||
|
||||
// Filter and map out path info
|
||||
RA.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((method) => !!pathObj[method]),
|
||||
O.map((method) => ({ method, info: pathObj[method]! }))
|
||||
)
|
||||
),
|
||||
|
||||
// Construct request object
|
||||
RA.map(({ method, info }) =>
|
||||
makeRESTRequest({
|
||||
name: info.operationId ?? info.summary ?? "Untitled Request",
|
||||
method: method.toUpperCase(),
|
||||
endpoint: `<<baseUrl>>${replaceOpenApiPathTemplating(pathName)}`, // TODO: Make this proper
|
||||
|
||||
// We don't need to worry about reference types as the Dereferencing pass should remove them
|
||||
params: parseOpenAPIParams(
|
||||
(info.parameters as OpenAPIParamsType[]) ?? []
|
||||
),
|
||||
headers: parseOpenAPIHeaders(
|
||||
(info.parameters as OpenAPIParamsType[]) ?? []
|
||||
),
|
||||
|
||||
auth: parseOpenAPIAuth(doc, info),
|
||||
|
||||
body: parseOpenAPIBody(doc, info),
|
||||
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
),
|
||||
|
||||
// Disable Readonly
|
||||
RA.toArray
|
||||
)
|
||||
|
||||
const convertOpenApiDocToHopp = (
|
||||
doc: OpenAPI.Document
|
||||
): TE.TaskEither<never, Collection<HoppRESTRequest>[]> => {
|
||||
const name = doc.info.title
|
||||
|
||||
const paths = Object.entries(doc.paths ?? {})
|
||||
.map(([pathName, pathObj]) => convertPathToHoppReqs(doc, pathName, pathObj))
|
||||
.flat()
|
||||
|
||||
return TE.of([
|
||||
makeCollection<HoppRESTRequest>({
|
||||
name,
|
||||
folders: [],
|
||||
requests: paths,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const parseOpenAPIDocContent = (str: string) =>
|
||||
pipe(
|
||||
str,
|
||||
safeParseJSON,
|
||||
O.match(
|
||||
() => safeParseYAML(str),
|
||||
(data) => O.of(data)
|
||||
)
|
||||
)
|
||||
|
||||
export default defineImporter({
|
||||
name: "import.from_openapi",
|
||||
icon: "file",
|
||||
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
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Deference the references
|
||||
TE.chainW((obj) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() => SwaggerParser.dereference(obj),
|
||||
() => OPENAPI_DEREF_ERROR
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
TE.chainW(convertOpenApiDocToHopp)
|
||||
),
|
||||
})
|
||||
264
packages/hoppscotch-app/helpers/import-export/import/postman.ts
Normal file
264
packages/hoppscotch-app/helpers/import-export/import/postman.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
Collection as PMCollection,
|
||||
FormParam,
|
||||
Item,
|
||||
ItemGroup,
|
||||
QueryParam,
|
||||
RequestAuthDefinition,
|
||||
VariableDefinition,
|
||||
} from "postman-collection"
|
||||
import {
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
makeRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
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 { Collection, makeCollection } from "~/newstore/collections"
|
||||
|
||||
const safeParseJSON = (jsonStr: string) => O.tryCatch(() => JSON.parse(jsonStr))
|
||||
|
||||
const isPMItem = (x: unknown): x is Item => Item.isItem(x)
|
||||
|
||||
const replacePMVarTemplating = flow(
|
||||
S.replace(/{{\s*/g, "<<"),
|
||||
S.replace(/\s*}}/g, ">>")
|
||||
)
|
||||
|
||||
const isPMItemGroup = (x: unknown): x is ItemGroup<Item> =>
|
||||
ItemGroup.isItemGroup(x)
|
||||
|
||||
const readPMCollection = (def: string) =>
|
||||
pipe(
|
||||
def,
|
||||
safeParseJSON,
|
||||
O.chain((data) => O.tryCatch(() => new PMCollection(data)))
|
||||
)
|
||||
|
||||
const getHoppReqHeaders = (item: Item): HoppRESTHeader[] =>
|
||||
pipe(
|
||||
item.request.headers.all(),
|
||||
A.map((header) => {
|
||||
return <HoppRESTHeader>{
|
||||
key: replacePMVarTemplating(header.key),
|
||||
value: replacePMVarTemplating(header.value),
|
||||
active: !header.disabled,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const getHoppReqParams = (item: Item): HoppRESTParam[] => {
|
||||
return pipe(
|
||||
item.request.url.query.all(),
|
||||
A.filter(
|
||||
(param): param is QueryParam & { key: string } =>
|
||||
param.key !== undefined && param.key !== null && param.key.length > 0
|
||||
),
|
||||
A.map((param) => {
|
||||
return <HoppRESTHeader>{
|
||||
key: replacePMVarTemplating(param.key),
|
||||
value: replacePMVarTemplating(param.value ?? ""),
|
||||
active: !param.disabled,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
type PMRequestAuthDef<
|
||||
AuthType extends RequestAuthDefinition["type"] = RequestAuthDefinition["type"]
|
||||
> = AuthType extends RequestAuthDefinition["type"] & string
|
||||
? // eslint-disable-next-line no-unused-vars
|
||||
{ type: AuthType } & { [x in AuthType]: VariableDefinition[] }
|
||||
: { type: AuthType }
|
||||
|
||||
const getVariableValue = (defs: VariableDefinition[], key: string) =>
|
||||
defs.find((param) => param.key === key)?.value as string | undefined
|
||||
|
||||
const getHoppReqAuth = (item: Item): HoppRESTAuth => {
|
||||
if (!item.request.auth) return { authType: "none", authActive: true }
|
||||
|
||||
// Cast to the type for more stricter checking down the line
|
||||
const auth = item.request.auth as unknown as PMRequestAuthDef
|
||||
|
||||
if (auth.type === "basic") {
|
||||
return {
|
||||
authType: "basic",
|
||||
authActive: true,
|
||||
username: replacePMVarTemplating(
|
||||
getVariableValue(auth.basic, "username") ?? ""
|
||||
),
|
||||
password: replacePMVarTemplating(
|
||||
getVariableValue(auth.basic, "password") ?? ""
|
||||
),
|
||||
}
|
||||
} else if (auth.type === "apikey") {
|
||||
return {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
key: replacePMVarTemplating(getVariableValue(auth.apikey, "key") ?? ""),
|
||||
value: replacePMVarTemplating(
|
||||
getVariableValue(auth.apikey, "value") ?? ""
|
||||
),
|
||||
addTo:
|
||||
(getVariableValue(auth.apikey, "in") ?? "query") === "query"
|
||||
? "Query params"
|
||||
: "Headers",
|
||||
}
|
||||
} else if (auth.type === "bearer") {
|
||||
return {
|
||||
authType: "bearer",
|
||||
authActive: true,
|
||||
token: replacePMVarTemplating(
|
||||
getVariableValue(auth.bearer, "token") ?? ""
|
||||
),
|
||||
}
|
||||
} else if (auth.type === "oauth2") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
|
||||
),
|
||||
authURL: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "authUrl") ?? ""
|
||||
),
|
||||
clientID: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "clientId") ?? ""
|
||||
),
|
||||
scope: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "scope") ?? ""
|
||||
),
|
||||
token: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "accessToken") ?? ""
|
||||
),
|
||||
oidcDiscoveryURL: "",
|
||||
}
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
|
||||
type PMFormDataParamType = FormParam & {
|
||||
type: "file" | "text"
|
||||
}
|
||||
|
||||
const getHoppReqBody = (item: Item): HoppRESTReqBody => {
|
||||
if (!item.request.body) return { contentType: null, body: null }
|
||||
|
||||
// TODO: Implement
|
||||
const body = item.request.body
|
||||
|
||||
if (body.mode === "formdata") {
|
||||
return {
|
||||
contentType: "multipart/form-data",
|
||||
body:
|
||||
(body.formdata?.all() as PMFormDataParamType[]).map((param) => ({
|
||||
key: replacePMVarTemplating(param.key),
|
||||
value: replacePMVarTemplating(
|
||||
param.type === "text" ? (param.value as string) : ""
|
||||
),
|
||||
active: !param.disabled,
|
||||
isFile: false, // TODO: Preserve isFile state ?
|
||||
})) ?? [],
|
||||
}
|
||||
} else if (body.mode === "urlencoded") {
|
||||
return {
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body:
|
||||
body.urlencoded
|
||||
?.all()
|
||||
.map(
|
||||
(param) =>
|
||||
`${replacePMVarTemplating(
|
||||
param.key ?? ""
|
||||
)}: ${replacePMVarTemplating(param.value ?? "")}`
|
||||
)
|
||||
.join("\n") ?? "",
|
||||
}
|
||||
} else if (body.mode === "raw") {
|
||||
// Find content type from the content type header
|
||||
const contentType = getHoppReqHeaders(item).find(
|
||||
({ key }) => key.toLowerCase() === "content-type"
|
||||
)?.value
|
||||
|
||||
if (contentType && body.raw !== undefined && body.raw !== null)
|
||||
return {
|
||||
contentType: contentType as any,
|
||||
body: replacePMVarTemplating(body.raw),
|
||||
}
|
||||
else return { contentType: null, body: null } // TODO: Any sort of recovery ?
|
||||
}
|
||||
|
||||
// TODO: File
|
||||
// TODO: GraphQL ?
|
||||
|
||||
return { contentType: null, body: null }
|
||||
}
|
||||
|
||||
const getHoppReqURL = (item: Item): string =>
|
||||
pipe(
|
||||
item.request.url.toString(true),
|
||||
S.replace(/\?.+/g, ""),
|
||||
replacePMVarTemplating
|
||||
)
|
||||
|
||||
const getHoppRequest = (item: Item): HoppRESTRequest => {
|
||||
return makeRESTRequest({
|
||||
name: item.name,
|
||||
endpoint: getHoppReqURL(item),
|
||||
method: item.request.method.toUpperCase(),
|
||||
headers: getHoppReqHeaders(item),
|
||||
params: getHoppReqParams(item),
|
||||
auth: getHoppReqAuth(item),
|
||||
body: getHoppReqBody(item),
|
||||
|
||||
// TODO: Decide about this
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
}
|
||||
|
||||
const getHoppFolder = (ig: ItemGroup<Item>): Collection<HoppRESTRequest> =>
|
||||
makeCollection({
|
||||
name: ig.name,
|
||||
folders: pipe(
|
||||
ig.items.all(),
|
||||
A.filter(isPMItemGroup),
|
||||
A.map(getHoppFolder)
|
||||
),
|
||||
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
|
||||
})
|
||||
|
||||
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
|
||||
|
||||
export default defineImporter({
|
||||
name: "import.from_postman",
|
||||
icon: "postman",
|
||||
steps: [
|
||||
step({
|
||||
stepName: "FILE_IMPORT",
|
||||
metadata: {
|
||||
caption: "import.from_postman_description",
|
||||
acceptedFileTypes: ".json",
|
||||
},
|
||||
}),
|
||||
] as const,
|
||||
importer: ([fileContent]) =>
|
||||
pipe(
|
||||
// Try reading
|
||||
fileContent,
|
||||
readPMCollection,
|
||||
|
||||
O.map(flow(getHoppCollection, A.of)),
|
||||
|
||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||
),
|
||||
})
|
||||
82
packages/hoppscotch-app/helpers/import-export/steps.ts
Normal file
82
packages/hoppscotch-app/helpers/import-export/steps.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
function transformUrl(insomniaUrl: string) {
|
||||
if (insomniaUrl === "") return {}
|
||||
const postmanUrl: any = {}
|
||||
postmanUrl.raw = insomniaUrl
|
||||
const urlParts = insomniaUrl.split(/:\/\//)
|
||||
let rawHostAndPath
|
||||
if (urlParts.length === 1) {
|
||||
rawHostAndPath = urlParts[0]
|
||||
} else if (urlParts.length === 2) {
|
||||
postmanUrl.protocol = urlParts[0]
|
||||
rawHostAndPath = urlParts[1]
|
||||
} else {
|
||||
console.error(
|
||||
"Error: Unexpected number of components found in the URL string. Exiting."
|
||||
)
|
||||
process.exit(3)
|
||||
}
|
||||
// https://stackoverflow.com/questions/4607745/split-string-only-on-first-instance-of-specified-character
|
||||
const hostAndPath = rawHostAndPath.split(/\/(.+)/)
|
||||
postmanUrl.host = hostAndPath[0].split(/\./)
|
||||
postmanUrl.path =
|
||||
hostAndPath[1] === undefined ? [] : hostAndPath[1].split(/\//)
|
||||
return postmanUrl
|
||||
}
|
||||
|
||||
function transformHeaders(insomniaHeaders: any) {
|
||||
const outputHeaders: any[] = []
|
||||
insomniaHeaders.forEach((element: any) => {
|
||||
const header: any = {}
|
||||
header.key = element.name
|
||||
header.value = element.value
|
||||
outputHeaders.push(header)
|
||||
})
|
||||
return outputHeaders
|
||||
}
|
||||
|
||||
function transformBody(insomniaBody: any) {
|
||||
const body: any = {}
|
||||
switch (insomniaBody.mimeType) {
|
||||
case "":
|
||||
case "application/json":
|
||||
case "application/xml":
|
||||
body.mode = "raw"
|
||||
body.raw = insomniaBody.text
|
||||
break
|
||||
case "multipart/form-data":
|
||||
body.mode = "formdata"
|
||||
body.formdata = []
|
||||
insomniaBody.params.forEach((param: any) => {
|
||||
body.formdata.push({ key: param.name, value: param.value })
|
||||
})
|
||||
break
|
||||
case "application/x-www-form-urlencoded":
|
||||
body.mode = "urlencoded"
|
||||
body.urlencoded = []
|
||||
insomniaBody.params.forEach((param: any) => {
|
||||
body.urlencoded.push({ key: param.name, value: param.value })
|
||||
})
|
||||
break
|
||||
case "application/octet-stream":
|
||||
body.mode = "file"
|
||||
body.file = {}
|
||||
body.file.src = "/C:/PleaseSelectAFile"
|
||||
console.warn(
|
||||
"Warning: A file is supposed to be a part of the request!!! Would need to be manually selected in Postman."
|
||||
)
|
||||
break
|
||||
default:
|
||||
console.warn(
|
||||
"Warning: Body type unsupported; skipped!!! ... " +
|
||||
insomniaBody.mimeType
|
||||
)
|
||||
body.mode = "raw"
|
||||
body.raw =
|
||||
"github.com/Vyoam/InsomniaToPostmanFormat: Unsupported body type " +
|
||||
insomniaBody.mimeType
|
||||
break
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
function transformItem(insomniaItem: any) {
|
||||
const postmanItem: any = {}
|
||||
postmanItem.name = insomniaItem.name
|
||||
const request: any = {}
|
||||
request.method = insomniaItem.method
|
||||
request.header = transformHeaders(insomniaItem.headers)
|
||||
if (Object.keys(insomniaItem.body).length !== 0) {
|
||||
request.body = transformBody(insomniaItem.body)
|
||||
}
|
||||
request.url = transformUrl(insomniaItem.url)
|
||||
if (insomniaItem.parameters && insomniaItem.parameters.length > 0) {
|
||||
if (request.url.raw !== undefined && request.url.raw.includes("?")) {
|
||||
console.warn(
|
||||
"Warning: Query params detected in both the raw query and the 'parameters' object of Insomnia request!!! Exported Postman collection may need manual editing for erroneous '?' in url."
|
||||
)
|
||||
}
|
||||
request.url.query = []
|
||||
insomniaItem.parameters.forEach((param: any) => {
|
||||
request.url.query.push({ key: param.name, value: param.value })
|
||||
})
|
||||
}
|
||||
request.auth = {} // todo
|
||||
if (Object.keys(insomniaItem.authentication).length !== 0) {
|
||||
console.warn("Warning: Auth param export not yet supported!!!")
|
||||
}
|
||||
postmanItem.request = request
|
||||
postmanItem.response = []
|
||||
return postmanItem
|
||||
}
|
||||
|
||||
const rootId = "d1097c3b-2011-47a4-8f95-87b8f4b54d6d" // unique guid for root
|
||||
|
||||
function generateMaps(insomniaParentChildList: any) {
|
||||
const parentChildrenMap = new Map()
|
||||
const flatMap = new Map()
|
||||
insomniaParentChildList.forEach((element: any) => {
|
||||
flatMap.set(element._id, element)
|
||||
let elementArray = []
|
||||
switch (element._type) {
|
||||
case "workspace":
|
||||
// 'bug': only one workspace to be selected (the last one which comes up here)
|
||||
elementArray.push(element)
|
||||
parentChildrenMap.set(rootId, elementArray) // in any case will select the top workspace when creating tree
|
||||
break
|
||||
case "request":
|
||||
elementArray = parentChildrenMap.get(element.parentId)
|
||||
if (elementArray === undefined) elementArray = []
|
||||
elementArray.push(element)
|
||||
parentChildrenMap.set(element.parentId, elementArray)
|
||||
break
|
||||
case "request_group":
|
||||
elementArray = parentChildrenMap.get(element.parentId)
|
||||
if (elementArray === undefined) elementArray = []
|
||||
elementArray.push(element)
|
||||
parentChildrenMap.set(element.parentId, elementArray)
|
||||
break
|
||||
default:
|
||||
console.warn(
|
||||
"Warning: Item type unsupported; skipped!!! ... " + element._type
|
||||
)
|
||||
}
|
||||
})
|
||||
const maps = [parentChildrenMap, flatMap]
|
||||
return maps
|
||||
}
|
||||
|
||||
function getCollectionName(insomniaParentChildList: any) {
|
||||
let collectionName = "Untitled"
|
||||
|
||||
insomniaParentChildList.forEach((element: any) => {
|
||||
if (element._type === "workspace") {
|
||||
collectionName = element.name
|
||||
}
|
||||
})
|
||||
|
||||
return collectionName
|
||||
}
|
||||
|
||||
function generateTreeRecursively(element: any, parentChildrenMap: any) {
|
||||
let postmanItem: any = {}
|
||||
switch (element._type) {
|
||||
case "request_group":
|
||||
postmanItem.name = element.name
|
||||
postmanItem.item = []
|
||||
parentChildrenMap.get(element._id).forEach((child: any) => {
|
||||
postmanItem.item.push(generateTreeRecursively(child, parentChildrenMap))
|
||||
})
|
||||
break
|
||||
case "request":
|
||||
postmanItem = transformItem(element)
|
||||
break
|
||||
default:
|
||||
console.warn(
|
||||
"Warning: Item type unsupported; skipped!!! ... " + element._type
|
||||
)
|
||||
}
|
||||
return postmanItem
|
||||
}
|
||||
|
||||
function getSubItemTrees(parentChildrenMap: any) {
|
||||
const subItemTrees: any[] = []
|
||||
const roots = parentChildrenMap.get(rootId)
|
||||
parentChildrenMap.get(roots[0]._id).forEach((element: any) => {
|
||||
subItemTrees.push(generateTreeRecursively(element, parentChildrenMap))
|
||||
})
|
||||
return subItemTrees
|
||||
}
|
||||
|
||||
export function parseInsomniaCollection(inputDataString: string) {
|
||||
const inputData = JSON.parse(inputDataString)
|
||||
|
||||
console.log("Parsing Insomnia collection...", inputData)
|
||||
|
||||
if (inputData.__export_format !== 4) {
|
||||
console.error(
|
||||
"Error: Version (__export_format " +
|
||||
inputData.__export_format +
|
||||
") not supported. Only version 4 is supported."
|
||||
)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const outputData: any = {
|
||||
info: {
|
||||
_postman_id: "",
|
||||
name: "",
|
||||
schema:
|
||||
"https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
},
|
||||
item: [],
|
||||
}
|
||||
|
||||
outputData.info._postman_id = uuidv4()
|
||||
outputData.info.name = getCollectionName(inputData.resources)
|
||||
|
||||
const maps = generateMaps(inputData.resources)
|
||||
|
||||
const parentChildrenMap = maps[0]
|
||||
// const flatMap = maps[1]
|
||||
|
||||
const subItems: any = getSubItemTrees(parentChildrenMap)
|
||||
outputData.item.push(...subItems)
|
||||
|
||||
return outputData
|
||||
}
|
||||
|
||||
function uuidv4(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiket",
|
||||
"learn_more": "Leer meer",
|
||||
"more": "Meer",
|
||||
"less": "Less",
|
||||
"new": "Nuut",
|
||||
"no": "Geen",
|
||||
"preserve_current": "Bewaar stroom",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Verwyder",
|
||||
"replace_current": "Vervang stroom",
|
||||
"replace_json": "Vervang deur JSON",
|
||||
"restore": "Herstel",
|
||||
"save": "Stoor",
|
||||
"search": "Soek",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Skakel af",
|
||||
"turn_on": "Sit aan",
|
||||
"undo": "Ontdoen",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Bewaar stroom",
|
||||
"replace_current": "Vervang stroom",
|
||||
"replace_json": "Vervang deur JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Voeg nuwe",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Voer versamelings in",
|
||||
"curl": "Voer cURL in",
|
||||
"failed": "Invoer misluk",
|
||||
"from_gist": "Invoer vanaf Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Invoer uit My versamelings",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Voer Gist URL in",
|
||||
"json": "Invoer vanaf JSON",
|
||||
"title": "Invoer"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Invoer",
|
||||
"from_gist": "Invoer vanaf Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "ملصق",
|
||||
"learn_more": "يتعلم أكثر",
|
||||
"more": "أكثر",
|
||||
"less": "Less",
|
||||
"new": "جديد",
|
||||
"no": "رقم",
|
||||
"preserve_current": "الحفاظ على التيار",
|
||||
"prettify": "جمال",
|
||||
"remove": "يزيل",
|
||||
"replace_current": "استبدال التيار",
|
||||
"replace_json": "استبدل بـ JSON",
|
||||
"restore": "يعيد",
|
||||
"save": "يحفظ",
|
||||
"search": "بحث",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "اطفئه",
|
||||
"turn_on": "شغله",
|
||||
"undo": "الغاء التحميل",
|
||||
"yes": "نعم"
|
||||
"yes": "نعم",
|
||||
"preserve_current": "الحفاظ على التيار",
|
||||
"replace_current": "استبدال التيار",
|
||||
"replace_json": "استبدل بـ JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "اضف جديد",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "مجموعات الاستيراد",
|
||||
"curl": "استيراد cURL",
|
||||
"failed": "فشل الاستيراد",
|
||||
"from_gist": "الاستيراد من Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "الاستيراد من \"مجموعاتي\"",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "أدخل عنوان URL لـ Gist",
|
||||
"json": "استيراد من JSON",
|
||||
"title": "يستورد"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "يستورد",
|
||||
"from_gist": "الاستيراد من Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Aprèn més",
|
||||
"more": "Més",
|
||||
"less": "Less",
|
||||
"new": "Novetat",
|
||||
"no": "No",
|
||||
"preserve_current": "Conservar el corrent",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Elimina",
|
||||
"replace_current": "Substitueix el corrent",
|
||||
"replace_json": "Substitueix per JSON",
|
||||
"restore": "Restaura",
|
||||
"save": "Desa",
|
||||
"search": "Cerca",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Tanca",
|
||||
"turn_on": "Encendre",
|
||||
"undo": "Desfés",
|
||||
"yes": "Sí"
|
||||
"yes": "Sí",
|
||||
"preserve_current": "Conservar el corrent",
|
||||
"replace_current": "Substitueix el corrent",
|
||||
"replace_json": "Substitueix per JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Afegir nou",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importar col·leccions",
|
||||
"curl": "Importa cURL",
|
||||
"failed": "La importació ha fallat",
|
||||
"from_gist": "Importa de Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importa de Les meves col·leccions",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Introduïu l'URL Gist",
|
||||
"json": "Importa de JSON",
|
||||
"title": "Importació"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importació",
|
||||
"from_gist": "Importa de Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "标签",
|
||||
"learn_more": "了解更多",
|
||||
"more": "更多",
|
||||
"less": "Less",
|
||||
"new": "新增",
|
||||
"no": "否",
|
||||
"preserve_current": "保持当前",
|
||||
"prettify": "美化",
|
||||
"remove": "移除",
|
||||
"replace_current": "替换当前",
|
||||
"replace_json": "替换为 JSON",
|
||||
"restore": "恢复",
|
||||
"save": "保存",
|
||||
"search": "搜索",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "关闭",
|
||||
"turn_on": "开启",
|
||||
"undo": "撤消",
|
||||
"yes": "是"
|
||||
"yes": "是",
|
||||
"preserve_current": "保持当前",
|
||||
"replace_current": "替换当前",
|
||||
"replace_json": "替换为 JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "新增",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "导入组合",
|
||||
"curl": "导入 cURL",
|
||||
"failed": "导入失败",
|
||||
"from_gist": "从 Gist 导入",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "从我的组合导入",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "输入 Gist URL",
|
||||
"json": "从 JSON 导入",
|
||||
"title": "导入"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "导入",
|
||||
"from_gist": "从 Gist 导入"
|
||||
},
|
||||
"layout": {
|
||||
"column": "垂直布局",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Označení",
|
||||
"learn_more": "Další informace",
|
||||
"more": "Více",
|
||||
"less": "Less",
|
||||
"new": "Nový",
|
||||
"no": "Ne",
|
||||
"preserve_current": "Zachovat proud",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Odstranit",
|
||||
"replace_current": "Vyměňte proud",
|
||||
"replace_json": "Nahradit za JSON",
|
||||
"restore": "Obnovit",
|
||||
"save": "Uložit",
|
||||
"search": "Vyhledávání",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Vypnout",
|
||||
"turn_on": "Zapnout",
|
||||
"undo": "vrátit",
|
||||
"yes": "Ano"
|
||||
"yes": "Ano",
|
||||
"preserve_current": "Zachovat proud",
|
||||
"replace_current": "Vyměňte proud",
|
||||
"replace_json": "Nahradit za JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Přidat nový",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Import sbírek",
|
||||
"curl": "Importovat cURL",
|
||||
"failed": "Import se nezdařil",
|
||||
"from_gist": "Import z Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importovat z mých sbírek",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Zadejte URL adresy",
|
||||
"json": "Import z JSON",
|
||||
"title": "Import"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import",
|
||||
"from_gist": "Import z Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiket",
|
||||
"learn_more": "Lær mere",
|
||||
"more": "Mere",
|
||||
"less": "Less",
|
||||
"new": "Ny",
|
||||
"no": "Ingen",
|
||||
"preserve_current": "Bevar strøm",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Fjerne",
|
||||
"replace_current": "Udskift strøm",
|
||||
"replace_json": "Udskift med JSON",
|
||||
"restore": "Gendan",
|
||||
"save": "Gemme",
|
||||
"search": "Søg",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Sluk",
|
||||
"turn_on": "Tænde for",
|
||||
"undo": "Fortryd",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Bevar strøm",
|
||||
"replace_current": "Udskift strøm",
|
||||
"replace_json": "Udskift med JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Tilføj ny",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importer samlinger",
|
||||
"curl": "Importer cURL",
|
||||
"failed": "Import mislykkedes",
|
||||
"from_gist": "Import fra Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Import fra Mine samlinger",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Indtast Gist URL",
|
||||
"json": "Import fra JSON",
|
||||
"title": "Importere"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importere",
|
||||
"from_gist": "Import fra Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etikett",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"more": "Mehr",
|
||||
"less": "Less",
|
||||
"new": "Neu",
|
||||
"no": "Nein",
|
||||
"preserve_current": "Aktuelles behalten",
|
||||
"prettify": "Verschönern",
|
||||
"remove": "Entfernen",
|
||||
"replace_current": "Aktuelles ersetzen",
|
||||
"replace_json": "Durch JSON ersetzen",
|
||||
"restore": "Wiederherstellen",
|
||||
"save": "Speichern",
|
||||
"search": "Suchen",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Ausschalten",
|
||||
"turn_on": "Einschalten",
|
||||
"undo": "Rückgängig machen",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Aktuelles behalten",
|
||||
"replace_current": "Aktuelles ersetzen",
|
||||
"replace_json": "Durch JSON ersetzen"
|
||||
},
|
||||
"add": {
|
||||
"new": "Neue hinzufügen",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Sammlungen importieren",
|
||||
"curl": "cURL importieren",
|
||||
"failed": "Importieren fehlgeschlagen",
|
||||
"from_gist": "Import aus Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Aus Meine Sammlungen importieren",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Gist-URL eingeben",
|
||||
"json": "Aus JSON importieren",
|
||||
"title": "Importieren"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importieren",
|
||||
"from_gist": "Import aus Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Επιγραφή",
|
||||
"learn_more": "Μάθε περισσότερα",
|
||||
"more": "Περισσότερο",
|
||||
"less": "Less",
|
||||
"new": "Νέος",
|
||||
"no": "Οχι",
|
||||
"preserve_current": "Διατήρηση ρεύματος",
|
||||
"prettify": "Ωραιοποιώ",
|
||||
"remove": "Αφαιρώ",
|
||||
"replace_current": "Αντικαταστήστε το ρεύμα",
|
||||
"replace_json": "Αντικαταστήστε με JSON",
|
||||
"restore": "Επαναφέρω",
|
||||
"save": "Αποθηκεύσετε",
|
||||
"search": "Αναζήτηση",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Σβήνω",
|
||||
"turn_on": "Ανάβω",
|
||||
"undo": "Ξεκάνω",
|
||||
"yes": "Ναί"
|
||||
"yes": "Ναί",
|
||||
"preserve_current": "Διατήρηση ρεύματος",
|
||||
"replace_current": "Αντικαταστήστε το ρεύμα",
|
||||
"replace_json": "Αντικαταστήστε με JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Προσθεσε νεο",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Εισαγωγή συλλογών",
|
||||
"curl": "Εισαγωγή cURL",
|
||||
"failed": "Η εισαγωγή απέτυχε",
|
||||
"from_gist": "Εισαγωγή από το Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Εισαγωγή από τις Συλλογές μου",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Εισαγάγετε Gist URL",
|
||||
"json": "Εισαγωγή από JSON",
|
||||
"title": "Εισαγωγή"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Εισαγωγή",
|
||||
"from_gist": "Εισαγωγή από το Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"more": "More",
|
||||
"less": "Less",
|
||||
"new": "New",
|
||||
"no": "No",
|
||||
"preserve_current": "Preserve current",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Remove",
|
||||
"replace_current": "Replace current",
|
||||
"replace_json": "Replace with JSON",
|
||||
"restore": "Restore",
|
||||
"save": "Save",
|
||||
"search": "Search",
|
||||
@@ -232,9 +230,20 @@
|
||||
"curl": "Import cURL",
|
||||
"failed": "Error while importing: format not recognized",
|
||||
"from_gist": "Import from Gist",
|
||||
"from_gist_description": "Import from Gist URL",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_insomnia_description": "Import from Insomnia collection",
|
||||
"from_my_collections": "Import from My Collections",
|
||||
"from_my_collections_description": "Import from My Collections file",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_url": "Import from URL",
|
||||
"from_json": "Import from Hoppscotch",
|
||||
"from_json_description": "Import from Hoppscotch collection file",
|
||||
"gist_url": "Enter Gist URL",
|
||||
"json": "Import from JSON",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import"
|
||||
},
|
||||
"layout": {
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Aprende más",
|
||||
"more": "Más",
|
||||
"less": "Less",
|
||||
"new": "Nuevo",
|
||||
"no": "No",
|
||||
"preserve_current": "Mantener actual",
|
||||
"prettify": "Prettify",
|
||||
"remove": "Eliminar",
|
||||
"replace_current": "Reemplazar actual",
|
||||
"replace_json": "Reemplazar JSON",
|
||||
"restore": "Restaurar",
|
||||
"save": "Guardar",
|
||||
"search": "Búsqueda",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Desactivar",
|
||||
"turn_on": "Activar",
|
||||
"undo": "Deshacer",
|
||||
"yes": "Sí"
|
||||
"yes": "Sí",
|
||||
"preserve_current": "Mantener actual",
|
||||
"replace_current": "Reemplazar actual",
|
||||
"replace_json": "Reemplazar JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Agregar nuevo",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importar colecciones",
|
||||
"curl": "Importar cURL",
|
||||
"failed": "Importación fallida",
|
||||
"from_gist": "Importar desde Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importar desde Mis colecciones",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Ingrese la URL de Gist",
|
||||
"json": "Importar desde JSON",
|
||||
"title": "Importar"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importar",
|
||||
"from_gist": "Importar desde Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Layout vertical",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiketti",
|
||||
"learn_more": "Lue lisää",
|
||||
"more": "Lisää",
|
||||
"less": "Less",
|
||||
"new": "Uusi",
|
||||
"no": "Ei",
|
||||
"preserve_current": "Säilytä virta",
|
||||
"prettify": "Koristella",
|
||||
"remove": "Poista",
|
||||
"replace_current": "Vaihda virta",
|
||||
"replace_json": "Korvaa JSONilla",
|
||||
"restore": "Palauttaa",
|
||||
"save": "Tallentaa",
|
||||
"search": "Hae",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Sammuttaa",
|
||||
"turn_on": "Kiihottua",
|
||||
"undo": "Kumoa",
|
||||
"yes": "Joo"
|
||||
"yes": "Joo",
|
||||
"preserve_current": "Säilytä virta",
|
||||
"replace_current": "Vaihda virta",
|
||||
"replace_json": "Korvaa JSONilla"
|
||||
},
|
||||
"add": {
|
||||
"new": "Lisää uusi",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Tuo kokoelmia",
|
||||
"curl": "Tuo cURL",
|
||||
"failed": "Tuonti epäonnistui",
|
||||
"from_gist": "Tuo Gististä",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Tuo omista kokoelmista",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Anna Gist URL",
|
||||
"json": "Tuo JSONista",
|
||||
"title": "Tuonti"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Tuonti",
|
||||
"from_gist": "Tuo Gististä"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Étiqueter",
|
||||
"learn_more": "En savoir plus",
|
||||
"more": "Suite",
|
||||
"less": "Less",
|
||||
"new": "Nouveau",
|
||||
"no": "Non",
|
||||
"preserve_current": "Conserver l'actuel",
|
||||
"prettify": "Formater",
|
||||
"remove": "Supprimer",
|
||||
"replace_current": "Remplacer l'actuel",
|
||||
"replace_json": "Remplacer par JSON",
|
||||
"restore": "Restaurer",
|
||||
"save": "Sauvegarder",
|
||||
"search": "Chercher",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Éteindre",
|
||||
"turn_on": "Allumer",
|
||||
"undo": "Annuler",
|
||||
"yes": "Oui"
|
||||
"yes": "Oui",
|
||||
"preserve_current": "Conserver l'actuel",
|
||||
"replace_current": "Remplacer l'actuel",
|
||||
"replace_json": "Remplacer par JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Ajouter un nouveau",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importer des collections",
|
||||
"curl": "Importer en cURL",
|
||||
"failed": "Échec de l'importation",
|
||||
"from_gist": "Importer depuis Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importer depuis Mes collections",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Entrez l'URL principale",
|
||||
"json": "Importer depuis JSON",
|
||||
"title": "Importer"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importer",
|
||||
"from_gist": "Importer depuis Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Disposition verticale",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "תווית",
|
||||
"learn_more": "למד עוד",
|
||||
"more": "יותר",
|
||||
"less": "Less",
|
||||
"new": "חָדָשׁ",
|
||||
"no": "לא",
|
||||
"preserve_current": "שמור על זרם",
|
||||
"prettify": "לְיַפּוֹת",
|
||||
"remove": "לְהַסִיר",
|
||||
"replace_current": "החלף זרם",
|
||||
"replace_json": "החלף ב- JSON",
|
||||
"restore": "לשחזר",
|
||||
"save": "להציל",
|
||||
"search": "לחפש",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "לכבות",
|
||||
"turn_on": "להדליק",
|
||||
"undo": "לבטל",
|
||||
"yes": "כן"
|
||||
"yes": "כן",
|
||||
"preserve_current": "שמור על זרם",
|
||||
"replace_current": "החלף זרם",
|
||||
"replace_json": "החלף ב- JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "הוסף חדש",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "ייבוא אוספים",
|
||||
"curl": "ייבא cURL",
|
||||
"failed": "הייבוא נכשל",
|
||||
"from_gist": "ייבוא מ- Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "ייבוא מהאוספים שלי",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "הזן Gist URL",
|
||||
"json": "ייבוא מ- JSON",
|
||||
"title": "יְבוּא"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "יְבוּא",
|
||||
"from_gist": "ייבוא מ- Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Címke",
|
||||
"learn_more": "Tudj meg többet",
|
||||
"more": "Több",
|
||||
"less": "Less",
|
||||
"new": "Új",
|
||||
"no": "Nem",
|
||||
"preserve_current": "Tartsa meg az áramot",
|
||||
"prettify": "Szépít",
|
||||
"remove": "Eltávolítás",
|
||||
"replace_current": "Cserélje ki az áramot",
|
||||
"replace_json": "Cserélje ki JSON -ra",
|
||||
"restore": "visszaállítás",
|
||||
"save": "Mentés",
|
||||
"search": "Keresés",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Kikapcsolni",
|
||||
"turn_on": "Bekapcsol",
|
||||
"undo": "Visszavonás",
|
||||
"yes": "Igen"
|
||||
"yes": "Igen",
|
||||
"preserve_current": "Tartsa meg az áramot",
|
||||
"replace_current": "Cserélje ki az áramot",
|
||||
"replace_json": "Cserélje ki JSON -ra"
|
||||
},
|
||||
"add": {
|
||||
"new": "Új hozzáadása",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Gyűjtemények importálása",
|
||||
"curl": "CURL importálása",
|
||||
"failed": "Az importálás sikertelen",
|
||||
"from_gist": "Importálás a Gist -ből",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importálás a Saját gyűjteményekből",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Írja be a lényeg URL -jét",
|
||||
"json": "Importálás a JSON -ból",
|
||||
"title": "Importálás"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importálás",
|
||||
"from_gist": "Importálás a Gist -ből"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etichetta",
|
||||
"learn_more": "Per saperne di più",
|
||||
"more": "Di più",
|
||||
"less": "Less",
|
||||
"new": "Nuovo",
|
||||
"no": "No",
|
||||
"preserve_current": "Conserva attuale",
|
||||
"prettify": "Abbellisci",
|
||||
"remove": "Rimuovi",
|
||||
"replace_current": "Sostituisci attuale",
|
||||
"replace_json": "Sostituisci con JSON",
|
||||
"restore": "Ripristina",
|
||||
"save": "Salva",
|
||||
"search": "Cerca",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Spegni",
|
||||
"turn_on": "Accendi",
|
||||
"undo": "Annulla",
|
||||
"yes": "Sì"
|
||||
"yes": "Sì",
|
||||
"preserve_current": "Conserva attuale",
|
||||
"replace_current": "Sostituisci attuale",
|
||||
"replace_json": "Sostituisci con JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Aggiungi nuova",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importa raccolte",
|
||||
"curl": "Importa cURL",
|
||||
"failed": "Importazione non riuscita",
|
||||
"from_gist": "Importa da Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importa da Le Mie Raccolte",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Inserisci l'URL del Gist",
|
||||
"json": "Importa da JSON",
|
||||
"title": "Importa"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importa",
|
||||
"from_gist": "Importa da Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Disposizione verticale",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "ラベル",
|
||||
"learn_more": "もっと詳しく知る",
|
||||
"more": "もっと",
|
||||
"less": "Less",
|
||||
"new": "新しい",
|
||||
"no": "番号",
|
||||
"preserve_current": "現在を維持する",
|
||||
"prettify": "きれいにする",
|
||||
"remove": "削除する",
|
||||
"replace_current": "現在を交換してください",
|
||||
"replace_json": "JSONに置き換えます",
|
||||
"restore": "戻す",
|
||||
"save": "保存する",
|
||||
"search": "探す",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "消す",
|
||||
"turn_on": "オンにする",
|
||||
"undo": "元に戻す",
|
||||
"yes": "はい"
|
||||
"yes": "はい",
|
||||
"preserve_current": "現在を維持する",
|
||||
"replace_current": "現在を交換してください",
|
||||
"replace_json": "JSONに置き換えます"
|
||||
},
|
||||
"add": {
|
||||
"new": "新しく追加する",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "コレクションをインポートする",
|
||||
"curl": "cURLをインポートする",
|
||||
"failed": "インポートに失敗しました",
|
||||
"from_gist": "要旨からインポート",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "マイコレクションからインポート",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "要旨URLを入力してください",
|
||||
"json": "JSONからインポート",
|
||||
"title": "輸入"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "輸入",
|
||||
"from_gist": "要旨からインポート"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "이름",
|
||||
"learn_more": "더 알아보기",
|
||||
"more": "더보기",
|
||||
"less": "Less",
|
||||
"new": "추가",
|
||||
"no": "아니요",
|
||||
"preserve_current": "현재 보존",
|
||||
"prettify": "예쁘게",
|
||||
"remove": "제거",
|
||||
"replace_current": "현재 교체",
|
||||
"replace_json": "JSON으로 바꾸기",
|
||||
"restore": "복원",
|
||||
"save": "저장",
|
||||
"search": "검색",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "끄기",
|
||||
"turn_on": "켜기",
|
||||
"undo": "실행 취소",
|
||||
"yes": "예"
|
||||
"yes": "예",
|
||||
"preserve_current": "현재 보존",
|
||||
"replace_current": "현재 교체",
|
||||
"replace_json": "JSON으로 바꾸기"
|
||||
},
|
||||
"add": {
|
||||
"new": "추가",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "모음집 가져오기",
|
||||
"curl": "cURL 가져오기",
|
||||
"failed": "가져오기 실패",
|
||||
"from_gist": "요점에서 가져오기",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "내 모음집에서 가져오기",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "요점 URL 입력",
|
||||
"json": "JSON에서 가져오기",
|
||||
"title": "가저오기"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "가저오기",
|
||||
"from_gist": "요점에서 가져오기"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Label",
|
||||
"learn_more": "Leer meer",
|
||||
"more": "Meer",
|
||||
"less": "Less",
|
||||
"new": "Nieuw",
|
||||
"no": "Nee",
|
||||
"preserve_current": "Huidige behouden",
|
||||
"prettify": "Netter opmaken",
|
||||
"remove": "Verwijderen",
|
||||
"replace_current": "Vervang huidige",
|
||||
"replace_json": "Vervang door JSON",
|
||||
"restore": "Herstellen",
|
||||
"save": "Opslaan",
|
||||
"search": "Zoeken",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Uitschakelen",
|
||||
"turn_on": "Inschakelen",
|
||||
"undo": "Ongedaan maken",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Huidige behouden",
|
||||
"replace_current": "Vervang huidige",
|
||||
"replace_json": "Vervang door JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Nieuwe toevoegen",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Collecties importeren",
|
||||
"curl": "cURL-commando importeren",
|
||||
"failed": "Importeren mislukt",
|
||||
"from_gist": "Importeren uit Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importeren uit Mijn collecties",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Vul de hoofd-URL in",
|
||||
"json": "Importeren uit JSON",
|
||||
"title": "Importeren"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importeren",
|
||||
"from_gist": "Importeren uit Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Merkelapp",
|
||||
"learn_more": "Lær mer",
|
||||
"more": "Mer",
|
||||
"less": "Less",
|
||||
"new": "Ny",
|
||||
"no": "Nei",
|
||||
"preserve_current": "Bevar nåværende",
|
||||
"prettify": "Forskjønn",
|
||||
"remove": "Ta bort",
|
||||
"replace_current": "Bytt ut nåværende",
|
||||
"replace_json": "Erstatt JSON",
|
||||
"restore": "Gjenopprett",
|
||||
"save": "Lagre",
|
||||
"search": "Søk",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Skru av",
|
||||
"turn_on": "Slå på",
|
||||
"undo": "Angre",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Bevar nåværende",
|
||||
"replace_current": "Bytt ut nåværende",
|
||||
"replace_json": "Erstatt JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Legg til ny",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importer samlinger",
|
||||
"curl": "Importer cURL",
|
||||
"failed": "Import mislyktes",
|
||||
"from_gist": "Import fra Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importer fra Mine samlinger",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Skriv inn Gist URL",
|
||||
"json": "Import fra JSON",
|
||||
"title": "Import"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import",
|
||||
"from_gist": "Import fra Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etykieta",
|
||||
"learn_more": "Dowiedz się więcej",
|
||||
"more": "Więcej",
|
||||
"less": "Less",
|
||||
"new": "Nowa",
|
||||
"no": "Nie",
|
||||
"preserve_current": "Zachowaj bieżący",
|
||||
"prettify": "Popraw czytelność",
|
||||
"remove": "Usuń",
|
||||
"replace_current": "Zastąp bieżący",
|
||||
"replace_json": "Zastąp JSON",
|
||||
"restore": "Przywróć",
|
||||
"save": "Zapisz",
|
||||
"search": "Szukaj",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Wyłącz",
|
||||
"turn_on": "Włącz",
|
||||
"undo": "Cofnij",
|
||||
"yes": "Tak"
|
||||
"yes": "Tak",
|
||||
"preserve_current": "Zachowaj bieżący",
|
||||
"replace_current": "Zastąp bieżący",
|
||||
"replace_json": "Zastąp JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Dodaj nowe",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importuj kolekcje",
|
||||
"curl": "Importuj CURL",
|
||||
"failed": "Import nie powiódł się",
|
||||
"from_gist": "Importuj z Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importuj z Moich kolekcji",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Wpisz adres URL Gist",
|
||||
"json": "Importuj z JSON",
|
||||
"title": "Import"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import",
|
||||
"from_gist": "Importuj z Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Pionowy układ",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Saber mais",
|
||||
"more": "Mais",
|
||||
"less": "Less",
|
||||
"new": "Novo",
|
||||
"no": "Não",
|
||||
"preserve_current": "Preservar corrente",
|
||||
"prettify": "Embelezar",
|
||||
"remove": "Remover",
|
||||
"replace_current": "Substitua a corrente",
|
||||
"replace_json": "Substitua por JSON",
|
||||
"restore": "Restaurar",
|
||||
"save": "Salvar",
|
||||
"search": "Procurar",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Desligar",
|
||||
"turn_on": "Ligar",
|
||||
"undo": "Desfazer",
|
||||
"yes": "sim"
|
||||
"yes": "sim",
|
||||
"preserve_current": "Preservar corrente",
|
||||
"replace_current": "Substitua a corrente",
|
||||
"replace_json": "Substitua por JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Adicionar novo",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importar coleções",
|
||||
"curl": "Importar cURL",
|
||||
"failed": "A importação falhou",
|
||||
"from_gist": "Importar do Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importar de minhas coleções",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Insira o URL da essência",
|
||||
"json": "Importar de JSON",
|
||||
"title": "Importar"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importar",
|
||||
"from_gist": "Importar do Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Saber mais",
|
||||
"more": "Mais",
|
||||
"less": "Less",
|
||||
"new": "Novo",
|
||||
"no": "Não",
|
||||
"preserve_current": "Preservar corrente",
|
||||
"prettify": "Embelezar",
|
||||
"remove": "Remover",
|
||||
"replace_current": "Substitua a corrente",
|
||||
"replace_json": "Substitua por JSON",
|
||||
"restore": "Restaurar",
|
||||
"save": "Salvar",
|
||||
"search": "Procurar",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Desligar",
|
||||
"turn_on": "Ligar",
|
||||
"undo": "Desfazer",
|
||||
"yes": "sim"
|
||||
"yes": "sim",
|
||||
"preserve_current": "Preservar corrente",
|
||||
"replace_current": "Substitua a corrente",
|
||||
"replace_json": "Substitua por JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Adicionar novo",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importar coleções",
|
||||
"curl": "Importar cURL",
|
||||
"failed": "A importação falhou",
|
||||
"from_gist": "Importar do Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importar de minhas coleções",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Insira o URL da essência",
|
||||
"json": "Importar de JSON",
|
||||
"title": "Importar"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importar",
|
||||
"from_gist": "Importar do Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Eticheta",
|
||||
"learn_more": "Află mai multe",
|
||||
"more": "Mai mult",
|
||||
"less": "Less",
|
||||
"new": "Nou",
|
||||
"no": "Nu",
|
||||
"preserve_current": "Păstrați curentul",
|
||||
"prettify": "Dăruiește",
|
||||
"remove": "Elimina",
|
||||
"replace_current": "Înlocuiți curentul",
|
||||
"replace_json": "Înlocuiți cu JSON",
|
||||
"restore": "Restabili",
|
||||
"save": "salva",
|
||||
"search": "Căutare",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Opriți",
|
||||
"turn_on": "Aprinde",
|
||||
"undo": "Anula",
|
||||
"yes": "da"
|
||||
"yes": "da",
|
||||
"preserve_current": "Păstrați curentul",
|
||||
"replace_current": "Înlocuiți curentul",
|
||||
"replace_json": "Înlocuiți cu JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Adăuga nou",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Import colecții",
|
||||
"curl": "Importați cURL",
|
||||
"failed": "Importul nu a reușit",
|
||||
"from_gist": "Import din Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Import din colecțiile mele",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Introduceți adresa URL Gist",
|
||||
"json": "Importați din JSON",
|
||||
"title": "Import"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Import",
|
||||
"from_gist": "Import din Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Название",
|
||||
"learn_more": "Узнать больше",
|
||||
"more": "Больше",
|
||||
"less": "Less",
|
||||
"new": "Создать новый",
|
||||
"no": "Нет",
|
||||
"preserve_current": "Сохранить текущий",
|
||||
"prettify": "Форматировать",
|
||||
"remove": "Удалить",
|
||||
"replace_current": "Заменить текущий",
|
||||
"replace_json": "Заменить на JSON",
|
||||
"restore": "Восстановить",
|
||||
"save": "Сохранить",
|
||||
"search": "Поиск",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Выключить",
|
||||
"turn_on": "Включить",
|
||||
"undo": "Отменить",
|
||||
"yes": "Да"
|
||||
"yes": "Да",
|
||||
"preserve_current": "Сохранить текущий",
|
||||
"replace_current": "Заменить текущий",
|
||||
"replace_json": "Заменить на JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Добавить новое",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Импортировать коллекции",
|
||||
"curl": "Импортировать cURL",
|
||||
"failed": "Ошибка импорта",
|
||||
"from_gist": "Импорт из Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Импортировать из моих коллекций",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Введите URL-адрес Gist",
|
||||
"json": "Импорт из JSON",
|
||||
"title": "Импортировать"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Импортировать",
|
||||
"from_gist": "Импорт из Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Вертикальное оформление",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Ознака",
|
||||
"learn_more": "Сазнајте више",
|
||||
"more": "Више",
|
||||
"less": "Less",
|
||||
"new": "Нова",
|
||||
"no": "Не",
|
||||
"preserve_current": "Очувајте струју",
|
||||
"prettify": "Преттифи",
|
||||
"remove": "Уклони",
|
||||
"replace_current": "Замените струју",
|
||||
"replace_json": "Замените са ЈСОН",
|
||||
"restore": "Ресторе",
|
||||
"save": "сачувати",
|
||||
"search": "Претрага",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Искључити",
|
||||
"turn_on": "Укључити",
|
||||
"undo": "Поништи",
|
||||
"yes": "да"
|
||||
"yes": "да",
|
||||
"preserve_current": "Очувајте струју",
|
||||
"replace_current": "Замените струју",
|
||||
"replace_json": "Замените са ЈСОН"
|
||||
},
|
||||
"add": {
|
||||
"new": "Додај нови",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Увоз збирки",
|
||||
"curl": "Увези цУРЛ",
|
||||
"failed": "Увоз није успео",
|
||||
"from_gist": "Увоз из Гиста",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Увези из Мојих колекција",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Унесите Гист УРЛ",
|
||||
"json": "Увези из ЈСОН -а",
|
||||
"title": "Увоз"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Увоз",
|
||||
"from_gist": "Увоз из Гиста"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Märka",
|
||||
"learn_more": "Läs mer",
|
||||
"more": "Mer",
|
||||
"less": "Less",
|
||||
"new": "Ny",
|
||||
"no": "Nej",
|
||||
"preserve_current": "Behåll ström",
|
||||
"prettify": "Försköna",
|
||||
"remove": "Avlägsna",
|
||||
"replace_current": "Byt ut ström",
|
||||
"replace_json": "Ersätt med JSON",
|
||||
"restore": "Återställ",
|
||||
"save": "Spara",
|
||||
"search": "Sök",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Stäng av",
|
||||
"turn_on": "Sätta på",
|
||||
"undo": "Ångra",
|
||||
"yes": "Ja"
|
||||
"yes": "Ja",
|
||||
"preserve_current": "Behåll ström",
|
||||
"replace_current": "Byt ut ström",
|
||||
"replace_json": "Ersätt med JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Lägg till ny",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Importera samlingar",
|
||||
"curl": "Importera cURL",
|
||||
"failed": "Importen misslyckades",
|
||||
"from_gist": "Importera från Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Importera från Mina samlingar",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Ange Gist URL",
|
||||
"json": "Importera från JSON",
|
||||
"title": "Importera"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Importera",
|
||||
"from_gist": "Importera från Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Etiket",
|
||||
"learn_more": "Daha fazla bilgi edin",
|
||||
"more": "Daha",
|
||||
"less": "Less",
|
||||
"new": "Yeni",
|
||||
"no": "Numara",
|
||||
"preserve_current": "Şimdikini koru",
|
||||
"prettify": "Güzelleştir",
|
||||
"remove": "Kaldır",
|
||||
"replace_current": "Şimdikini değiştir",
|
||||
"replace_json": "JSON ile değiştir",
|
||||
"restore": "Onar",
|
||||
"save": "Kaydet",
|
||||
"search": "Arama",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Kapat",
|
||||
"turn_on": "Aç",
|
||||
"undo": "Geri al",
|
||||
"yes": "Evet"
|
||||
"yes": "Evet",
|
||||
"preserve_current": "Şimdikini koru",
|
||||
"replace_current": "Şimdikini değiştir",
|
||||
"replace_json": "JSON ile değiştir"
|
||||
},
|
||||
"add": {
|
||||
"new": "Yeni ekle",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Koleksiyonları içe aktar",
|
||||
"curl": "cURL'yi içe aktar",
|
||||
"failed": "İçe aktarılamadı",
|
||||
"from_gist": "Gist'ten içe aktar",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Koleksiyonlarımdan İçe Aktar",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Gist URL'sini girin",
|
||||
"json": "JSON'dan içe aktar",
|
||||
"title": "İçe aktar"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "İçe aktar",
|
||||
"from_gist": "Gist'ten içe aktar"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Dikey görünüm",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "標籤",
|
||||
"learn_more": "瞭解更多",
|
||||
"more": "更多",
|
||||
"less": "Less",
|
||||
"new": "新增",
|
||||
"no": "否",
|
||||
"preserve_current": "保持目前",
|
||||
"prettify": "美化",
|
||||
"remove": "移除",
|
||||
"replace_current": "替換目前",
|
||||
"replace_json": "替換為 JSON",
|
||||
"restore": "還原",
|
||||
"save": "儲存",
|
||||
"search": "搜尋",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "關閉",
|
||||
"turn_on": "開啟",
|
||||
"undo": "復原",
|
||||
"yes": "是"
|
||||
"yes": "是",
|
||||
"preserve_current": "保持目前",
|
||||
"replace_current": "替換目前",
|
||||
"replace_json": "替換為 JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "新增",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "匯入組合",
|
||||
"curl": "匯入 cURL",
|
||||
"failed": "匯入失敗",
|
||||
"from_gist": "從 Gist 匯入",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "從我的組合匯入",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "輸入 Gist URL",
|
||||
"json": "從 JSON 匯入",
|
||||
"title": "匯入"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "匯入",
|
||||
"from_gist": "從 Gist 匯入"
|
||||
},
|
||||
"layout": {
|
||||
"column": "垂直布局",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Мітка",
|
||||
"learn_more": "Вчи більше",
|
||||
"more": "Більше",
|
||||
"less": "Less",
|
||||
"new": "Новий",
|
||||
"no": "Немає",
|
||||
"preserve_current": "Зберегти струм",
|
||||
"prettify": "Прикрасьте",
|
||||
"remove": "Видалити",
|
||||
"replace_current": "Замінити струм",
|
||||
"replace_json": "Замінити на JSON",
|
||||
"restore": "Відновлювати",
|
||||
"save": "Зберегти",
|
||||
"search": "Пошук",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Вимкнути",
|
||||
"turn_on": "Ввімкнути",
|
||||
"undo": "Скасувати",
|
||||
"yes": "Так"
|
||||
"yes": "Так",
|
||||
"preserve_current": "Зберегти струм",
|
||||
"replace_current": "Замінити струм",
|
||||
"replace_json": "Замінити на JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Додати новий",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Імпортувати колекції",
|
||||
"curl": "Імпортувати cURL",
|
||||
"failed": "Не вдалося імпортувати",
|
||||
"from_gist": "Імпорт з Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Імпортувати з Моїх колекцій",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Введіть URL -адресу Gist",
|
||||
"json": "Імпорт із JSON",
|
||||
"title": "Імпорт"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Імпорт",
|
||||
"from_gist": "Імпорт з Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
"label": "Nhãn",
|
||||
"learn_more": "Tìm hiểu thêm",
|
||||
"more": "Hơn",
|
||||
"less": "Less",
|
||||
"new": "Mới mẻ",
|
||||
"no": "Không",
|
||||
"preserve_current": "Bảo tồn hiện tại",
|
||||
"prettify": "Kiểm tra trước",
|
||||
"remove": "Tẩy",
|
||||
"replace_current": "Thay thế hiện tại",
|
||||
"replace_json": "Thay thế bằng JSON",
|
||||
"restore": "Khôi phục",
|
||||
"save": "Cứu",
|
||||
"search": "Tìm kiếm",
|
||||
@@ -35,7 +33,10 @@
|
||||
"turn_off": "Tắt",
|
||||
"turn_on": "Bật",
|
||||
"undo": "Hoàn tác",
|
||||
"yes": "đúng"
|
||||
"yes": "đúng",
|
||||
"preserve_current": "Bảo tồn hiện tại",
|
||||
"replace_current": "Thay thế hiện tại",
|
||||
"replace_json": "Thay thế bằng JSON"
|
||||
},
|
||||
"add": {
|
||||
"new": "Thêm mới",
|
||||
@@ -231,11 +232,18 @@
|
||||
"collections": "Nhập bộ sưu tập",
|
||||
"curl": "Nhập cURL",
|
||||
"failed": "Nhập không thành công",
|
||||
"from_gist": "Nhập từ Gist",
|
||||
"gist": "Import from Gist",
|
||||
"gist_description": "Import a Gist",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
"from_my_collections": "Nhập từ Bộ sưu tập của tôi",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_postman": "Import from Postman",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Nhập URL Gist",
|
||||
"json": "Nhập từ JSON",
|
||||
"title": "Nhập khẩu"
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "Nhập khẩu",
|
||||
"from_gist": "Nhập từ Gist"
|
||||
},
|
||||
"layout": {
|
||||
"column": "Vertical layout",
|
||||
|
||||
@@ -694,6 +694,10 @@ export function removeRESTCollection(collectionIndex: number) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getRESTCollection(collectionIndex: number) {
|
||||
return restCollectionStore.value.state[collectionIndex]
|
||||
}
|
||||
|
||||
export function editRESTCollection(
|
||||
collectionIndex: number,
|
||||
collection: Collection<HoppRESTRequest>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"gql-codegen": "graphql-codegen --config gql-codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@apollo/client": "^3.5.6",
|
||||
"@codemirror/autocomplete": "^0.19.10",
|
||||
"@codemirror/closebrackets": "^0.19.0",
|
||||
@@ -82,13 +83,17 @@
|
||||
"graphql-language-service-parser": "^1.10.4",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"httpsnippet": "^2.0.0",
|
||||
"insomnia-importers": "^2.4.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"node-interval-tree": "^1.3.3",
|
||||
"nuxt": "^2.15.8",
|
||||
"openapi-types": "^10.0.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"postman-collection": "^4.1.1",
|
||||
"rxjs": "^7.5.1",
|
||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
||||
@@ -139,6 +144,7 @@
|
||||
"@types/har-format": "^1.2.8",
|
||||
"@types/httpsnippet": "^1.23.1",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/postman-collection": "^3.5.7",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
|
||||
Reference in New Issue
Block a user