feat: save api responses (#4382)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -147,6 +147,8 @@ const handleImport = () => {
|
||||
type: "HOPP_REST_IMPORT_CURL",
|
||||
})
|
||||
|
||||
if (tabs.currentActiveTab.value.document.type === "example-response") return
|
||||
|
||||
tabs.currentActiveTab.value.document.request = req
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -263,7 +263,7 @@ import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
@@ -288,7 +288,7 @@ const toast = useToast()
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRequestDocument> }>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
@@ -583,10 +583,10 @@ defineActionHandler("request.reset", clearContent)
|
||||
defineActionHandler("request.share-request", shareRequest)
|
||||
defineActionHandler("request.method.next", cycleDownMethod)
|
||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler("request-response.save", saveRequest)
|
||||
defineActionHandler("request.save-as", (req) => {
|
||||
showSaveRequestModal.value = true
|
||||
if (req?.requestType === "rest") {
|
||||
if (req?.requestType === "rest" && req.request) {
|
||||
request.value = req.request
|
||||
}
|
||||
})
|
||||
|
||||
@@ -50,26 +50,35 @@
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties?.includes('preRequestScript') ?? true"
|
||||
v-if="showPreRequestScriptTab"
|
||||
:id="'preRequestScript'"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
request.preRequestScript && request.preRequestScript.length > 0
|
||||
'preRequestScript' in request &&
|
||||
request.preRequestScript &&
|
||||
request.preRequestScript.length > 0
|
||||
? true
|
||||
: false
|
||||
"
|
||||
>
|
||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||
<HttpPreRequestScript
|
||||
v-if="'preRequestScript' in request"
|
||||
v-model="request.preRequestScript"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties?.includes('tests') ?? true"
|
||||
v-if="showTestsTab"
|
||||
:id="'tests'"
|
||||
:label="`${t('tab.tests')}`"
|
||||
:indicator="
|
||||
request.testScript && request.testScript.length > 0 ? true : false
|
||||
'testScript' in request &&
|
||||
request.testScript &&
|
||||
request.testScript.length > 0
|
||||
? true
|
||||
: false
|
||||
"
|
||||
>
|
||||
<HttpTests v-model="request.testScript" />
|
||||
<HttpTests v-if="'testScript' in request" v-model="request.testScript" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties?.includes('requestVariables') ?? true"
|
||||
@@ -85,7 +94,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
HoppRESTResponseOriginalRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
@@ -109,7 +121,7 @@ const t = useI18n()
|
||||
// v-model integration with props and emit
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppRESTRequest
|
||||
modelValue: HoppRESTRequest | HoppRESTResponseOriginalRequest
|
||||
optionTab: RESTOptionTabs
|
||||
properties?: string[]
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
@@ -128,6 +140,17 @@ const emit = defineEmits<{
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
const selectedOptionTab = useVModel(props, "optionTab", emit)
|
||||
|
||||
const showPreRequestScriptTab = computed(() => {
|
||||
return (
|
||||
props.properties?.includes("preRequestScript") ??
|
||||
"preRequestScript" in request.value
|
||||
)
|
||||
})
|
||||
|
||||
const showTestsTab = computed(() => {
|
||||
return props.properties?.includes("tests") ?? "testScript" in request.value
|
||||
})
|
||||
|
||||
const changeOptionTab = (e: RESTOptionTabs) => {
|
||||
selectedOptionTab.value = e
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ import { useVModel } from "@vueuse/core"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
|
||||
// TODO: Move Response and Request execution code to over here
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRequestDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTab<HoppRESTDocument>): void
|
||||
(e: "update:modelValue", val: HoppTab<HoppRequestDocument>): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
@@ -4,22 +4,44 @@
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
v-model:document="doc"
|
||||
@save-as-example="saveAsExample"
|
||||
/>
|
||||
</div>
|
||||
<HttpSaveResponseName
|
||||
v-model="responseName"
|
||||
:show="showSaveResponseName"
|
||||
@submit="onSaveAsExample"
|
||||
@hide-modal="showSaveResponseName = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed } from "vue"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
import { useResponseBody } from "@composables/lens-actions"
|
||||
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
||||
import {
|
||||
HoppRESTResponseOriginalRequest,
|
||||
HoppRESTRequestResponse,
|
||||
} from "@hoppscotch/data"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppRESTDocument
|
||||
document: HoppRequestDocument
|
||||
isEmbed: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRESTDocument): void
|
||||
(e: "update:tab", val: HoppRequestDocument): void
|
||||
}>()
|
||||
|
||||
const doc = useVModel(props, "document", emit)
|
||||
@@ -30,5 +52,97 @@ const hasResponse = computed(
|
||||
doc.value.response?.type === "fail"
|
||||
)
|
||||
|
||||
const responseName = ref("")
|
||||
const showSaveResponseName = ref(false)
|
||||
|
||||
const loading = computed(() => doc.value.response?.type === "loading")
|
||||
|
||||
const saveAsExample = () => {
|
||||
showSaveResponseName.value = true
|
||||
}
|
||||
|
||||
const onSaveAsExample = () => {
|
||||
const response = doc.value.response
|
||||
|
||||
if (response && response.type === "success") {
|
||||
const { responseBodyText } = useResponseBody(response)
|
||||
|
||||
const statusText = getStatusCodeReasonPhrase(
|
||||
response.statusCode,
|
||||
response.statusText
|
||||
)
|
||||
|
||||
const {
|
||||
method,
|
||||
endpoint,
|
||||
headers,
|
||||
body,
|
||||
auth,
|
||||
params,
|
||||
name,
|
||||
requestVariables,
|
||||
} = response.req
|
||||
|
||||
const originalRequest: HoppRESTResponseOriginalRequest = {
|
||||
v: "1",
|
||||
method,
|
||||
endpoint,
|
||||
headers,
|
||||
body,
|
||||
auth,
|
||||
params,
|
||||
name,
|
||||
requestVariables,
|
||||
}
|
||||
|
||||
const resName = responseName.value.trim()
|
||||
|
||||
const responseObj: HoppRESTRequestResponse = {
|
||||
status: statusText,
|
||||
code: response.statusCode,
|
||||
headers: response.headers,
|
||||
body: responseBodyText.value,
|
||||
name: resName,
|
||||
originalRequest,
|
||||
}
|
||||
|
||||
doc.value.request.responses = {
|
||||
...doc.value.request.responses,
|
||||
[resName]: responseObj,
|
||||
}
|
||||
|
||||
showSaveResponseName.value = false
|
||||
|
||||
const saveCtx = doc.value.saveContext
|
||||
|
||||
if (!saveCtx) return
|
||||
|
||||
const req = doc.value.request
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
try {
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||
|
||||
toast.success(`${t("response.saved")}`)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
doc.value.isDirty = false
|
||||
|
||||
toast.success(`${t("request.saved")}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -179,9 +179,15 @@ const response = computed(() => {
|
||||
const pageCategory = getCurrentPageCategory()
|
||||
|
||||
if (pageCategory === "rest") {
|
||||
const res = restTabs.currentActiveTab.value.document.response
|
||||
if (res?.type === "success" || res?.type === "fail") {
|
||||
response = getResponseBodyText(res.body)
|
||||
const doc = restTabs.currentActiveTab.value.document
|
||||
if (doc.type === "request") {
|
||||
const res = doc.response
|
||||
if (res?.type === "success" || res?.type === "fail") {
|
||||
response = getResponseBodyText(res.body)
|
||||
}
|
||||
} else {
|
||||
const res = doc.response.body
|
||||
response = res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +250,7 @@ const filteredResponseInterfaces = computed(() => {
|
||||
const { copyIcon, copyResponse } = useCopyResponse(interfaceCode)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"",
|
||||
interfaceCode
|
||||
interfaceCode,
|
||||
t("filename.response_interface")
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('modal.response_name')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex gap-1">
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
class="flex-grow"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editRequest"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="editRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
loadingState: boolean
|
||||
modelValue?: string
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
loadingState: false,
|
||||
modelValue: "",
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit", name: string): void
|
||||
(e: "hide-modal"): void
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const editingName = useVModel(props, "modelValue")
|
||||
|
||||
const editRequest = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("response.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:title="tab.document.request.name"
|
||||
:title="tabState.name"
|
||||
class="flex items-center truncate px-2"
|
||||
@dblclick="emit('open-rename-modal')"
|
||||
@contextmenu.prevent="options?.tippy?.show()"
|
||||
@click.middle="emit('close-tab')"
|
||||
>
|
||||
<span
|
||||
class="text-tiny font-semibold mr-2"
|
||||
:style="{ color: getMethodLabelColorClassOf(tab.document.request) }"
|
||||
class="text-tiny font-semibold mr-2 p-1 rounded-sm relative"
|
||||
:class="{
|
||||
'border border-dashed border-primaryDark grayscale': isResponseExample,
|
||||
}"
|
||||
:style="{ color: getMethodLabelColorClassOf(tabState.method) }"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
{{ tabState.method }}
|
||||
</span>
|
||||
<tippy
|
||||
ref="options"
|
||||
@@ -21,7 +24,7 @@
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ tab.document.request.name }}
|
||||
{{ tabState.name }}
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
@@ -36,6 +39,7 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-if="!isResponseExample"
|
||||
ref="renameAction"
|
||||
:icon="IconFileEdit"
|
||||
:label="t('request.rename')"
|
||||
@@ -48,6 +52,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isResponseExample"
|
||||
ref="duplicateAction"
|
||||
:icon="IconCopy"
|
||||
:label="t('tab.duplicate')"
|
||||
@@ -60,6 +65,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isResponseExample"
|
||||
ref="shareRequestAction"
|
||||
:icon="IconShare2"
|
||||
:label="t('tab.share_tab_request')"
|
||||
@@ -104,7 +110,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { ref, computed } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
@@ -114,15 +120,37 @@ import IconFileEdit from "~icons/lucide/file-edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import {
|
||||
HoppRequestDocument,
|
||||
HoppSavedExampleDocument,
|
||||
} from "~/helpers/rest/document"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
tab: HoppTab<HoppRESTDocument>
|
||||
const props = defineProps<{
|
||||
tab: HoppTab<HoppRequestDocument | HoppSavedExampleDocument>
|
||||
isRemovable: boolean
|
||||
}>()
|
||||
|
||||
const tabState = computed(() => {
|
||||
if (props.tab.document.type === "request") {
|
||||
return {
|
||||
name: props.tab.document.request.name,
|
||||
method: props.tab.document.request.method,
|
||||
request: props.tab.document.request,
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: props.tab.document.response.name,
|
||||
method: props.tab.document.response.originalRequest.method,
|
||||
request: props.tab.document.response.originalRequest,
|
||||
}
|
||||
})
|
||||
|
||||
const isResponseExample = computed(() => {
|
||||
return props.tab.document.type === "example-response"
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "open-rename-modal"): void
|
||||
(event: "close-tab"): void
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedLensTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
|
||||
>
|
||||
<HoppSmartTab
|
||||
v-for="(lens, index) in validLenses"
|
||||
:id="lens.renderer"
|
||||
:key="`lens-${index}`"
|
||||
:label="t(lens.lensName)"
|
||||
class="flex h-full w-full flex-1 flex-col"
|
||||
>
|
||||
<component
|
||||
:is="lensRendererFor(lens.renderer)"
|
||||
v-model:response="doc.response"
|
||||
:is-savable="false"
|
||||
:is-editable="true"
|
||||
@save-as-example="$emit('save-as-example')"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="doc.response.headers"
|
||||
id="headers"
|
||||
:label="t('response.headers')"
|
||||
:info="`${doc.response.headers.length}`"
|
||||
class="flex flex-1 flex-col"
|
||||
>
|
||||
<LensesHeadersRenderer
|
||||
v-model="doc.response.headers"
|
||||
:is-editable="true"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import {
|
||||
getSuitableLenses,
|
||||
getLensRenderers,
|
||||
Lens,
|
||||
} from "~/helpers/lenses/lenses"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import {
|
||||
HoppRequestDocument,
|
||||
HoppSavedExampleDocument,
|
||||
} from "~/helpers/rest/document"
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppSavedExampleDocument
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:document", document: HoppRequestDocument): void
|
||||
(e: "save-as-example"): void
|
||||
}>()
|
||||
|
||||
const doc = useVModel(props, "document", emit)
|
||||
|
||||
const allLensRenderers = getLensRenderers()
|
||||
|
||||
function lensRendererFor(name: string) {
|
||||
return allLensRenderers[name]
|
||||
}
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const selectedLensTab = ref("")
|
||||
|
||||
const validLenses = computed(() => {
|
||||
if (!doc.value.response) return []
|
||||
return getSuitableLenses(doc.value.response)
|
||||
})
|
||||
|
||||
watch(
|
||||
validLenses,
|
||||
(newLenses: Lens[]) => {
|
||||
if (newLenses.length === 0 || selectedLensTab.value) return
|
||||
|
||||
selectedLensTab.value = newLenses[0].renderer
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<HttpExampleResponseMeta v-model:response="doc.response" />
|
||||
<HttpExampleLenseBodyRenderer v-model:document="doc" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import {
|
||||
HoppRequestDocument,
|
||||
HoppSavedExampleDocument,
|
||||
} from "~/helpers/rest/document"
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppSavedExampleDocument
|
||||
isEmbed: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRequestDocument): void
|
||||
}>()
|
||||
|
||||
const doc = useVModel(props, "document", emit)
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-50 flex-none flex-shrink-0 items-center justify-center whitespace-nowrap bg-primary p-4"
|
||||
>
|
||||
<div v-if="responseCtx" class="flex flex-1 flex-col">
|
||||
<div class="flex items-center text-tiny font-semibold">
|
||||
<div class="inline-flex flex-1 space-x-4">
|
||||
<div class="flex-1 flex items-center space-x-2">
|
||||
<span class="text-secondary"> {{ t("response.status") }}: </span>
|
||||
<div class="flex-1 flex whitespace-nowrap max-w-xs">
|
||||
<SmartEnvInput
|
||||
v-model="status"
|
||||
:auto-complete-source="getStatusCodeOptions"
|
||||
class="flex-1 border border-divider"
|
||||
@update:model-value="
|
||||
(statusCode: string) => {
|
||||
setResponseStatusCode(statusCode)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import {
|
||||
getFullStatusCodePhrase,
|
||||
getStatusCodePhrase,
|
||||
getStatusAndCode,
|
||||
isValidStatusCode,
|
||||
} from "~/helpers/utils/statusCodes"
|
||||
import { HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTRequestResponse
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:response", val: HoppRESTRequestResponse): void
|
||||
}>()
|
||||
|
||||
const responseCtx = useVModel(props, "response", emit)
|
||||
|
||||
const status = ref(
|
||||
getStatusCodePhrase(responseCtx.value.code, responseCtx.value.status)
|
||||
)
|
||||
|
||||
const getStatusCodeOptions = computed(() => {
|
||||
return getFullStatusCodePhrase()
|
||||
})
|
||||
|
||||
const setResponseStatusCode = (statusCode: string) => {
|
||||
if (!isValidStatusCode(statusCode)) {
|
||||
responseCtx.value.status = statusCode
|
||||
responseCtx.value.code = undefined
|
||||
return
|
||||
}
|
||||
responseCtx.value.code = getStatusAndCode(statusCode).code
|
||||
responseCtx.value.status = getStatusAndCode(statusCode).status
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-20 flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||
>
|
||||
<div
|
||||
class="min-w-[12rem] flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||
>
|
||||
<div class="relative flex">
|
||||
<label for="method">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => methodTippyActions.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<input
|
||||
id="method"
|
||||
class="flex w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||
:value="tab.document.response.originalRequest.method"
|
||||
:readonly="!isCustomMethod"
|
||||
:placeholder="`${t('request.method')}`"
|
||||
@input="onSelectMethod($event)"
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="methodTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(method, index) in methods"
|
||||
:key="`method-${index}`"
|
||||
:label="method"
|
||||
:style="{
|
||||
color: getMethodLabelColor(method),
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
updateMethod(method)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-model="tab.document.response.originalRequest.endpoint"
|
||||
:placeholder="`${t('request.url_placeholder')}`"
|
||||
:auto-complete-env="true"
|
||||
:inspection-results="tabResults"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex sm:mt-0 items-stretch space-x-2">
|
||||
<HoppButtonPrimary
|
||||
id="send"
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
title="Try"
|
||||
label="Try"
|
||||
class="min-w-[5rem] flex-1"
|
||||
@click="tryExampleResponse"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
label="Save"
|
||||
filled
|
||||
:icon="IconSave"
|
||||
class="flex-1 rounded"
|
||||
@click="saveExample()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppSavedExampleDocument } from "~/helpers/rest/document"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import { editRESTRequest, restCollections$ } from "~/newstore/collections"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { getRequestsByPath } from "~/helpers/collection/request"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { getSingleRequest } from "~/helpers/teams/TeamRequest"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const methods = [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"CONNECT",
|
||||
"TRACE",
|
||||
"CUSTOM",
|
||||
]
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppSavedExampleDocument> }>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
const newMethod = computed(() => {
|
||||
return tab.value.document.response.originalRequest.method
|
||||
})
|
||||
|
||||
const tryExampleResponse = () => {
|
||||
const {
|
||||
endpoint,
|
||||
method,
|
||||
auth,
|
||||
body,
|
||||
headers,
|
||||
name,
|
||||
params,
|
||||
requestVariables,
|
||||
} = tab.value.document.response.originalRequest
|
||||
|
||||
tabs.createNewTab({
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
request: {
|
||||
...getDefaultRESTRequest(),
|
||||
endpoint,
|
||||
method,
|
||||
auth,
|
||||
body,
|
||||
headers,
|
||||
name,
|
||||
params,
|
||||
requestVariables,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [], "deep")
|
||||
|
||||
const saveExample = async () => {
|
||||
const saveCtx = tab.value.document.saveContext
|
||||
|
||||
if (!saveCtx) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = cloneDeep(tab.value.document.response)
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
const request = cloneDeep(
|
||||
getRequestsByPath(myCollections.value, saveCtx.folderPath)[
|
||||
saveCtx.requestIndex
|
||||
] as HoppRESTRequest
|
||||
)
|
||||
|
||||
if (!request) return
|
||||
|
||||
const responseName = response.name
|
||||
|
||||
request.responses[responseName] = response
|
||||
|
||||
try {
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, request)
|
||||
tab.value.document.isDirty = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
toast.success(`${t("response.saved")}`)
|
||||
} else if (saveCtx.originLocation === "team-collection") {
|
||||
const request = await getSingleRequest(saveCtx.requestID)
|
||||
|
||||
if (E.isRight(request)) {
|
||||
const req = request.right.request
|
||||
|
||||
if (req) {
|
||||
const parsedRequest: HoppRESTRequest = JSON.parse(req.request)
|
||||
|
||||
if (!parsedRequest) return
|
||||
|
||||
const responseName = response.name
|
||||
|
||||
parsedRequest.responses[responseName] = response
|
||||
|
||||
try {
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
title: parsedRequest.name,
|
||||
request: JSON.stringify(parsedRequest),
|
||||
},
|
||||
})().then((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
} else {
|
||||
tab.value.document.isDirty = false
|
||||
toast.success(`${t("response.saved")}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const methodTippyActions = ref<any | null>(null)
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const updateMethod = (method: string) => {
|
||||
tab.value.document.response.originalRequest.method = method
|
||||
}
|
||||
|
||||
const onSelectMethod = (e: Event | any) => {
|
||||
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
|
||||
updateMethod(e.target.value)
|
||||
}
|
||||
|
||||
const isCustomMethod = computed(() => {
|
||||
return (
|
||||
tab.value.document.response.originalRequest.method === "CUSTOM" ||
|
||||
!methods.includes(newMethod.value)
|
||||
)
|
||||
})
|
||||
|
||||
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
|
||||
|
||||
defineActionHandler("request-response.save", saveExample)
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="rest-primary">
|
||||
<template #primary>
|
||||
<HttpExampleResponseRequest v-model="tab" />
|
||||
<HttpRequestOptions
|
||||
v-model="tab.document.response.originalRequest"
|
||||
v-model:option-tab="optionTabPreference"
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpExampleResponse v-model:document="tab.document" :is-embed="false" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppSavedExampleDocument } from "~/helpers/rest/document"
|
||||
import { RESTOptionTabs } from "../RequestOptions.vue"
|
||||
import { isEqual } from "lodash-es"
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppSavedExampleDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTab<HoppSavedExampleDocument>): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
const optionTabPreference = ref<RESTOptionTabs>("params")
|
||||
|
||||
// TODO: Come up with a better dirty check
|
||||
let oldResponse = cloneDeep(tab.value.document.response)
|
||||
watch(
|
||||
() => tab.value.document.response,
|
||||
(updatedValue) => {
|
||||
if (!tab.value.document.isDirty && !isEqual(oldResponse, updatedValue)) {
|
||||
tab.value.document.isDirty = true
|
||||
}
|
||||
|
||||
oldResponse = cloneDeep(updatedValue)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
Reference in New Issue
Block a user