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