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

@@ -7,9 +7,9 @@
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
v-if="request && 'method' in request"
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
:style="{ color: getMethodLabelColorClassOf(request) }"
:style="{ color: getMethodLabelColorClassOf(request.method) }"
>
{{ request.method.toUpperCase() }}
</span>

View File

@@ -92,6 +92,9 @@ watch(
() => props.show,
(show) => {
if (show) {
if (tabs.currentActiveTab.value.document.type === "example-response")
return
editingName.value = tabs.currentActiveTab.value.document.request.name
}
}

View File

@@ -62,7 +62,7 @@
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
:title="t('request.add')"
class="hidden group-hover:inline-flex"
@click="emit('add-request')"
/>

View File

@@ -0,0 +1,95 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('modal.edit_response')"
@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="editResponse"
/>
</div>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('action.save')"
:loading="loadingState"
outline
@click="editResponse"
/>
<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 { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
const toast = useToast()
const t = useI18n()
const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
modelValue?: string
requestContext: HoppRESTRequest | null
}>(),
{
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 editResponse = () => {
if (editingName.value.trim() === "") {
toast.error(t("response.invalid_name"))
return
}
const responses = props.requestContext?.responses || []
//check if any other response has the same name
const hasSameNameResponse = Object.keys(responses).some(
(key) => key === editingName.value
)
if (hasSameNameResponse) {
toast.error(t("request.response_name_exists"))
return
}
emit("submit", editingName.value)
}
const hideModal = () => {
editingName.value = ""
emit("hide-modal")
}
</script>

View File

@@ -0,0 +1,247 @@
<template>
<div
class="flex items-center w-full flex-1 justify-between cursor-pointer group"
@contextmenu.prevent="options?.tippy?.show()"
>
<div
class="pointer-events-auto flex min-w-0 flex-1 space-x-2 cursor-pointer items-center justify-center"
@click="selectResponse()"
>
<span
class="pointer-events-none flex w-10 px-2 items-center justify-start truncate relative"
>
<span
class="truncate text-tiny font-semibold relative"
:class="statusCategory.className"
>
{{ response.code ?? response.status }}
</span>
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate font-semibold group-hover:text-secondaryDark">
{{ responseName }}
</span>
<span
v-if="isActiveExample"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
</span>
</div>
<div v-if="!hasNoTeamAccess" class="flex">
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
class="!py-1"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
emit('edit-response', {
responseName: responseName,
responseID: saveContext.exampleID,
})
hide()
}
"
/>
<HoppSmartItem
ref="duplicate"
:icon="IconCopy"
:label="t('action.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-response', {
responseName: responseName,
responseID: saveContext.exampleID,
})
hide()
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-response', {
responseName: responseName,
responseID: saveContext.exampleID,
})
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import { ref, PropType, computed } from "vue"
import { useI18n } from "@composables/i18n"
import { TippyComponent } from "vue-tippy"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppRESTRequestResponse } from "@hoppscotch/data"
import { HoppRESTSaveContext } from "~/helpers/rest/document"
import findStatusGroup from "@helpers/findStatusGroup"
const t = useI18n()
type CollectionType = "my-collections" | "team-collections"
type SaveContext = {
requestID: string | number
exampleID: string
parentID: string
collectionsType: CollectionType
saveRequest: boolean
}
const props = defineProps({
response: {
type: Object as PropType<HoppRESTRequestResponse>,
default: null,
required: true,
},
responseName: {
type: String,
default: "",
required: true,
},
hasNoTeamAccess: {
type: Boolean,
default: false,
required: false,
},
saveContext: {
type: Object as PropType<SaveContext>,
default: null,
required: false,
},
})
type ResponsePayload = {
responseName: string
responseID: string
}
const statusCategory = computed(() => {
return findStatusGroup(props.response.code ?? 0)
})
const emit = defineEmits<{
(event: "edit-response", payload: ResponsePayload): void
(event: "duplicate-response", payload: ResponsePayload): void
(event: "remove-response", payload: ResponsePayload): void
(event: "select-response", payload: ResponsePayload): void
}>()
const tabs = useService(RESTTabService)
const pathToIndex = (path: string) => {
const pathArr = path.split("/")
return pathArr[pathArr.length - 1]
}
const getSaveContext = (): HoppRESTSaveContext => {
if (props.saveContext.collectionsType === "my-collections") {
return {
originLocation: "user-collection",
folderPath: props.saveContext.parentID,
requestIndex: Number(pathToIndex(props.saveContext.requestID.toString())),
exampleID: props.saveContext.exampleID,
}
}
return {
originLocation: "team-collection",
requestID: props.saveContext.requestID.toString() as string,
collectionID: props.saveContext.parentID,
exampleID: props.saveContext.exampleID,
}
}
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveExample = computed(() => {
const saveCtx = getSaveContext()
if (!saveCtx) return
if (saveCtx.originLocation === "team-collection") {
return (
active.value?.originLocation === "team-collection" &&
active.value?.requestID === saveCtx.requestID &&
active.value?.exampleID === saveCtx.exampleID
)
}
return (
active.value?.originLocation === "user-collection" &&
active.value?.folderPath === saveCtx.folderPath &&
active.value?.requestIndex === saveCtx.requestIndex &&
active.value?.exampleID === saveCtx.exampleID
)
})
const selectResponse = () => {
emit("select-response", {
responseName: props.responseName,
responseID: props.saveContext.exampleID,
})
}
const tippyActions = ref<HTMLButtonElement | null>(null)
const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null)
</script>

View File

@@ -105,7 +105,8 @@
})
"
@dragging="
(isDraging) => highlightChildren(isDraging ? node.id : null)
(isDraging: boolean) =>
highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
@@ -187,7 +188,8 @@
})
"
@dragging="
(isDraging) => highlightChildren(isDraging ? node.id : null)
(isDraging: boolean) =>
highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
@@ -228,6 +230,15 @@
request: node.data.data.data,
})
"
@edit-response="
emit('edit-response', {
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
request: node.data.data.data,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
@@ -235,6 +246,15 @@
request: node.data.data.data,
})
"
@duplicate-response="
emit('duplicate-response', {
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
request: node.data.data.data,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
@@ -242,6 +262,15 @@
requestIndex: pathToIndex(node.id),
})
"
@remove-response="
emit('remove-response', {
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
request: node.data.data.data,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@select-request="
node.data.type === 'requests' &&
selectRequest({
@@ -250,6 +279,15 @@
requestIndex: pathToIndex(node.id),
})
"
@select-response="
emit('select-response', {
responseName: $event.responseName,
responseID: $event.responseID,
request: node.data.data.data,
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
@@ -431,6 +469,14 @@ const props = defineProps({
},
})
type ResponsePayload = {
folderPath: string
requestIndex: string
request: HoppRESTRequest
responseName: string
responseID: string
}
const emit = defineEmits<{
(event: "display-modal-add"): void
(
@@ -483,6 +529,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "edit-response", payload: ResponsePayload): void
(
event: "duplicate-request",
payload: {
@@ -490,6 +537,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "duplicate-response", payload: ResponsePayload): void
(event: "export-data", payload: HoppCollection): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
@@ -500,6 +548,7 @@ const emit = defineEmits<{
requestIndex: string
}
): void
(event: "remove-response", payload: ResponsePayload): void
(
event: "select-request",
payload: {
@@ -550,6 +599,7 @@ const emit = defineEmits<{
): void
(event: "select", payload: Picked | null): void
(event: "display-modal-import-export"): void
(event: "select-response", payload: ResponsePayload): void
}>()
const refFilterCollection = toRef(props, "filteredCollections")
@@ -600,7 +650,8 @@ const isActiveRequest = (folderPath: string, requestIndex: number) => {
(active) =>
active.originLocation === "user-collection" &&
active.folderPath === folderPath &&
active.requestIndex === requestIndex
active.requestIndex === requestIndex &&
active.exampleID === undefined
),
O.isSome
)

View File

@@ -13,7 +13,7 @@
@dragend="resetDragState"
></div>
<div
class="group flex items-stretch"
class="group flex items-center"
:draggable="!hasNoTeamAccess"
@drop="handelDrop"
@dragstart="dragStart"
@@ -22,13 +22,21 @@
@dragend="resetDragState"
@contextmenu.prevent="options?.tippy?.show()"
>
<div class="w-5 p-1 flex items-center justify-center">
<component
:is="isResponseVisible ? IconArrowDown : IconArrowRight"
v-if="request.responses && Object.keys(request.responses).length > 0"
class="svg-icons cursor-pointer hover:bg-primaryDark transition"
@click="toggleRequestResponse()"
/>
</div>
<div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@click="selectRequest()"
>
<span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:style="{ color: getMethodLabelColorClassOf(request) }"
class="pointer-events-none flex w-12 items-center justify-start truncate px-2"
:style="{ color: getMethodLabelColorClassOf(request.method) }"
>
<component
:is="IconCheckCircle"
@@ -163,6 +171,33 @@
@dragleave="resetDragState"
@dragend="resetDragState"
></div>
<div v-if="isResponseVisible" class="flex">
<div
class="ml-[.6rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark"
></div>
<div class="flex flex-col w-full pl-3">
<CollectionsExampleResponse
v-for="[index, [key, value]] of Object.entries(
Object.entries(request.responses)
)"
:key="key"
:response-name="key"
:response="value"
:save-context="{
requestID: requestID,
exampleID: index,
parentID: parentID,
collectionsType: collectionsType,
saveRequest: saveRequest,
}"
@edit-response="emit('edit-response', $event)"
@remove-response="emit('remove-response', $event)"
@duplicate-response="emit('duplicate-response', $event)"
@select-response="emit('select-response', $event)"
/>
</div>
</div>
</div>
</template>
@@ -174,6 +209,8 @@ import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import IconArrowRight from "~icons/lucide/chevron-right"
import IconArrowDown from "~icons/lucide/chevron-down"
import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
@@ -247,8 +284,14 @@ const props = defineProps({
},
})
type ResponsePayload = {
responseName: string
responseID: string
}
const emit = defineEmits<{
(event: "edit-request"): void
(event: "edit-response", payload: ResponsePayload): void
(event: "duplicate-request"): void
(event: "remove-request"): void
(event: "select-request"): void
@@ -256,6 +299,10 @@ const emit = defineEmits<{
(event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void
(event: "duplicate-response", payload: ResponsePayload): void
(event: "remove-response", payload: ResponsePayload): void
(event: "select-response", payload: ResponsePayload): void
(event: "toggle-children"): void
}>()
const tippyActions = ref<HTMLButtonElement | null>(null)
@@ -269,6 +316,8 @@ const dragging = ref(false)
const ordering = ref(false)
const orderingLastItem = ref(false)
const isResponseVisible = ref(false)
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
type: "collection",
id: "",
@@ -288,6 +337,11 @@ const selectRequest = () => {
emit("select-request")
}
const toggleRequestResponse = () => {
emit("toggle-children")
isResponseVisible.value = !isResponseVisible.value
}
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
emit("drag-request", dataTransfer)

View File

@@ -149,7 +149,10 @@ const gqlRequestName = computedWithControl(
const restRequestName = computedWithControl(
() => RESTTabs.currentActiveTab.value,
() => RESTTabs.currentActiveTab.value.document.request.name
() =>
RESTTabs.currentActiveTab.value.document.type === "request"
? RESTTabs.currentActiveTab.value.document.request.name
: ""
)
const reqName = computed(() => {
@@ -166,7 +169,10 @@ const requestContext = computed(() => {
return props.request
}
if (props.mode === "rest") {
if (
props.mode === "rest" &&
RESTTabs.currentActiveTab.value.document.type === "request"
) {
return RESTTabs.currentActiveTab.value.document.request
}
@@ -184,7 +190,10 @@ const {
watch(
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
() => {
if (props.mode === "rest") {
if (
props.mode === "rest" &&
RESTTabs.currentActiveTab.value.document.type === "request"
) {
requestName.value =
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
} else {
@@ -249,9 +258,15 @@ const saveRequestAs = async () => {
const requestUpdated =
props.mode === "rest"
? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
? cloneDeep(
RESTTabs.currentActiveTab.value.document.type === "request"
? RESTTabs.currentActiveTab.value.document.request
: null
)
: cloneDeep(GQLTabs.currentActiveTab.value.document.request)
if (!requestUpdated) return
requestUpdated.name = requestName.value
if (picked.value.pickedType === "my-collection") {
@@ -263,13 +278,17 @@ const saveRequestAs = async () => {
requestUpdated
)
if (RESTTabs.currentActiveTab.value.document.type !== "request") return
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
exampleID: undefined,
},
}
@@ -303,6 +322,7 @@ const saveRequestAs = async () => {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
@@ -341,6 +361,7 @@ const saveRequestAs = async () => {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
@@ -541,6 +562,7 @@ const updateTeamCollectionOrFolder = (
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
type: "request",
saveContext: {
originLocation: "team-collection",
requestID: createRequestInCollection.id,

View File

@@ -123,7 +123,7 @@
})
"
@dragging="
(isDraging) =>
(isDraging: boolean) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
@@ -220,7 +220,7 @@
})
"
@dragging="
(isDraging) =>
(isDraging: boolean) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
@@ -267,6 +267,15 @@
request: node.data.data.data.request,
})
"
@edit-response="
emit('edit-response', {
folderPath: node.data.data.parentIndex,
requestIndex: node.data.data.data.id,
request: node.data.data.data.request,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
@@ -274,6 +283,15 @@
request: node.data.data.data.request,
})
"
@duplicate-response="
emit('duplicate-response', {
folderPath: node.data.data.parentIndex,
requestIndex: node.data.data.data.id,
request: node.data.data.data.request,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
@@ -281,6 +299,15 @@
requestIndex: node.data.data.data.id,
})
"
@remove-response="
emit('remove-response', {
folderPath: node.data.data.parentIndex,
requestIndex: node.data.data.data.id,
request: node.data.data.data.request,
responseName: $event.responseName,
responseID: $event.responseID,
})
"
@select-request="
node.data.type === 'requests' &&
selectRequest({
@@ -289,6 +316,15 @@
folderPath: getPath(node.id),
})
"
@select-response="
emit('select-response', {
responseName: $event.responseName,
responseID: $event.responseID,
request: node.data.data.data.request,
folderPath: getPath(node.id),
requestIndex: node.data.data.data.id,
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
@@ -488,6 +524,14 @@ const props = defineProps({
const isShowingSearchResults = computed(() => props.filterText.length > 0)
type ResponsePayload = {
folderPath: string
requestIndex: string
request: HoppRESTRequest
responseName: string
responseID: string
}
const emit = defineEmits<{
(
event: "add-request",
@@ -537,6 +581,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "edit-response", payload: ResponsePayload): void
(
event: "duplicate-request",
payload: {
@@ -544,6 +589,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "duplicate-response", payload: ResponsePayload): void
(event: "export-data", payload: TeamCollection): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
@@ -554,6 +600,7 @@ const emit = defineEmits<{
requestIndex: string
}
): void
(event: "remove-response", payload: ResponsePayload): void
(
event: "select-request",
payload: {
@@ -563,6 +610,7 @@ const emit = defineEmits<{
folderPath: string
}
): void
(event: "select-response", payload: ResponsePayload): void
(
event: "share-request",
payload: {
@@ -682,7 +730,8 @@ const isActiveRequest = (requestID: string) => {
O.filter(
(active) =>
active.originLocation === "team-collection" &&
active.requestID === requestID
active.requestID === requestID &&
active.exampleID === undefined
),
O.isSome
)
@@ -704,7 +753,7 @@ const selectRequest = (data: {
request: request,
requestIndex: requestIndex,
isActive: isActiveRequest(requestIndex),
folderPath: data.folderPath,
folderPath: data.folderPath ?? "",
})
}
}

View File

@@ -35,25 +35,29 @@
:picked="picked"
@add-folder="addFolder"
@add-request="addRequest"
@edit-request="editRequest"
@edit-collection="editCollection"
@edit-folder="editFolder"
@edit-response="editResponse"
@drop-request="dropRequest"
@drop-collection="dropCollection"
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
@duplicate-collection="duplicateCollection"
@duplicate-request="duplicateRequest"
@duplicate-response="duplicateResponse"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@remove-request="removeRequest"
@remove-response="removeResponse"
@share-request="shareRequest"
@drop-collection="dropCollection"
@select="selectPicked"
@select-response="selectResponse"
@select-request="selectRequest"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
@edit-request="editRequest"
@duplicate-request="duplicateRequest"
@remove-request="removeRequest"
@select-request="selectRequest"
@select="selectPicked"
@drop-request="dropRequest"
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
/>
<CollectionsTeamCollections
v-else
@@ -76,28 +80,32 @@
:request-move-loading="requestMoveLoading"
@add-request="addRequest"
@add-folder="addFolder"
@edit-collection="editCollection"
@edit-folder="editFolder"
@collection-click="handleCollectionClick"
@duplicate-collection="duplicateCollection"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@share-request="shareRequest"
@edit-request="editRequest"
@duplicate-request="duplicateRequest"
@remove-request="removeRequest"
@select-request="selectRequest"
@select="selectPicked"
@duplicate-response="duplicateResponse"
@drop-request="dropRequest"
@drop-collection="dropCollection"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
@expand-team-collection="expandTeamCollection"
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
@collection-click="handleCollectionClick"
@edit-collection="editCollection"
@edit-folder="editFolder"
@edit-request="editRequest"
@edit-response="editResponse"
@edit-properties="editProperties"
@export-data="exportData"
@expand-team-collection="expandTeamCollection"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@remove-request="removeRequest"
@remove-response="removeResponse"
@run-collection="runCollectionHandler"
@share-request="shareRequest"
@select-request="selectRequest"
@select-response="selectResponse"
@select="selectPicked"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
/>
<div
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
@@ -148,6 +156,14 @@
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsEditResponse
v-model="editingResponseName"
:show="showModalEditResponse"
:request-context="editingRequest"
:loading-state="modalLoadingState"
@submit="updateEditingResponse"
@hide-modal="displayModalEditResponse(false)"
/>
<HoppSmartConfirmModal
:show="showConfirmModal"
:title="confirmModalTitle"
@@ -276,6 +292,7 @@ import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { Collection as NodeCollection } from "./MyCollections.vue"
import { EditingProperties } from "./Properties.vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
const t = useI18n()
const toast = useToast()
@@ -322,8 +339,11 @@ const editingFolderName = ref<string | null>(null)
const editingFolderPath = ref<string | null>(null)
const editingRequest = ref<HoppRESTRequest | null>(null)
const editingRequestName = ref("")
const editingResponseName = ref("")
const editingResponseOldName = ref("")
const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null)
const editingResponseID = ref<string | null>(null)
const editingProperties = ref<EditingProperties>({
collection: null,
@@ -665,6 +685,7 @@ const showModalAddFolder = ref(false)
const showModalEditCollection = ref(false)
const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false)
const showModalEditResponse = ref(false)
const showModalImportExport = ref(false)
const showModalEditProperties = ref(false)
const showConfirmModal = ref(false)
@@ -710,6 +731,12 @@ const displayModalEditRequest = (show: boolean) => {
if (!show) resetSelectedData()
}
const displayModalEditResponse = (show: boolean) => {
showModalEditResponse.value = show
if (!show) resetSelectedData()
}
const displayModalImportExport = (show: boolean) => {
showModalImportExport.value = show
@@ -796,12 +823,21 @@ const addRequest = (payload: {
}
const requestContext = computed(() => {
return tabs.currentActiveTab.value.document.request
return tabs.currentActiveTab.value.document.type === "request"
? tabs.currentActiveTab.value.document.request
: null
})
const onAddRequest = (requestName: string) => {
const request =
tabs.currentActiveTab.value.document.type === "request"
? tabs.currentActiveTab.value.document.request
: getDefaultRESTRequest()
if (!request) return
const newRequest = {
...cloneDeep(tabs.currentActiveTab.value.document.request),
...cloneDeep(request),
name: requestName,
}
@@ -815,6 +851,7 @@ const onAddRequest = (requestName: string) => {
tabs.createNewTab({
request: newRequest,
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: path,
@@ -869,6 +906,7 @@ const onAddRequest = (requestName: string) => {
tabs.createNewTab({
request: newRequest,
isDirty: false,
type: "request",
saveContext: {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
@@ -1127,7 +1165,10 @@ const updateEditingRequest = (newName: string) => {
editRESTRequest(folderPath, requestIndex, requestUpdated)
if (possibleActiveTab) {
if (
possibleActiveTab &&
possibleActiveTab.value.document.type === "request"
) {
possibleActiveTab.value.document.request.name = requestUpdated.name
nextTick(() => {
possibleActiveTab.value.document.isDirty = false
@@ -1168,7 +1209,7 @@ const updateEditingRequest = (newName: string) => {
requestID,
})
if (possibleTab) {
if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.request.name = requestName
nextTick(() => {
possibleTab.value.document.isDirty = false
@@ -1177,6 +1218,171 @@ const updateEditingRequest = (newName: string) => {
}
}
type ResponseConfigPayload = {
folderPath: string | undefined
requestIndex: string
request: HoppRESTRequest
responseName: string
responseID: string
}
const editResponse = (payload: ResponseConfigPayload) => {
const { folderPath, requestIndex, request, responseID, responseName } =
payload
editingRequest.value = request
editingRequestName.value = request.name ?? ""
editingResponseID.value = responseID
editingResponseName.value = responseName
//need to store the old name for updating the response key
editingResponseOldName.value = responseName
if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath
editingRequestIndex.value = parseInt(requestIndex)
} else {
editingRequestID.value = requestIndex
}
displayModalEditResponse(true)
}
const updateEditingResponse = (newName: string) => {
const request = cloneDeep(editingRequest.value)
if (!request) return
const responseOldName = editingResponseOldName.value
if (!responseOldName) return
if (responseOldName !== newName) {
// Convert object to entries array (preserving order)
const entries = Object.entries(request.responses)
// Replace the old key with the new key in the array
const updatedEntries = entries.map(([key, value]) =>
key === responseOldName
? [newName, { ...value, name: newName }]
: [key, value]
)
// Convert the array back into an object
request.responses = Object.fromEntries(updatedEntries)
}
if (collectionsType.value.type === "my-collections") {
const folderPath = editingFolderPath.value
const requestIndex = editingRequestIndex.value
if (folderPath === null || requestIndex === null) return
const possibleExampleActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex,
folderPath,
exampleID: editingResponseID.value ?? undefined,
})
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex,
folderPath,
})
editRESTRequest(folderPath, requestIndex, request)
if (
possibleExampleActiveTab &&
possibleExampleActiveTab.value.document.type === "example-response"
) {
possibleExampleActiveTab.value.document.response.name = newName
nextTick(() => {
possibleExampleActiveTab.value.document.isDirty = false
possibleExampleActiveTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
exampleID: editingResponseID.value!,
}
})
}
// update the request tab responses if it's open
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
request.responses
}
displayModalEditResponse(false)
toast.success(t("response.renamed"))
} else if (hasTeamWriteAccess.value) {
modalLoadingState.value = true
const requestID = editingRequestID.value
if (!requestID) return
const data = {
request: JSON.stringify(request),
title: request.name,
}
pipe(
updateTeamRequest(requestID, data),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
modalLoadingState.value = false
},
() => {
modalLoadingState.value = false
toast.success(t("response.renamed"))
displayModalEditResponse(false)
}
)
)()
const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
exampleID: editingResponseID.value ?? undefined,
})
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
})
if (
possibleActiveResponseTab &&
possibleActiveResponseTab.value.document.type === "example-response"
) {
possibleActiveResponseTab.value.document.response.name = newName
nextTick(() => {
possibleActiveResponseTab.value.document.isDirty = false
possibleActiveResponseTab.value.document.saveContext = {
originLocation: "team-collection",
requestID,
exampleID: editingResponseID.value!,
}
})
}
// update the request tab responses if it's open
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
request.responses
}
}
}
const duplicateRequest = (payload: {
folderPath: string
request: HoppRESTRequest
@@ -1220,6 +1426,93 @@ const duplicateRequest = (payload: {
}
}
const duplicateResponse = (payload: ResponseConfigPayload) => {
const { folderPath, requestIndex, request, responseName } = payload
const response = request.responses[responseName]
if (!response || !folderPath || !requestIndex) return
const newName = `${responseName} - ${t("action.duplicate")}`
// if the new name is already taken, show a toast and return
if (Object.keys(request.responses).includes(newName)) {
toast.error(t("response.duplicate_name_error"))
return
}
const newResponse = {
...cloneDeep(response),
name: newName,
}
const updatedRequest = {
...request,
responses: {
...request.responses,
[newResponse.name]: newResponse,
},
}
if (collectionsType.value.type === "my-collections") {
editRESTRequest(folderPath, parseInt(requestIndex), updatedRequest)
toast.success(t("response.duplicated"))
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex: parseInt(requestIndex),
folderPath,
})
// update the request tab responses if it's open
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
updatedRequest.responses
}
} else if (hasTeamWriteAccess.value) {
duplicateRequestLoading.value = true
if (!collectionsType.value.selectedTeam) return
const data = {
request: JSON.stringify(updatedRequest),
title: request.name,
}
pipe(
updateTeamRequest(requestIndex, data),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
modalLoadingState.value = false
},
() => {
modalLoadingState.value = false
toast.success(t("response.duplicated"))
displayModalEditResponse(false)
}
)
)()
// update the request tab responses if it's open
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
})
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
updatedRequest.responses
}
}
}
const removeCollection = (id: string) => {
if (collectionsType.value.type === "my-collections")
editingCollectionIndex.value = parseInt(id)
@@ -1406,9 +1699,12 @@ const onRemoveRequest = () => {
})
// If there is a tab attached to this request, dissociate its state and mark it dirty
if (possibleTab) {
if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = null
possibleTab.value.document.isDirty = true
// since the request is deleted, we need to remove the saved responses as well
possibleTab.value.document.request.responses = {}
}
const requestToRemove = navigateToFolderWithIndexPath(
@@ -1464,9 +1760,176 @@ const onRemoveRequest = () => {
requestID,
})
if (possibleTab) {
if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = null
possibleTab.value.document.isDirty = true
// since the request is deleted, we need to remove the saved responses as well
possibleTab.value.document.request.responses = {}
}
}
}
const removeResponse = (payload: ResponseConfigPayload) => {
const { folderPath, requestIndex, request, responseID, responseName } =
payload
if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath
editingRequestIndex.value = parseInt(requestIndex)
editingResponseID.value = responseID
editingRequest.value = request
editingResponseName.value = responseName
} else {
editingRequestID.value = requestIndex
editingResponseID.value = payload.responseID
editingRequest.value = request
editingResponseName.value = responseName
}
confirmModalTitle.value = `${t("confirm.remove_response")}`
displayConfirmModal(true)
}
const onRemoveResponse = () => {
const request = cloneDeep(editingRequest.value)
if (!request) return
const responseName = editingResponseName.value
const responseID = editingResponseID.value
delete request.responses[responseName]
const requestUpdated: HoppRESTRequest = {
...request,
}
if (collectionsType.value.type === "my-collections") {
const folderPath = editingFolderPath.value
const requestIndex = editingRequestIndex.value
if (folderPath === null || requestIndex === null) return
editRESTRequest(folderPath, requestIndex, requestUpdated)
const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex,
exampleID: responseID ?? undefined,
})
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex,
folderPath,
})
// If there is a tab attached to this request, close it and set the active tab to the first one
if (
possibleActiveResponseTab &&
possibleActiveResponseTab.value.document.type === "example-response"
) {
const activeTabs = tabs.getActiveTabs()
// if the last tab is the one we are closing, we need to create a new tab
if (
activeTabs.value.length === 1 &&
activeTabs.value[0].id === possibleActiveResponseTab.value.id
) {
tabs.createNewTab({
request: getDefaultRESTRequest(),
isDirty: false,
type: "request",
saveContext: undefined,
})
tabs.closeTab(possibleActiveResponseTab.value.id)
} else {
tabs.closeTab(possibleActiveResponseTab.value.id)
tabs.setActiveTab(activeTabs.value[0].id)
}
}
// update the request tab responses if it's open
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
requestUpdated.responses
}
toast.success(t("state.deleted"))
displayConfirmModal(false)
} else if (hasTeamWriteAccess.value) {
const requestID = editingRequestID.value
if (!requestID) return
modalLoadingState.value = true
const data = {
request: JSON.stringify(requestUpdated),
title: request.name,
}
pipe(
updateTeamRequest(requestID, data),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
modalLoadingState.value = false
},
() => {
modalLoadingState.value = false
toast.success(t("state.deleted"))
displayConfirmModal(false)
}
)
)()
const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
exampleID: responseID ?? undefined,
})
const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
})
// If there is a tab attached to this request, close it and set the active tab to the first one
if (
possibleActiveResponseTab &&
possibleActiveResponseTab.value.document.type === "example-response"
) {
const activeTabs = tabs.getActiveTabs()
// if the last tab is the one we are closing, we need to create a new tab
if (
activeTabs.value.length === 1 &&
activeTabs.value[0].id === possibleActiveResponseTab.value.id
) {
tabs.createNewTab({
request: getDefaultRESTRequest(),
isDirty: false,
type: "request",
saveContext: undefined,
})
tabs.closeTab(possibleActiveResponseTab.value.id)
} else {
tabs.closeTab(possibleActiveResponseTab.value.id)
tabs.setActiveTab(activeTabs.value[0].id)
}
}
// update the request tab responses if it's open
if (
possibleRequestActiveTab &&
possibleRequestActiveTab.value.document.type === "request"
) {
possibleRequestActiveTab.value.document.request.responses =
requestUpdated.responses
}
}
}
@@ -1516,10 +1979,12 @@ const selectRequest = (selectedRequest: {
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
type: "request",
saveContext: {
originLocation: "team-collection",
requestID: requestIndex,
collectionID: folderPath,
exampleID: undefined,
},
inheritedProperties: inheritedProperties,
})
@@ -1541,6 +2006,7 @@ const selectRequest = (selectedRequest: {
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: folderPath!,
@@ -1555,6 +2021,72 @@ const selectRequest = (selectedRequest: {
}
}
const selectResponse = (payload: {
folderPath: string
requestIndex: string
responseName: string
request: HoppRESTRequest
responseID: string
}) => {
const { folderPath, requestIndex, responseName, request, responseID } =
payload
const response = request.responses[responseName]
if (collectionsType.value.type === "my-collections") {
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex: parseInt(requestIndex),
folderPath: folderPath!,
exampleID: responseID,
})
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
} else {
tabs.createNewTab({
response: {
...cloneDeep(response),
name: responseName,
},
isDirty: false,
type: "example-response",
saveContext: {
originLocation: "user-collection",
folderPath: folderPath!,
requestIndex: parseInt(requestIndex),
exampleID: responseID,
},
})
}
} else {
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
exampleID: responseID,
})
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
} else {
tabs.createNewTab({
response: {
...cloneDeep(response),
name: responseName,
},
isDirty: false,
type: "example-response",
saveContext: {
originLocation: "team-collection",
requestID: requestIndex,
collectionID: folderPath,
exampleID: responseID,
},
})
}
}
}
/**
* Used to get the index of the request from the path
* @param path The path of the request
@@ -1593,7 +2125,7 @@ const dropRequest = (payload: {
})
// If there is a tab attached to this request, change save its save context
if (possibleTab) {
if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: destinationCollectionIndex,
@@ -1654,7 +2186,7 @@ const dropRequest = (payload: {
requestID: requestIndex,
})
if (possibleTab) {
if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = {
originLocation: "team-collection",
requestID: requestIndex,
@@ -1872,6 +2404,13 @@ const dropToRoot = ({ dataTransfer }: DragEvent) => {
} else {
moveRESTFolder(collectionIndexDragged, null)
toast.success(`${t("collection.moved")}`)
const rootLength = myCollections.value.length
updateSaveContextForAffectedRequests(
collectionIndexDragged,
`${rootLength - 1}`
)
}
draggingToRoot.value = false
@@ -2352,6 +2891,7 @@ const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()
else if (title === `${t("confirm.remove_folder")}`) onRemoveFolder()
else if (title === `${t("confirm.remove_response")}`) onRemoveResponse()
else {
console.error(
`Confirm modal title ${title} is not handled by the component`

View File

@@ -64,13 +64,13 @@ import { useStreamSubscriber } from "~/composables/stream"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { HoppRequestDocument } from "~/helpers/rest/document"
const toast = useToast()
const t = useI18n()
const props = defineProps<{
modelTab: HoppTab<HoppRESTDocument>
modelTab: HoppTab<HoppRequestDocument>
sharedRequestURL: string
}>()

View File

@@ -27,11 +27,11 @@
import { computed, useModel } from "vue"
import { ref } from "vue"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { HoppRequestDocument } from "~/helpers/rest/document"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
const props = defineProps<{
modelTab: HoppTab<HoppRESTDocument>
modelTab: HoppTab<HoppRequestDocument>
properties: RESTOptionTabs[]
sharedRequestID: string
}>()

View File

@@ -75,6 +75,7 @@ import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { HoppRESTHeaders } from "@hoppscotch/data"
const VALID_GQL_OPERATIONS = [
"query",
@@ -158,7 +159,10 @@ const runQuery = async (
let runHeaders: HoppGQLRequest["headers"] = []
if (inheritedHeaders) {
runHeaders = [...inheritedHeaders, ...clone(request.value.headers)]
runHeaders = [
...inheritedHeaders,
...clone(request.value.headers),
] as HoppRESTHeaders
} else {
runHeaders = clone(request.value.headers)
}
@@ -266,7 +270,7 @@ const changeOptionTab = (e: GQLOptionTabs) => {
}
defineActionHandler("request.send-cancel", runQuery)
defineActionHandler("request.save", saveRequest)
defineActionHandler("request-response.save", saveRequest)
defineActionHandler("request.save-as", () => {
showSaveRequestModal.value = true
})

View File

@@ -300,6 +300,7 @@ const useHistory = (entry: RESTHistoryEntry) => {
tabs.createNewTab({
request: entry.request,
isDirty: false,
type: "request",
})
}

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>

View File

@@ -19,7 +19,10 @@
<LensesHeadersRendererEntry
v-for="(header, index) in headers"
:key="index"
:header="header"
v-model:headerKey="header.key"
v-model:headerValue="header.value"
:is-editable="isEditable"
@delete-header="deleteHeader(index)"
/>
</div>
</template>
@@ -32,23 +35,35 @@ import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import type { HoppRESTResponseHeader } from "~/helpers/types/HoppRESTResponse"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
headers: HoppRESTResponseHeader[]
modelValue: HoppRESTResponseHeader[]
isEditable: boolean
}>()
const emit = defineEmits<{
(e: "update:modelValue"): void
}>()
const headers = useVModel(props, "modelValue", emit)
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyHeaders = () => {
copyToClipboard(JSON.stringify(props.headers))
copyToClipboard(JSON.stringify(props.modelValue))
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const deleteHeader = (index: number) => {
headers.value.splice(index, 1)
}
</script>

View File

@@ -2,25 +2,47 @@
<div
class="group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span
class="flex min-w-0 flex-1 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="select-all truncate rounded-sm">
{{ header.key }}
<span class="flex min-w-0 flex-1 transition group-hover:text-secondaryDark">
<span
v-if="!isEntryEditable"
class="select-all truncate rounded-sm py-2 pl-4"
>
{{ headerKey }}
</span>
<SmartEnvInput
v-else
:model-value="headerKey"
@update:model-value="emit('update:headerKey', $event)"
/>
</span>
<span
class="flex min-w-0 flex-1 justify-between py-2 pl-4 transition group-hover:text-secondaryDark"
class="flex min-w-0 flex-1 justify-between transition group-hover:text-secondaryDark"
>
<span class="select-all truncate rounded-sm">
{{ header.value }}
<span
v-if="!isEntryEditable"
class="select-all truncate rounded-sm py-2 pl-4"
>
{{ headerValue }}
</span>
<SmartEnvInput
v-else
:model-value="headerValue"
@update:model-value="emit('update:headerValue', $event)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyIcon"
class="hidden !py-0 group-hover:inline-flex"
@click="copyHeader(header.value)"
@click="copyHeader(headerValue)"
/>
<HoppButtonSecondary
v-if="isEntryEditable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash"
class="hidden !py-0 group-hover:inline-flex"
@click="deleteHeader(headerKey)"
/>
</span>
</div>
@@ -28,21 +50,36 @@
<script setup lang="ts">
import IconCopy from "~icons/lucide/copy"
import IconTrash from "~icons/lucide/trash"
import IconCheck from "~icons/lucide/check"
import { refAutoReset } from "@vueuse/core"
import type { HoppRESTResponseHeader } from "~/helpers/types/HoppRESTResponse"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { computed } from "vue"
const t = useI18n()
const toast = useToast()
defineProps<{
header: HoppRESTResponseHeader
const props = defineProps<{
headerKey: string
headerValue: string
isEditable: boolean
}>()
const emit = defineEmits<{
(e: "update:headerKey", value: string): void
(e: "update:headerValue", value: string): void
(e: "delete-header", key: string): void
}>()
// we can allow editing only if the header is not content-type
// because editing content-type can break lense
const isEntryEditable = computed(
() => props.isEditable && props.headerKey.toLowerCase() !== "content-type"
)
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
@@ -53,4 +90,8 @@ const copyHeader = (headerValue: string) => {
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const deleteHeader = (headerKey: string) => {
emit("delete-header", headerKey)
}
</script>

View File

@@ -13,7 +13,10 @@
>
<component
:is="lensRendererFor(lens.renderer)"
:response="doc.response"
v-model:response="doc.response"
:is-savable="isSavable"
:is-editable="isEditable"
@save-as-example="$emit('save-as-example')"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -23,9 +26,10 @@
:info="`${maybeHeaders.length}`"
class="flex flex-1 flex-col"
>
<LensesHeadersRenderer :headers="maybeHeaders" />
<LensesHeadersRenderer v-model="maybeHeaders" />
</HoppSmartTab>
<HoppSmartTab
v-if="!isEditable"
id="results"
:label="t('test.results')"
:indicator="showIndicator"
@@ -45,18 +49,24 @@ import {
} from "~/helpers/lenses/lenses"
import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { HoppRequestDocument } from "~/helpers/rest/document"
const props = defineProps<{
document: HoppRESTDocument
document: HoppRequestDocument
isEditable: boolean
}>()
const emit = defineEmits<{
(e: "update:document", document: HoppRESTDocument): void
(e: "update:document", document: HoppRequestDocument): void
(e: "save-as-example"): void
}>()
const doc = useVModel(props, "document", emit)
const isSavable = computed(() => {
return doc.value.response?.type === "success" && doc.value.saveContext
})
const showIndicator = computed(() => {
if (!doc.value.testResults) return false

View File

@@ -2,6 +2,7 @@
<div class="flex flex-1 flex-col">
<div
class="sticky top-lowerSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="{ 'py-2': !responseBodyText }"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("response.body") }}
@@ -33,6 +34,22 @@
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="
isSavable
? `${t(
'action.save_as_example'
)} <kbd>${getSpecialKey()}</kbd><kbd>E</kbd>`
: t('response.please_save_request')
"
:icon="IconSave"
:class="{
'opacity-75 cursor-not-allowed select-none': !isSavable,
}"
@click="isSavable ? saveAsExample() : null"
/>
<HoppButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -68,7 +85,7 @@ import {
useResponseBody,
} from "@composables/lens-actions"
import { useService } from "dioc/vue"
import { reactive, ref } from "vue"
import { reactive, ref, computed } from "vue"
import { useNestedSetting } from "~/composables/settings"
import { defineActionHandler } from "~/helpers/actions"
@@ -79,23 +96,44 @@ import { PersistenceService } from "~/services/persistence"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconWrapText from "~icons/lucide/wrap-text"
import IconSave from "~icons/lucide/save"
import { HoppRESTRequestResponse } from "@hoppscotch/data"
const t = useI18n()
const persistenceService = useService(PersistenceService)
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
response:
| (HoppRESTResponse & { type: "success" | "fail" })
| HoppRESTRequestResponse
isSavable: boolean
isEditable: boolean
}>()
const emit = defineEmits<{
(e: "save-as-example"): void
}>()
const htmlResponse = ref<any | null>(null)
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
const responseName = computed(() => {
if ("type" in props.response) {
if (props.response.type === "success" || props.response.type === "fail") {
return props.response.req.name
}
return "Untitled"
}
return props.response.name
})
const { responseBodyText } = useResponseBody(props.response)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"text/html",
responseBodyText,
t("filename.lens", {
request_name: props.response.req.name,
request_name: responseName.value,
})
)
const defaultPreview =
@@ -116,13 +154,17 @@ const doTogglePreview = () => {
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const saveAsExample = () => {
emit("save-as-example")
}
useCodemirror(
htmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "htmlmixed",
readOnly: true,
readOnly: !props.isEditable,
lineWrapping: WRAP_LINES,
},
linter: null,
@@ -134,6 +176,9 @@ useCodemirror(
defineActionHandler("response.preview.toggle", () => doTogglePreview())
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
defineActionHandler("response.save-as-example", () => {
props.isSavable ? saveAsExample() : null
})
</script>
<style lang="scss" scoped>

View File

@@ -1,17 +1,15 @@
<template>
<div
v-if="response.type === 'success' || response.type === 'fail'"
class="flex flex-1 flex-col"
>
<div v-if="showResponse" class="flex flex-1 flex-col">
<div
class="sticky top-lowerSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="{ 'py-2': !responseBodyText }"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex items-center">
<HoppButtonSecondary
v-if="response.body"
v-if="showResponse"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
@@ -19,7 +17,7 @@
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
/>
<HoppButtonSecondary
v-if="response.body"
v-if="showResponse"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.filter')"
:icon="IconFilter"
@@ -27,7 +25,7 @@
@click.prevent="toggleFilterState"
/>
<HoppButtonSecondary
v-if="response.body"
v-if="showResponse"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
@@ -36,7 +34,23 @@
@click="downloadResponse"
/>
<HoppButtonSecondary
v-if="response.body"
v-if="showResponse && !isEditable"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="
isSavable
? `${t(
'action.save_as_example'
)} <kbd>${getSpecialKey()}</kbd><kbd>E</kbd>`
: t('response.please_save_request')
"
:icon="IconSave"
:class="{
'opacity-75 cursor-not-allowed select-none': !isSavable,
}"
@click="isSavable ? saveAsExample() : null"
/>
<HoppButtonSecondary
v-if="showResponse"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.copy'
@@ -45,7 +59,7 @@
@click="copyResponse"
/>
<tippy
v-if="response.body"
v-if="showResponse"
interactive
trigger="click"
theme="popover"
@@ -109,7 +123,7 @@
<span>{{ filterResponseError.error }}</span>
</div>
<HoppButtonSecondary
v-if="response.body"
v-if="showResponse"
v-tippy="{ theme: 'tooltip' }"
:title="t('app.wiki')"
:icon="IconHelpCircle"
@@ -234,6 +248,7 @@ import IconFilter from "~icons/lucide/filter"
import IconMore from "~icons/lucide/more-horizontal"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconNetwork from "~icons/lucide/network"
import IconSave from "~icons/lucide/save"
import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
@@ -258,14 +273,35 @@ import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppRESTRequestResponse } from "@hoppscotch/data"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse
response: HoppRESTResponse | HoppRESTRequestResponse
isSavable: boolean
isEditable: boolean
}>()
const { responseBodyText } = useResponseBody(props.response)
const emit = defineEmits<{
(e: "save-as-example"): void
(e: "update:response", val: HoppRESTRequestResponse): void
}>()
const showResponse = computed(() => {
if ("type" in props.response) {
return props.response.type === "success" || props.response.type === "fail"
}
return "body" in props.response
})
const isHttpResponse = computed(() => {
return (
"type" in props.response &&
(props.response.type === "success" || props.response.type === "fail")
)
})
const toggleFilter = ref(false)
const filterQueryText = ref("")
@@ -274,18 +310,39 @@ type BodyParseError =
| { type: "JSON_PARSE_FAILED" }
| { type: "JSON_PATH_QUERY_FAILED"; error: Error }
const responseJsonObject = computed(() =>
pipe(
responseBodyText.value,
E.tryCatchK(
LJSON.parse,
(): BodyParseError => ({ type: "JSON_PARSE_FAILED" })
const responseJsonObject = computed(() => {
if (isHttpResponse.value) {
const { responseBodyText } = useResponseBody(
props.response as HoppRESTResponse
)
)
)
return pipe(
responseBodyText.value,
E.tryCatchK(
LJSON.parse,
(): BodyParseError => ({ type: "JSON_PARSE_FAILED" })
)
)
}
return undefined
})
const responseName = computed(() => {
if ("type" in props.response) {
if (props.response.type === "success") {
return props.response.req.name
}
return "Untitled"
}
return props.response.name
})
const { responseBodyText } = useResponseBody(props.response)
const jsonResponseBodyText = computed(() => {
if (filterQueryText.value.length > 0) {
if (filterQueryText.value.length > 0 && responseJsonObject.value) {
return pipe(
responseJsonObject.value,
E.chain((parsedJSON) =>
@@ -307,15 +364,19 @@ const jsonResponseBodyText = computed(() => {
return E.right(responseBodyText.value)
})
const jsonBodyText = computed(() =>
pipe(
const jsonBodyText = computed(() => {
const { responseBodyText } = useResponseBody(
props.response as HoppRESTResponse
)
return pipe(
jsonResponseBodyText.value,
E.getOrElse(() => responseBodyText.value),
O.tryCatchK(LJSON.parse),
O.map((val) => LJSON.stringify(val, undefined, 2)),
O.getOrElse(() => responseBodyText.value)
)
)
})
const ast = computed(() =>
pipe(
@@ -351,12 +412,16 @@ const filterResponseError = computed(() =>
)
)
const saveAsExample = () => {
emit("save-as-example")
}
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
jsonBodyText,
t("filename.lens", {
request_name: props.response.req.name,
request_name: responseName.value,
})
)
@@ -372,12 +437,15 @@ const { cursor } = useCodemirror(
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
readOnly: !props.isEditable,
lineWrapping: WRAP_LINES,
},
linter: null,
completer: null,
environmentHighlights: true,
onChange: (update: string) => {
emit("update:response", { ...props.response, body: update })
},
})
)
@@ -408,6 +476,9 @@ const toggleFilterState = () => {
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
defineActionHandler("response.save-as-example", () => {
props.isSavable ? saveAsExample() : null
})
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,8 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky top-lowerSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
class="sticky top-lowerSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 py-1"
:class="{ 'py-2': !responseBodyText }"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("response.body") }}
@@ -24,6 +25,22 @@
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-if="showResponse && !isEditable"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="
isSavable
? `${t(
'action.save_as_example'
)} <kbd>${getSpecialKey()}</kbd><kbd>E</kbd>`
: t('response.please_save_request')
"
:icon="IconSave"
:class="{
'opacity-75 cursor-not-allowed select-none': !isSavable,
}"
@click="isSavable ? saveAsExample() : null"
/>
<HoppButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -43,6 +60,7 @@
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconSave from "~icons/lucide/save"
import { ref, computed, reactive } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
@@ -62,21 +80,53 @@ import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppRESTRequestResponse } from "@hoppscotch/data"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
response:
| (HoppRESTResponse & { type: "success" | "fail" })
| HoppRESTRequestResponse
isEditable: boolean
isSavable: boolean
}>()
const emit = defineEmits<{
(
e: "update:response",
val:
| (HoppRESTResponse & { type: "success" | "fail" })
| HoppRESTRequestResponse
): void
(e: "save-as-example"): void
}>()
const { responseBodyText } = useResponseBody(props.response)
const isHttpResponse = computed(() => {
return (
"type" in props.response &&
(props.response.type === "success" || props.response.type === "fail")
)
})
const rawResponseBody = computed(() =>
props.response.type === "fail" || props.response.type === "success"
? props.response.body
: new ArrayBuffer(0)
isHttpResponse.value ? props.response.body : new ArrayBuffer(0)
)
const showResponse = computed(() => {
if ("type" in props.response) {
return props.response.type === "success" || props.response.type === "fail"
}
return "body" in props.response
})
const saveAsExample = () => {
emit("save-as-example")
}
const responseType = computed(() =>
pipe(
props.response,
@@ -93,11 +143,22 @@ const responseType = computed(() =>
)
)
const responseName = computed(() => {
if ("type" in props.response) {
if (props.response.type === "success" || props.response.type === "fail") {
return props.response.req.name
}
return "Untitled"
}
return props.response.name
})
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
rawResponseBody,
t("filename.lens", {
request_name: props.response.req.name,
request_name: responseName.value,
})
)
@@ -112,12 +173,15 @@ useCodemirror(
reactive({
extendedEditorConfig: {
mode: "text/plain",
readOnly: true,
readOnly: !props.isEditable,
lineWrapping: WRAP_LINES,
},
linter: null,
completer: null,
environmentHighlights: true,
onChange: (update: string) => {
emit("update:response", { ...props.response, body: update })
},
})
)

View File

@@ -24,6 +24,22 @@
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-if="response.body && !isEditable"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="
isSavable
? `${t(
'action.save_as_example'
)} <kbd>${getSpecialKey()}</kbd><kbd>E</kbd>`
: t('response.please_save_request')
"
:icon="IconSave"
:class="{
'opacity-75 cursor-not-allowed select-none': !isSavable,
}"
@click="isSavable ? saveAsExample() : null"
/>
<HoppButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -43,6 +59,7 @@
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconSave from "~icons/lucide/save"
import { computed, ref, reactive } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
@@ -62,36 +79,67 @@ import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objFieldMatches } from "~/helpers/functional/object"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppRESTRequestResponse } from "@hoppscotch/data"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
response:
| (HoppRESTResponse & { type: "success" | "fail" })
| HoppRESTRequestResponse
isEditable: boolean
isSavable: boolean
}>()
const emit = defineEmits<{
(e: "save-as-example"): void
}>()
const { responseBodyText } = useResponseBody(props.response)
const responseType = computed(() =>
pipe(
props.response,
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
O.chain(
// Try getting content-type
flow(
(res) => res.headers,
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
)
),
O.getOrElse(() => "text/plain")
const isHttpResponse = computed(() => {
return (
"type" in props.response &&
(props.response.type === "success" || props.response.type === "fail")
)
)
})
const responseType = computed(() => {
if (isHttpResponse.value) {
return pipe(
props.response,
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
O.chain(
// Try getting content-type
flow(
(res) => res.headers,
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
)
),
O.getOrElse(() => "text/plain")
)
}
return "text/plain"
})
const responseName = computed(() => {
if ("type" in props.response) {
if (props.response.type === "success") {
return props.response.req.name
}
return "Untitled"
}
return props.response.name
})
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
responseBodyText,
t("filename.lens", {
request_name: props.response.req.name,
request_name: responseName.value,
})
)
@@ -100,13 +148,17 @@ const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const xmlResponse = ref<any | null>(null)
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
const saveAsExample = () => {
emit("save-as-example")
}
useCodemirror(
xmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "application/xml",
readOnly: true,
readOnly: !props.isEditable,
lineWrapping: WRAP_LINES,
},
linter: null,
@@ -117,4 +169,7 @@ useCodemirror(
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
defineActionHandler("response.save-as-example", () => {
props.isSavable ? saveAsExample() : null
})
</script>

View File

@@ -142,7 +142,7 @@ const parseRequest = computed(() =>
)
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(parseRequest.value)
getMethodLabelColorClassOf(parseRequest.value.method)
)
const customizeSharedRequest = () => {

View File

@@ -507,6 +507,7 @@ const openRequestInNewTab = (request: HoppRESTRequest) => {
restTab.createNewTab({
isDirty: false,
request,
type: "request",
})
}

View File

@@ -376,17 +376,23 @@ const envVars = computed(() => {
}
})
}
const requestVariables =
tabs.currentActiveTab.value.document.type === "example-response"
? tabs.currentActiveTab.value.document.response.originalRequest
.requestVariables
: tabs.currentActiveTab.value.document.request.requestVariables
return [
...tabs.currentActiveTab.value.document.request.requestVariables.map(
({ active, key, value }) =>
active
? {
key,
value,
sourceEnv: "RequestVariable",
secret: false,
}
: ({} as AggregateEnvironment)
...requestVariables.map(({ active, key, value }) =>
active
? {
key,
value,
sourceEnv: "RequestVariable",
secret: false,
}
: ({} as AggregateEnvironment)
),
...aggregateEnvs.value,
]