feat: save api responses (#4382)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Nivedin
2024-09-30 19:06:53 +05:30
committed by GitHub
parent fdf5bf34ed
commit 58857be650
84 changed files with 3080 additions and 321 deletions

View File

@@ -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)

View File

@@ -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
}
})

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>