"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "packages/hoppscotch-app/components/http/Request.vue" between
hoppscotch-2.2.1.tar.gz and hoppscotch-3.0.0.tar.gz

About: Hoppscotch is a light-weight, web based API development suite.

Request.vue  (hoppscotch-2.2.1):Request.vue  (hoppscotch-3.0.0)
<template> <template>
<div <div
class="sticky top-0 z-10 flex p-4 space-x-2 overflow-x-auto bg-primary hide- scrollbar" class="sticky top-0 z-20 flex-none p-4 sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary"
> >
<div class="flex flex-1"> <div
class="flex flex-1 border rounded min-w-52 border-divider whitespace-nowra
p"
>
<div class="relative flex"> <div class="relative flex">
<label for="method"> <label for="method">
<tippy <tippy
ref="methodOptions" ref="methodOptions"
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
arrow arrow
:on-shown="() => methodTippyActions.focus()"
> >
<template #trigger> <span class="select-wrapper">
<span class="select-wrapper"> <input
<input id="method"
id="method" class="flex px-4 py-2 font-semibold rounded-l cursor-pointer tra
class="flex px-4 py-2 font-semibold border rounded-l cursor-po nsition text-secondaryDark w-26 bg-primaryLight"
inter bg-primaryLight border-divider text-secondaryDark w-26 hover:border-divide :value="newMethod"
rDark focus-visible:bg-transparent focus-visible:border-dividerDark" :readonly="!isCustomMethod"
:value="newMethod" :placeholder="`${t('request.method')}`"
:readonly="!isCustomMethod" @input="onSelectMethod($event.target.value)"
:placeholder="`${t('request.method')}`" />
@input="onSelectMethod($event.target.value)" </span>
<template #content="{ hide }">
<div
ref="methodTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
v-for="(method, index) in methods"
:key="`method-${index}`"
:label="method"
@click="
() => {
onSelectMethod(method)
hide()
}
"
/> />
</span> </div>
</template> </template>
<SmartItem
v-for="(method, index) in methods"
:key="`method-${index}`"
:label="method"
@click.native="onSelectMethod(method)"
/>
</tippy> </tippy>
</label> </label>
</div> </div>
<div class="flex flex-1"> <div
class="flex flex-1 overflow-auto border-l rounded-r transition border-di
vider bg-primaryLight whitespace-nowrap"
>
<SmartEnvInput <SmartEnvInput
v-model="newEndpoint" v-model="newEndpoint"
:placeholder="`${t('request.url')}`" :placeholder="`${t('request.url')}`"
styles="
bg-primaryLight
border border-divider
flex
flex-1
rounded-r
text-secondaryDark
min-w-32
py-1
px-4
hover:border-dividerDark
focus-visible:border-dividerDark
focus-visible:bg-transparent
"
@enter="newSendRequest()" @enter="newSendRequest()"
@paste="onPasteUrl($event)" @paste="onPasteUrl($event)"
/> />
</div> </div>
</div> </div>
<div class="flex"> <div class="flex mt-2 sm:mt-0">
<ButtonPrimary <ButtonPrimary
id="send" id="send"
class="flex-1 rounded-r-none min-w-20" class="flex-1 rounded-r-none min-w-20"
:label="`${!loading ? t('action.send') : t('action.cancel')}`" :label="`${!loading ? t('action.send') : t('action.cancel')}`"
@click.native="!loading ? newSendRequest() : cancelRequest()" @click="!loading ? newSendRequest() : cancelRequest()"
/> />
<span class="flex"> <span class="flex">
<tippy <tippy
ref="sendOptions" ref="sendOptions"
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
arrow arrow
:on-shown="() => sendTippyActions.focus()" :on-shown="() => sendTippyActions.focus()"
> >
<template #trigger> <ButtonPrimary
<ButtonPrimary class="rounded-l-none" filled svg="chevron-down" /> class="rounded-l-none"
filled
:icon="IconChevronDown"
/>
<template #content="{ hide }">
<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="hide()"
>
<SmartItem
ref="curl"
:label="`${t('import.curl')}`"
:icon="IconFileCode"
:shortcut="['C']"
@click="
() => {
showCurlImportModal = !showCurlImportModal
hide()
}
"
/>
<SmartItem
ref="show"
:label="`${t('show.code')}`"
:icon="IconCode2"
:shortcut="['S']"
@click="
() => {
showCodegenModal = !showCodegenModal
hide()
}
"
/>
<SmartItem
ref="clearAll"
:label="`${t('action.clear_all')}`"
:icon="IconRotateCCW"
:shortcut="['⌫']"
@click="
() => {
clearContent()
hide()
}
"
/>
</div>
</template> </template>
<div
ref="sendTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@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> </tippy>
</span> </span>
<ButtonSecondary <ButtonSecondary
class="ml-2 rounded rounded-r-none" class="flex-1 ml-2 rounded rounded-r-none"
:label=" :label="COLUMN_LAYOUT ? `${t('request.save')}` : ''"
windowInnerWidth.x.value >= 768 && COLUMN_LAYOUT
? `${t('request.save')}`
: ''
"
filled filled
svg="save" :icon="IconSave"
@click.native="saveRequest()" @click="saveRequest()"
/> />
<span class="flex"> <span class="flex">
<tippy <tippy
ref="saveOptions" ref="saveOptions"
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
arrow arrow
:on-shown="() => saveTippyActions.focus()" :on-shown="() => saveTippyActions.focus()"
> >
<template #trigger> <ButtonSecondary
<ButtonSecondary :icon="IconChevronDown"
svg="chevron-down" filled
filled class="rounded rounded-l-none"
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 <template #content="{ hide }">
ref="saveTippyActions" <input
class="flex flex-col focus:outline-none" id="request-name"
tabindex="0" v-model="requestName"
@keyup.c="copyRequestAction.$el.click()" :placeholder="`${t('request.name')}`"
@keyup.s="saveRequestAction.$el.click()" name="request-name"
@keyup.escape="saveOptions.tippy().hide()" type="text"
> autocomplete="off"
<SmartItem class="mb-2 input"
ref="copyRequestAction" @keyup.enter="hide()"
:label="shareButtonText"
:svg="copyLinkIcon"
:loading="fetchingShareLink"
:shortcut="['C']"
@click.native="
() => {
copyRequest()
}
"
/> />
<SmartItem <div
ref="saveRequestAction" ref="saveTippyActions"
:label="`${t('request.save_as')}`" class="flex flex-col focus:outline-none"
svg="folder-plus" tabindex="0"
:shortcut="['S']" role="menu"
@click.native=" @keyup.c="copyRequestAction.$el.click()"
() => { @keyup.s="saveRequestAction.$el.click()"
showSaveRequestModal = true @keyup.escape="hide()"
saveOptions.tippy().hide() >
} <SmartItem
" ref="copyRequestAction"
/> :label="shareButtonText"
</div> :icon="copyLinkIcon"
:loading="fetchingShareLink"
:shortcut="['C']"
@click="
() => {
copyRequest()
}
"
/>
<SmartItem
:icon="IconLink2"
:label="`${t('request.view_my_links')}`"
to="/profile"
/>
<hr />
<SmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
:icon="IconFolderPlus"
:shortcut="['S']"
@click="
() => {
showSaveRequestModal = true
hide()
}
"
/>
</div>
</template>
</tippy> </tippy>
</span> </span>
</div> </div>
<HttpImportCurl <HttpImportCurl
:text="curlText" :text="curlText"
:show="showCurlImportModal" :show="showCurlImportModal"
@hide-modal="showCurlImportModal = false" @hide-modal="showCurlImportModal = false"
/> />
<HttpCodegenModal <HttpCodegenModal
:show="showCodegenModal" :show="showCodegenModal"
skipping to change at line 217 skipping to change at line 229
/> />
<CollectionsSaveRequest <CollectionsSaveRequest
mode="rest" mode="rest"
:show="showSaveRequestModal" :show="showSaveRequestModal"
@hide-modal="showSaveRequestModal = false" @hide-modal="showSaveRequestModal = false"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "@nuxtjs/composition-api" import IconShare2 from "~icons/lucide/share-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconFileCode from "~icons/lucide/file-code"
import IconCode2 from "~icons/lucide/code-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconLink2 from "~icons/lucide/link-2"
import IconFolderPlus from "~icons/lucide/folder-plus"
import { computed, ref, watch } from "vue"
import { isLeft, isRight } from "fp-ts/lib/Either" import { isLeft, isRight } from "fp-ts/lib/Either"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import { refAutoReset } from "@vueuse/core"
import { import {
updateRESTResponse, updateRESTResponse,
restEndpoint$, restEndpoint$,
setRESTEndpoint, setRESTEndpoint,
restMethod$, restMethod$,
updateRESTMethod, updateRESTMethod,
resetRESTRequest, resetRESTRequest,
useRESTRequestName, useRESTRequestName,
getRESTSaveContext, getRESTSaveContext,
getRESTRequest, getRESTRequest,
restRequest$, restRequest$,
setRESTSaveContext, setRESTSaveContext,
} from "~/newstore/RESTSession" } from "~/newstore/RESTSession"
import { editRESTRequest } from "~/newstore/collections" import { editRESTRequest } from "~/newstore/collections"
import { runRESTRequest$ } from "~/helpers/RequestRunner" import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { import {
useStreamSubscriber,
useStream, useStream,
useNuxt, useStreamSubscriber,
useI18n,
useToast,
useReadonlyStream, useReadonlyStream,
} from "~/helpers/utils/composables" } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useSetting } from "@composables/settings"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useSetting } from "~/newstore/settings"
import { overwriteRequestTeams } from "~/helpers/teams/utils"
import { apolloClient } from "~/helpers/apollo"
import useWindowSize from "~/helpers/utils/useWindowSize"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode" import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
const t = useI18n() const t = useI18n()
const methods = [ const methods = [
"GET", "GET",
"POST", "POST",
"PUT", "PUT",
"PATCH", "PATCH",
"DELETE", "DELETE",
"HEAD", "HEAD",
"CONNECT", "CONNECT",
"OPTIONS", "OPTIONS",
"TRACE", "TRACE",
"CUSTOM", "CUSTOM",
] ]
const toast = useToast() const toast = useToast()
const nuxt = useNuxt()
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint) const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
const curlText = ref("") const curlText = ref("")
const newMethod = useStream(restMethod$, "", updateRESTMethod) const newMethod = useStream(restMethod$, "", updateRESTMethod)
const loading = ref(false) const loading = ref(false)
const showCurlImportModal = ref(false) const showCurlImportModal = ref(false)
const showCodegenModal = ref(false) const showCodegenModal = ref(false)
const showSaveRequestModal = ref(false) const showSaveRequestModal = ref(false)
const hasNavigatorShare = !!navigator.share const hasNavigatorShare = !!navigator.share
// Template refs // Template refs
const methodOptions = ref<any | null>(null) const methodOptions = ref<any | null>(null)
const saveOptions = ref<any | null>(null) const saveOptions = ref<any | null>(null)
const sendOptions = ref<any | null>(null) const sendOptions = ref<any | null>(null)
const methodTippyActions = ref<any | null>(null)
const sendTippyActions = ref<any | null>(null) const sendTippyActions = ref<any | null>(null)
const saveTippyActions = ref<any | null>(null) const saveTippyActions = ref<any | null>(null)
const curl = ref<any | null>(null) const curl = ref<any | null>(null)
const show = ref<any | null>(null) const show = ref<any | null>(null)
const clearAll = ref<any | null>(null) const clearAll = ref<any | null>(null)
const copyRequestAction = ref<any | null>(null) const copyRequestAction = ref<any | null>(null)
const saveRequestAction = ref<any | null>(null) const saveRequestAction = ref<any | null>(null)
// Update Nuxt Loading bar // Update Nuxt Loading bar
watch(loading, () => { watch(loading, () => {
if (loading.value) { if (loading.value) {
nuxt.value.$loading.start() startPageProgress()
} else { } else {
nuxt.value.$loading.finish() completePageProgress()
} }
}) })
const newSendRequest = async () => { const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) { if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`) toast.error(`${t("empty.endpoint")}`)
return return
} }
ensureMethodInEndpoint()
loading.value = true loading.value = true
// Double calling is because the function returns a TaskEither than should be executed // Double calling is because the function returns a TaskEither than should be executed
const streamResult = await runRESTRequest$()() const streamResult = await runRESTRequest$()()
if (isRight(streamResult)) { if (isRight(streamResult)) {
subscribeToStream( subscribeToStream(
streamResult.right, streamResult.right,
(responseState) => { (responseState) => {
if (loading.value) { if (loading.value) {
skipping to change at line 348 skipping to change at line 373
} else { } else {
error = streamResult.left error = streamResult.left
} }
updateRESTResponse({ updateRESTResponse({
type: "script_fail", type: "script_fail",
error, error,
}) })
} }
} }
const onPasteUrl = (e: { event: ClipboardEvent; previousValue: string }) => { const ensureMethodInEndpoint = () => {
if (!e) return if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
const clipboardData = e.event.clipboardData !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 pastedData = clipboardData?.getData("Text") const onPasteUrl = (e: { pastedValue: string; prevValue: string }) => {
if (!e) return
if (!pastedData) return const pastedData = e.pastedValue
if (isCURL(pastedData)) { if (isCURL(pastedData)) {
e.event.preventDefault()
showCurlImportModal.value = true showCurlImportModal.value = true
curlText.value = pastedData curlText.value = pastedData
newEndpoint.value = e.previousValue newEndpoint.value = e.prevValue
} }
} }
function isCURL(curl: string) { function isCURL(curl: string) {
return curl.includes("curl ") return curl.includes("curl ")
} }
const cancelRequest = () => { const cancelRequest = () => {
loading.value = false loading.value = false
updateRESTResponse(null) updateRESTResponse(null)
} }
const updateMethod = (method: string) => { const updateMethod = (method: string) => {
updateRESTMethod(method) updateRESTMethod(method)
} }
const onSelectMethod = (method: string) => { const onSelectMethod = (method: string) => {
updateMethod(method) updateMethod(method)
// Vue-tippy has no typescript support yet
methodOptions.value.tippy().hide()
} }
const clearContent = () => { const clearContent = () => {
resetRESTRequest() resetRESTRequest()
} }
const copyLinkIcon = hasNavigatorShare ? ref("share-2") : ref("copy") const copyLinkIcon = refAutoReset<
typeof IconShare2 | typeof IconCopy | typeof IconCheck
>(hasNavigatorShare ? IconShare2 : IconCopy, 1000)
const shareLink = ref<string | null>("") const shareLink = ref<string | null>("")
const fetchingShareLink = ref(false) const fetchingShareLink = ref(false)
const shareButtonText = computed(() => { const shareButtonText = computed(() => {
if (shareLink.value) { if (shareLink.value) {
return shareLink.value return shareLink.value
} else if (fetchingShareLink.value) { } else if (fetchingShareLink.value) {
return t("state.loading") return t("state.loading")
} else { } else {
return t("request.copy_link") return t("request.copy_link")
skipping to change at line 431 skipping to change at line 466
copyShareLink(shareLink.value) copyShareLink(shareLink.value)
} }
fetchingShareLink.value = false fetchingShareLink.value = false
} }
} }
const copyShareLink = (shareLink: string) => { const copyShareLink = (shareLink: string) => {
if (navigator.share) { if (navigator.share) {
const time = new Date().toLocaleTimeString() const time = new Date().toLocaleTimeString()
const date = new Date().toLocaleDateString() const date = new Date().toLocaleDateString()
navigator navigator.share({
.share({ title: "Hoppscotch",
title: "Hoppscotch", text: `Hoppscotch • Open source API development ecosystem at ${time} on ${
text: `Hoppscotch • Open source API development ecosystem at ${time} on date}`,
${date}`, url: `https://hopp.sh/r${shareLink}`,
url: `https://hopp.sh/r${shareLink}`, })
})
.then(() => {})
.catch(() => {})
} else { } else {
copyLinkIcon.value = "check" copyLinkIcon.value = IconCheck
copyToClipboard(`https://hopp.sh/r${shareLink}`) copyToClipboard(`https://hopp.sh/r${shareLink}`)
toast.success(`${t("state.copied_to_clipboard")}`) toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyLinkIcon.value = "copy"), 2000)
} }
} }
const cycleUpMethod = () => { const cycleUpMethod = () => {
const currentIndex = methods.indexOf(newMethod.value) const currentIndex = methods.indexOf(newMethod.value)
if (currentIndex === -1) { if (currentIndex === -1) {
// Most probs we are in CUSTOM mode // Most probs we are in CUSTOM mode
// Cycle up from CUSTOM is PATCH // Cycle up from CUSTOM is PATCH
updateMethod("PATCH") updateMethod("PATCH")
} else if (currentIndex === 0) { } else if (currentIndex === 0) {
skipping to change at line 479 skipping to change at line 510
updateMethod(methods[currentIndex + 1]) updateMethod(methods[currentIndex + 1])
} }
} }
const saveRequest = () => { const saveRequest = () => {
const saveCtx = getRESTSaveContext() const saveCtx = getRESTSaveContext()
if (!saveCtx) { if (!saveCtx) {
showSaveRequestModal.value = true showSaveRequestModal.value = true
return return
} }
if (saveCtx.originLocation === "user-collection") { if (saveCtx.originLocation === "user-collection") {
const req = getRESTRequest()
try { try {
editRESTRequest( editRESTRequest(
saveCtx.folderPath, saveCtx.folderPath,
saveCtx.requestIndex, saveCtx.requestIndex,
getRESTRequest() getRESTRequest()
) )
setRESTSaveContext({
originLocation: "user-collection",
folderPath: saveCtx.folderPath,
requestIndex: saveCtx.requestIndex,
req: cloneDeep(req),
})
toast.success(`${t("request.saved")}`) toast.success(`${t("request.saved")}`)
} catch (e) { } catch (e) {
setRESTSaveContext(null) setRESTSaveContext(null)
saveRequest() saveRequest()
} }
} else if (saveCtx.originLocation === "team-collection") { } else if (saveCtx.originLocation === "team-collection") {
const req = getRESTRequest() const req = getRESTRequest()
// TODO: handle error case (NOTE: overwriteRequestTeams is async) // TODO: handle error case (NOTE: overwriteRequestTeams is async)
try { try {
overwriteRequestTeams( runMutation(UpdateRequestDocument, {
apolloClient, requestID: saveCtx.requestID,
JSON.stringify(req), data: {
req.name, title: req.name,
saveCtx.requestID request: JSON.stringify(req),
) },
.then(() => { })().then((result) => {
toast.success(`${t("request.saved")}`) if (E.isLeft(result)) {
})
.catch(() => {
toast.error(`${t("profile.no_permission")}`) toast.error(`${t("profile.no_permission")}`)
}) } else {
setRESTSaveContext({
originLocation: "team-collection",
requestID: saveCtx.requestID,
req: cloneDeep(req),
})
toast.success(`${t("request.saved")}`)
}
})
} catch (error) { } catch (error) {
showSaveRequestModal.value = true showSaveRequestModal.value = true
toast.error(`${t("error.something_went_wrong")}`) toast.error(`${t("error.something_went_wrong")}`)
console.error(error) console.error(error)
} }
} }
} }
defineActionHandler("request.send-cancel", () => { defineActionHandler("request.send-cancel", () => {
if (!loading.value) newSendRequest() if (!loading.value) newSendRequest()
skipping to change at line 542 skipping to change at line 586
defineActionHandler("request.method.put", () => updateMethod("PUT")) defineActionHandler("request.method.put", () => updateMethod("PUT"))
defineActionHandler("request.method.delete", () => updateMethod("DELETE")) defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD")) defineActionHandler("request.method.head", () => updateMethod("HEAD"))
const isCustomMethod = computed(() => { const isCustomMethod = computed(() => {
return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value) return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
}) })
const requestName = useRESTRequestName() const requestName = useRESTRequestName()
const windowInnerWidth = useWindowSize()
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT") const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
</script> </script>
 End of changes. 45 change blocks. 
189 lines changed or deleted 233 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)