Files
hoppscotch/components/http/Request.vue

411 lines
11 KiB
Vue

<template>
<div class="bg-primary flex space-x-2 p-4 top-0 z-10 sticky">
<div class="flex flex-1">
<div class="flex relative">
<label for="method">
<tippy
ref="methodOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
id="method"
class="
bg-primaryLight
border border-divider
rounded-l
cursor-pointer
flex
font-semibold
text-secondaryDark
py-2
px-4
w-28
hover:border-dividerDark
focus-visible:bg-primary focus-visible:border-dividerDark
"
:value="newMethod"
:readonly="!isCustomMethod"
:placeholder="$t('request.method')"
@input="onSelectMethod($event.target.value)"
/>
</span>
</template>
<SmartItem
v-for="(method, index) in methods"
:key="`method-${index}`"
:label="method"
@click.native="onSelectMethod(method)"
/>
</tippy>
</label>
</div>
<div class="flex flex-1">
<SmartEnvInput
v-if="EXPERIMENTAL_URL_BAR_ENABLED"
v-model="newEndpoint"
:placeholder="$t('request.url')"
styles="
bg-primaryLight
border border-divider
flex
flex-1
rounded-r
text-secondaryDark
py-1
px-4
hover:border-dividerDark
focus-visible:border-dividerDark
focus-visible:bg-primary
"
@enter="newSendRequest()"
/>
<input
v-else
id="url"
v-model="newEndpoint"
v-focus
class="
bg-primaryLight
border border-divider
rounded-r
flex
text-secondaryDark
w-full
py-2
px-4
hover:border-dividerDark
focus-visible:bg-primary focus-visible:border-dividerDark
"
name="url"
type="text"
spellcheck="false"
:placeholder="$t('request.url')"
autofocus
@keyup.enter="newSendRequest()"
/>
</div>
</div>
<div class="flex">
<ButtonPrimary
id="send"
class="rounded-r-none flex-1 min-w-22"
:label="!loading ? $t('action.send') : $t('action.cancel')"
@click.native="!loading ? newSendRequest() : cancelRequest()"
/>
<span class="flex">
<tippy
ref="sendOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonPrimary
class="rounded-l-none"
filled
icon="keyboard_arrow_down"
/>
</template>
<SmartItem
:label="$t('import.curl')"
icon="import_export"
@click.native="showCurlImportModal = !showCurlImportModal"
/>
<SmartItem
:label="$t('show.code')"
icon="code"
@click.native="showCodegenModal = !showCodegenModal"
/>
<SmartItem
ref="clearAll"
:label="$t('action.clear_all')"
icon="clear_all"
@click.native="clearContent()"
/>
</tippy>
</span>
<ButtonSecondary
class="rounded-r-none ml-2"
:label="$t('request.save')"
filled
icon="save"
@click.native="saveRequest()"
/>
<span class="flex">
<tippy
ref="saveOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
icon="keyboard_arrow_down"
filled
class="rounded-r"
/>
</template>
<input
id="request-name"
v-model="requestName"
:placeholder="$t('request.name')"
name="request-name"
type="text"
class="mb-2 input"
@keyup.enter="saveOptions.tippy().hide()"
/>
<SmartItem
ref="copyRequest"
:label="$t('request.copy_link')"
:icon="hasNavigatorShare ? 'share' : 'content_copy'"
@click.native="copyRequest()"
/>
<SmartItem
ref="saveRequest"
:label="$t('request.save_as')"
icon="create_new_folder"
@click.native="showSaveRequestModal = true"
/>
</tippy>
</span>
</div>
<HttpImportCurl
:show="showCurlImportModal"
@hide-modal="showCurlImportModal = false"
/>
<HttpCodegenModal
:show="showCodegenModal"
@hide-modal="showCodegenModal = false"
/>
<CollectionsSaveRequest
mode="rest"
:show="showSaveRequestModal"
@hide-modal="showSaveRequestModal = false"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
import {
updateRESTResponse,
restEndpoint$,
setRESTEndpoint,
restMethod$,
updateRESTMethod,
resetRESTRequest,
useRESTRequestName,
getRESTSaveContext,
getRESTRequest,
} from "~/newstore/RESTSession"
import { editRESTRequest } from "~/newstore/collections"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import {
useStreamSubscriber,
useStream,
useNuxt,
} from "~/helpers/utils/composables"
import { defineActionHandler } from "~/helpers/actions"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useSetting } from "~/newstore/settings"
import { overwriteRequestTeams } from "~/helpers/teams/utils"
import { apolloClient } from "~/helpers/apollo"
const methods = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE",
"CUSTOM",
]
const {
$toast,
app: { i18n },
} = useContext()
const nuxt = useNuxt()
const t = i18n.t.bind(i18n)
const { subscribeToStream } = useStreamSubscriber()
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
const newMethod = useStream(restMethod$, "", updateRESTMethod)
const loading = ref(false)
const showCurlImportModal = ref(false)
const showCodegenModal = ref(false)
const showSaveRequestModal = ref(false)
const hasNavigatorShare = !!navigator.share
// Template refs
const methodOptions = ref<any | null>(null)
const saveOptions = ref<any | null>(null)
const sendOptions = ref<any | null>(null)
// Update Nuxt Loading bar
watch(loading, () => {
if (loading.value) {
nuxt.value.$loading.start()
} else {
nuxt.value.$loading.finish()
}
})
const newSendRequest = () => {
loading.value = true
subscribeToStream(
runRESTRequest$(),
(responseState) => {
console.log(responseState)
if (loading.value) {
// Check exists because, loading can be set to false
// when cancelled
updateRESTResponse(responseState)
}
},
() => {
loading.value = false
},
() => {
loading.value = false
}
)
}
const cancelRequest = () => {
loading.value = false
updateRESTResponse(null)
}
const updateMethod = (method: string) => {
updateRESTMethod(method)
}
const onSelectMethod = (method: string) => {
updateMethod(method)
// Vue-tippy has no typescript support yet
methodOptions.value.tippy().hide()
}
const clearContent = () => {
resetRESTRequest()
}
const copyRequest = () => {
if (navigator.share) {
const time = new Date().toLocaleTimeString()
const date = new Date().toLocaleDateString()
navigator
.share({
title: "Hoppscotch",
text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
url: window.location.href,
})
.then(() => {})
.catch(() => {})
} else {
copyToClipboard(window.location.href)
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste",
})
}
}
const cycleUpMethod = () => {
const currentIndex = methods.indexOf(newMethod.value)
if (currentIndex === -1) {
// Most probs we are in CUSTOM mode
// Cycle up from CUSTOM is PATCH
updateMethod("PATCH")
} else if (currentIndex === 0) {
updateMethod("CUSTOM")
} else {
updateMethod(methods[currentIndex - 1])
}
}
const cycleDownMethod = () => {
const currentIndex = methods.indexOf(newMethod.value)
if (currentIndex === -1) {
// Most probs we are in CUSTOM mode
// Cycle down from CUSTOM is GET
updateMethod("GET")
} else if (currentIndex === methods.length - 1) {
updateMethod("GET")
} else {
updateMethod(methods[currentIndex + 1])
}
}
const saveRequest = () => {
const saveCtx = getRESTSaveContext()
if (!saveCtx) {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "user-collection") {
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, getRESTRequest())
} else if (saveCtx.originLocation === "team-collection") {
const req = getRESTRequest()
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
try {
overwriteRequestTeams(
apolloClient,
JSON.stringify(req),
req.name,
saveCtx.requestID
)
} catch (error) {
showSaveRequestModal.value = true
return
}
}
$toast.success(t("request.saved").toString(), {
icon: "playlist_add_check",
})
}
defineActionHandler("request.send-cancel", () => {
if (!loading.value) newSendRequest()
else cancelRequest()
})
defineActionHandler("request.reset", clearContent)
defineActionHandler("request.copy-link", copyRequest)
defineActionHandler("request.method.next", cycleDownMethod)
defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest)
defineActionHandler(
"request.save-as",
() => (showSaveRequestModal.value = true)
)
defineActionHandler("request.method.get", () => updateMethod("GET"))
defineActionHandler("request.method.post", () => updateMethod("POST"))
defineActionHandler("request.method.put", () => updateMethod("PUT"))
defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
const isCustomMethod = computed(() => {
return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
})
const requestName = useRESTRequestName()
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
</script>