123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569 |
- <template>
- <div
- class="sticky top-0 z-10 flex-none p-4 overflow-x-auto sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary hide-scrollbar"
- >
- <div
- class="flex flex-1 overflow-auto border rounded min-w-52 border-divider whitespace-nowrap hide-scrollbar"
- >
- <div class="relative flex">
- <label for="method">
- <tippy
- ref="methodOptions"
- interactive
- trigger="click"
- theme="popover"
- arrow
- >
- <template #trigger>
- <span class="select-wrapper">
- <input
- id="method"
- class="flex px-4 py-2 font-semibold rounded-l cursor-pointer transition text-secondaryDark w-26 bg-primaryLight"
- :value="newMethod"
- :readonly="!isCustomMethod"
- :placeholder="`${t('request.method')}`"
- @input="onSelectMethod($event.target.value)"
- />
- </span>
- </template>
- <div class="flex flex-col" role="menu">
- <SmartItem
- v-for="(method, index) in methods"
- :key="`method-${index}`"
- :label="method"
- @click.native="onSelectMethod(method)"
- />
- </div>
- </tippy>
- </label>
- </div>
- <div
- class="flex flex-1 overflow-auto border-l rounded-r transition border-divider bg-primaryLight whitespace-nowrap hide-scrollbar"
- >
- <SmartEnvInput
- v-model="newEndpoint"
- :placeholder="`${t('request.url')}`"
- @enter="newSendRequest()"
- @paste="onPasteUrl($event)"
- />
- </div>
- </div>
- <div class="flex mt-2 sm:mt-0">
- <ButtonPrimary
- id="send"
- class="flex-1 rounded-r-none min-w-20"
- :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
- :on-shown="() => sendTippyActions.focus()"
- >
- <template #trigger>
- <ButtonPrimary class="rounded-l-none" filled svg="chevron-down" />
- </template>
- <div
- ref="sendTippyActions"
- class="flex flex-col focus:outline-none"
- tabindex="0"
- role="menu"
- @keyup.c="curl.$el.click()"
- @keyup.s="show.$el.click()"
- @keyup.delete="clearAll.$el.click()"
- @keyup.escape="sendOptions.tippy().hide()"
- >
- <SmartItem
- ref="curl"
- :label="`${t('import.curl')}`"
- svg="file-code"
- :shortcut="['C']"
- @click.native="
- () => {
- showCurlImportModal = !showCurlImportModal
- sendOptions.tippy().hide()
- }
- "
- />
- <SmartItem
- ref="show"
- :label="`${t('show.code')}`"
- svg="code-2"
- :shortcut="['S']"
- @click.native="
- () => {
- showCodegenModal = !showCodegenModal
- sendOptions.tippy().hide()
- }
- "
- />
- <SmartItem
- ref="clearAll"
- :label="`${t('action.clear_all')}`"
- svg="rotate-ccw"
- :shortcut="['⌫']"
- @click.native="
- () => {
- clearContent()
- sendOptions.tippy().hide()
- }
- "
- />
- </div>
- </tippy>
- </span>
- <ButtonSecondary
- class="flex-1 ml-2 rounded rounded-r-none"
- :label="COLUMN_LAYOUT ? `${t('request.save')}` : ''"
- filled
- svg="save"
- @click.native="saveRequest()"
- />
- <span class="flex">
- <tippy
- ref="saveOptions"
- interactive
- trigger="click"
- theme="popover"
- arrow
- :on-shown="() => saveTippyActions.focus()"
- >
- <template #trigger>
- <ButtonSecondary
- svg="chevron-down"
- filled
- class="rounded rounded-l-none"
- />
- </template>
- <input
- id="request-name"
- v-model="requestName"
- :placeholder="`${t('request.name')}`"
- name="request-name"
- type="text"
- autocomplete="off"
- class="mb-2 input"
- @keyup.enter="saveOptions.tippy().hide()"
- />
- <div
- ref="saveTippyActions"
- class="flex flex-col focus:outline-none"
- tabindex="0"
- role="menu"
- @keyup.c="copyRequestAction.$el.click()"
- @keyup.s="saveRequestAction.$el.click()"
- @keyup.escape="saveOptions.tippy().hide()"
- >
- <SmartItem
- ref="copyRequestAction"
- :label="shareButtonText"
- :svg="copyLinkIcon"
- :loading="fetchingShareLink"
- :shortcut="['C']"
- @click.native="
- () => {
- copyRequest()
- }
- "
- />
- <SmartItem
- svg="link-2"
- :label="`${t('request.view_my_links')}`"
- to="/profile"
- />
- <hr />
- <SmartItem
- ref="saveRequestAction"
- :label="`${t('request.save_as')}`"
- svg="folder-plus"
- :shortcut="['S']"
- @click.native="
- () => {
- showSaveRequestModal = true
- saveOptions.tippy().hide()
- }
- "
- />
- </div>
- </tippy>
- </span>
- </div>
- <HttpImportCurl
- :text="curlText"
- :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, watch } from "@nuxtjs/composition-api"
- import { isLeft, isRight } from "fp-ts/lib/Either"
- import * as E from "fp-ts/Either"
- import cloneDeep from "lodash/cloneDeep"
- import { refAutoReset } from "@vueuse/core"
- import {
- updateRESTResponse,
- restEndpoint$,
- setRESTEndpoint,
- restMethod$,
- updateRESTMethod,
- resetRESTRequest,
- useRESTRequestName,
- getRESTSaveContext,
- getRESTRequest,
- restRequest$,
- setRESTSaveContext,
- } from "~/newstore/RESTSession"
- import { editRESTRequest } from "~/newstore/collections"
- import { runRESTRequest$ } from "~/helpers/RequestRunner"
- import {
- useStreamSubscriber,
- useStream,
- useNuxt,
- useI18n,
- useToast,
- useReadonlyStream,
- } from "~/helpers/utils/composables"
- import { defineActionHandler } from "~/helpers/actions"
- import { copyToClipboard } from "~/helpers/utils/clipboard"
- import { useSetting } from "~/newstore/settings"
- import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
- import { runMutation } from "~/helpers/backend/GQLClient"
- import { UpdateRequestDocument } from "~/helpers/backend/graphql"
- const t = useI18n()
- const methods = [
- "GET",
- "POST",
- "PUT",
- "PATCH",
- "DELETE",
- "HEAD",
- "CONNECT",
- "OPTIONS",
- "TRACE",
- "CUSTOM",
- ]
- const toast = useToast()
- const nuxt = useNuxt()
- const { subscribeToStream } = useStreamSubscriber()
- const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
- const curlText = ref("")
- 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)
- const sendTippyActions = ref<any | null>(null)
- const saveTippyActions = ref<any | null>(null)
- const curl = ref<any | null>(null)
- const show = ref<any | null>(null)
- const clearAll = ref<any | null>(null)
- const copyRequestAction = ref<any | null>(null)
- const saveRequestAction = ref<any | null>(null)
- // Update Nuxt Loading bar
- watch(loading, () => {
- if (loading.value) {
- nuxt.value.$loading.start()
- } else {
- nuxt.value.$loading.finish()
- }
- })
- const newSendRequest = async () => {
- if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
- toast.error(`${t("empty.endpoint")}`)
- return
- }
- ensureMethodInEndpoint()
- loading.value = true
- // Double calling is because the function returns a TaskEither than should be executed
- const streamResult = await runRESTRequest$()()
- if (isRight(streamResult)) {
- subscribeToStream(
- streamResult.right,
- (responseState) => {
- if (loading.value) {
- // Check exists because, loading can be set to false
- // when cancelled
- updateRESTResponse(responseState)
- }
- },
- () => {
- loading.value = false
- },
- () => {
- loading.value = false
- }
- )
- } else if (isLeft(streamResult)) {
- loading.value = false
- toast.error(`${t("error.script_fail")}`)
- let error: Error
- if (typeof streamResult.left === "string") {
- error = { name: "RequestFailure", message: streamResult.left }
- } else {
- error = streamResult.left
- }
- updateRESTResponse({
- type: "script_fail",
- error,
- })
- }
- }
- const ensureMethodInEndpoint = () => {
- if (
- !/^http[s]?:\/\//.test(newEndpoint.value) &&
- !newEndpoint.value.startsWith("<<") &&
- !newEndpoint.value.startsWith("{{")
- ) {
- const domain = newEndpoint.value.split(/[/:#?]+/)[0]
- if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
- setRESTEndpoint("http://" + newEndpoint.value)
- } else {
- setRESTEndpoint("https://" + newEndpoint.value)
- }
- }
- }
- const onPasteUrl = (e: { pastedValue: string; prevValue: string }) => {
- if (!e) return
- const pastedData = e.pastedValue
- if (isCURL(pastedData)) {
- showCurlImportModal.value = true
- curlText.value = pastedData
- newEndpoint.value = e.prevValue
- }
- }
- function isCURL(curl: string) {
- return curl.includes("curl ")
- }
- 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 copyLinkIcon = refAutoReset<"share-2" | "copy" | "check">(
- hasNavigatorShare ? "share-2" : "copy",
- 1000
- )
- const shareLink = ref<string | null>("")
- const fetchingShareLink = ref(false)
- const shareButtonText = computed(() => {
- if (shareLink.value) {
- return shareLink.value
- } else if (fetchingShareLink.value) {
- return t("state.loading")
- } else {
- return t("request.copy_link")
- }
- })
- const request = useReadonlyStream(restRequest$, getRESTRequest())
- watch(request, () => {
- shareLink.value = null
- })
- const copyRequest = async () => {
- if (shareLink.value) {
- copyShareLink(shareLink.value)
- } else {
- shareLink.value = ""
- fetchingShareLink.value = true
- const request = getRESTRequest()
- const shortcodeResult = await createShortcode(request)()
- if (E.isLeft(shortcodeResult)) {
- toast.error(`${shortcodeResult.left.error}`)
- shareLink.value = `${t("error.something_went_wrong")}`
- } else if (E.isRight(shortcodeResult)) {
- shareLink.value = `/${shortcodeResult.right.createShortcode.id}`
- copyShareLink(shareLink.value)
- }
- fetchingShareLink.value = false
- }
- }
- const copyShareLink = (shareLink: string) => {
- 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: `https://hopp.sh/r${shareLink}`,
- })
- .then(() => {})
- .catch(() => {})
- } else {
- copyLinkIcon.value = "check"
- copyToClipboard(`https://hopp.sh/r${shareLink}`)
- toast.success(`${t("state.copied_to_clipboard")}`)
- }
- }
- 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") {
- const req = getRESTRequest()
- try {
- editRESTRequest(
- saveCtx.folderPath,
- saveCtx.requestIndex,
- getRESTRequest()
- )
- setRESTSaveContext({
- originLocation: "user-collection",
- folderPath: saveCtx.folderPath,
- requestIndex: saveCtx.requestIndex,
- req: cloneDeep(req),
- })
- toast.success(`${t("request.saved")}`)
- } catch (e) {
- setRESTSaveContext(null)
- saveRequest()
- }
- } else if (saveCtx.originLocation === "team-collection") {
- const req = getRESTRequest()
- // TODO: handle error case (NOTE: overwriteRequestTeams is async)
- try {
- runMutation(UpdateRequestDocument, {
- requestID: saveCtx.requestID,
- data: {
- title: req.name,
- request: JSON.stringify(req),
- },
- })().then((result) => {
- if (E.isLeft(result)) {
- toast.error(`${t("profile.no_permission")}`)
- } else {
- setRESTSaveContext({
- originLocation: "team-collection",
- requestID: saveCtx.requestID,
- req: cloneDeep(req),
- })
- toast.success(`${t("request.saved")}`)
- }
- })
- } catch (error) {
- showSaveRequestModal.value = true
- toast.error(`${t("error.something_went_wrong")}`)
- console.error(error)
- }
- }
- }
- 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 COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
- </script>
|