feat: rest revamp (#2918)

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Anwarul Islam
2023-03-31 01:15:42 +06:00
committed by GitHub
parent dbb45e7253
commit defece95fc
63 changed files with 2262 additions and 1924 deletions

View File

@@ -32,7 +32,7 @@
:active="authName === 'None'"
@click="
() => {
authType = 'none'
auth.authType = 'none'
hide()
}
"
@@ -43,7 +43,7 @@
:active="authName === 'Basic Auth'"
@click="
() => {
authType = 'basic'
auth.authType = 'basic'
hide()
}
"
@@ -54,7 +54,7 @@
:active="authName === 'Bearer'"
@click="
() => {
authType = 'bearer'
auth.authType = 'bearer'
hide()
}
"
@@ -65,7 +65,7 @@
:active="authName === 'OAuth 2.0'"
@click="
() => {
authType = 'oauth-2'
auth.authType = 'oauth-2'
hide()
}
"
@@ -76,7 +76,7 @@
:active="authName === 'API key'"
@click="
() => {
authType = 'api-key'
auth.authType = 'api-key'
hide()
}
"
@@ -114,7 +114,7 @@
</div>
</div>
<div
v-if="authType === 'none'"
v-if="auth.authType === 'none'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
@@ -136,91 +136,22 @@
</div>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'">
<div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" />
</div>
<div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicUsername"
:placeholder="t('authorization.username')"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicPassword"
:placeholder="t('authorization.password')"
/>
<SmartEnvInput v-model="auth.token" placeholder="Token" />
</div>
</div>
<div v-if="authType === 'bearer'">
<div v-if="auth.authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
<SmartEnvInput v-model="auth.token" placeholder="Token" />
</div>
<HttpOAuth2Authorization v-model="auth" />
</div>
<div v-if="authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
</div>
<HttpOAuth2Authorization />
</div>
<div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiKey" placeholder="Key" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiValue" placeholder="Value" />
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => authTippyActions.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
:label="addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="authTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
addTo = 'Headers'
hide()
}
"
/>
<HoppSmartItem
:icon="
addTo === 'Query params' ? IconCircleDot : IconCircle
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
</div>
</div>
<div
@@ -248,49 +179,40 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref, Ref } from "vue"
import {
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
} from "@hoppscotch/data"
import { computed, ref } from "vue"
import { HoppRESTAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const colorMode = useColorMode()
const auth = useStream(
restAuth$,
{ authType: "none", authActive: true },
setRESTAuth
)
const props = defineProps<{
modelValue: HoppRESTAuth
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuth): void
}>()
const auth = useVModel(props, "modelValue", emit)
const AUTH_KEY_NAME = {
basic: "Basic Auth",
bearer: "Bearer",
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
} as const
const authType = pluckRef(auth, "authType")
const authName = computed(() => {
if (authType.value === "basic") return "Basic Auth"
else if (authType.value === "bearer") return "Bearer"
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
)
const authActive = pluckRef(auth, "authActive")
const basicUsername = pluckRef(auth as Ref<HoppRESTAuthBasic>, "username")
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const clearContent = () => {
auth.value = {
@@ -301,5 +223,4 @@ const clearContent = () => {
// Template refs
const tippyActions = ref<any | null>(null)
const authTippyActions = ref<any | null>(null)
</script>

View File

@@ -15,7 +15,7 @@
>
<span class="select-wrapper">
<HoppButtonSecondary
:label="contentType || t('state.none')"
:label="body.contentType || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
@@ -28,11 +28,11 @@
>
<HoppSmartItem
:label="t('state.none')"
:info-icon="contentType === null ? IconDone : null"
:active-info-icon="contentType === null"
:info-icon="(body.contentType === null ? IconDone : null) as any"
:active-info-icon="body.contentType === null"
@click="
() => {
contentType = null
body.contentType = null
hide()
}
"
@@ -57,12 +57,12 @@
:key="`contentTypeItem-${contentTypeIndex}`"
:label="contentTypeItem"
:info-icon="
contentTypeItem === contentType ? IconDone : null
contentTypeItem === body.contentType ? IconDone : null
"
:active-info-icon="contentTypeItem === contentType"
:active-info-icon="contentTypeItem === body.contentType"
@click="
() => {
contentType = contentTypeItem
body.contentType = contentTypeItem
hide()
}
"
@@ -93,13 +93,17 @@
/>
</span>
</div>
<HttpBodyParameters v-if="contentType === 'multipart/form-data'" />
<HttpURLEncodedParams
v-else-if="contentType === 'application/x-www-form-urlencoded'"
<HttpBodyParameters
v-if="body.contentType === 'multipart/form-data'"
v-model="body"
/>
<HttpRawBody v-else-if="contentType !== null" :content-type="contentType" />
<HttpURLEncodedParams
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
v-model="body"
/>
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
<div
v-if="contentType == null"
v-if="body.contentType == null"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
@@ -123,38 +127,37 @@
</template>
<script setup lang="ts">
import IconDone from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import IconExternalLink from "~icons/lucide/external-link"
import { computed, ref } from "vue"
import { pipe } from "fp-ts/function"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { HoppRESTHeader, HoppRESTReqBody } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import * as A from "fp-ts/Array"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { computed, ref } from "vue"
import { segmentedContentTypes } from "~/helpers/utils/contenttypes"
import {
restContentType$,
restHeaders$,
setRESTContentType,
setRESTHeaders,
addRESTHeader,
} from "~/newstore/RESTSession"
import IconDone from "~icons/lucide/check"
import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RequestOptionTabs } from "./RequestOptions.vue"
const colorMode = useColorMode()
const t = useI18n()
const emit = defineEmits<{
(e: "change-tab", value: string): void
const props = defineProps<{
body: HoppRESTReqBody
headers: HoppRESTHeader[]
}>()
const contentType = useStream(restContentType$, null, setRESTContentType)
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
(e: "update:headers", value: HoppRESTHeader[]): void
(e: "update:body", value: HoppRESTReqBody): void
}>()
// The functional headers list (the headers actually in the system)
const headers = useStream(restHeaders$, [], setRESTHeaders)
const headers = useVModel(props, "headers", emit)
const body = useVModel(props, "body", emit)
const overridenContentType = computed(() =>
pipe(
@@ -168,7 +171,9 @@ const overridenContentType = computed(() =>
const contentTypeOverride = (tab: RequestOptionTabs) => {
emit("change-tab", tab)
if (!isContentTypeAlreadyExist()) {
addRESTHeader({
// TODO: Fix this
headers.value.push({
key: "Content-Type",
value: "",
active: true,

View File

@@ -186,14 +186,26 @@ import { ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { FormDataKeyValue } from "@hoppscotch/data"
import { FormDataKeyValue, HoppRESTReqBody } from "@hoppscotch/data"
import { isEqual, clone } from "lodash-es"
import draggable from "vuedraggable-es"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import { useVModel } from "@vueuse/core"
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
const props = defineProps<{
modelValue: Body
}>()
const emit = defineEmits<{
(e: "update:modelValue", val: Body): void
}>()
const body = useVModel(props, "modelValue", emit)
type WorkingFormDataKeyValue = { id: number; entry: FormDataKeyValue }
@@ -206,7 +218,7 @@ const idTicker = ref(0)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body")
const bodyParams = pluckRef(body, "body")
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<WorkingFormDataKeyValue[]>([
@@ -355,7 +367,7 @@ const clearContent = () => {
const setRequestAttachment = (
index: number,
entry: FormDataKeyValue,
event: InputEvent
event: InputEvent | Event
) => {
// check if file exists or not
if ((event.target as HTMLInputElement).files?.length === 0) {

View File

@@ -148,7 +148,6 @@ import {
resolvesEnvsInBody,
} from "~/helpers/utils/EffectiveURL"
import { getAggregateEnvs } from "~/newstore/environments"
import { getRESTRequest } from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import {
@@ -164,6 +163,8 @@ import {
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconWrapText from "~icons/lucide/wrap-text"
import { currentActiveTab } from "~/helpers/rest/tab"
import cloneDeep from "lodash-es/cloneDeep"
const t = useI18n()
@@ -177,7 +178,7 @@ const emit = defineEmits<{
const toast = useToast()
const request = ref(getRESTRequest())
const request = ref(cloneDeep(currentActiveTab.value.document.request))
const codegenType = ref<CodegenName>("shell-curl")
const errorState = ref(false)
@@ -246,7 +247,7 @@ watch(
() => props.show,
(goingToShow) => {
if (goingToShow) {
request.value = getRESTRequest()
request.value = cloneDeep(currentActiveTab.value.document.request)
}
}
)

View File

@@ -48,7 +48,7 @@
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header) => `header-${header.id}`"
:item-key="(header: WorkingHeader) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
@@ -240,10 +240,11 @@ import IconEyeOff from "~icons/lucide/eye-off"
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconWrapText from "~icons/lucide/wrap-text"
import { useColorMode } from "@composables/theming"
import { computed, reactive, Ref, ref, watch } from "vue"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
import {
HoppRESTHeader,
HoppRESTRequest,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
@@ -256,15 +257,9 @@ import * as A from "fp-ts/Array"
import draggable from "vuedraggable-es"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { useCodemirror } from "@composables/codemirror"
import {
getRESTRequest,
restHeaders$,
restRequest$,
setRESTHeaders,
} from "~/newstore/RESTSession"
import { commonHeaders } from "~/helpers/headers"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { throwError } from "~/helpers/functional/error"
@@ -274,6 +269,7 @@ import {
getComputedHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const toast = useToast()
@@ -288,10 +284,16 @@ const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
(e: "update:modelValue", value: HoppRESTRequest): void
}>()
const request = useVModel(props, "modelValue", emit)
useCodemirror(
bulkEditor,
bulkHeaders,
@@ -307,13 +309,10 @@ useCodemirror(
})
)
// The functional headers list (the headers actually in the system)
const headers = useStream(restHeaders$, [], setRESTHeaders) as Ref<
HoppRESTHeader[]
>
type WorkingHeader = HoppRESTHeader & { id: number }
// The UI representation of the headers list (has the empty end headers)
const workingHeaders = ref<Array<HoppRESTHeader & { id: number }>>([
const workingHeaders = ref<Array<WorkingHeader>>([
{
id: idTicker.value++,
key: "",
@@ -339,7 +338,7 @@ watch(workingHeaders, (headersList) => {
// Sync logic between headers and working/bulk headers
watch(
headers,
request.value.headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
@@ -388,8 +387,8 @@ watch(workingHeaders, (newWorkingHeaders) => {
)
)
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = cloneDeep(fixedHeaders)
if (!isEqual(request.value.headers, fixedHeaders)) {
request.value.headers = cloneDeep(fixedHeaders)
}
})
@@ -405,8 +404,8 @@ watch(bulkHeaders, (newBulkHeaders) => {
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(headers.value, filteredBulkHeaders)) {
headers.value = filteredBulkHeaders
if (!isEqual(props.modelValue, filteredBulkHeaders)) {
request.value.headers = filteredBulkHeaders
}
})
@@ -481,11 +480,10 @@ const clearContent = () => {
bulkHeaders.value = ""
}
const restRequest = useReadonlyStream(restRequest$, getRESTRequest())
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
const computedHeaders = computed(() =>
getComputedHeaders(restRequest.value, aggregateEnvs.value).map(
getComputedHeaders(request.value, aggregateEnvs.value).map(
(header, index) => ({
id: `header-${index}`,
...header,

View File

@@ -81,7 +81,6 @@
import { reactive, ref, watch } from "vue"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { setRESTRequest } from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
@@ -94,6 +93,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2"
import { currentActiveTab } from "~/helpers/rest/tab"
const t = useI18n()
@@ -144,7 +144,7 @@ const handleImport = () => {
try {
const req = parseCurlToHoppRESTReq(text)
setRESTRequest(req)
currentActiveTab.value.document.request = req
} catch (e) {
console.error(e)
toast.error(`${t("error.curl_invalid_format")}`)

View File

@@ -31,89 +31,72 @@
</div>
</template>
<script lang="ts">
import { Ref, defineComponent } from "vue"
<script setup lang="ts">
import { ref, watch } from "vue"
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
import { tokenRequest } from "~/helpers/oauth"
import { getCombinedEnvVariables } from "~/helpers/preRequest"
export default defineComponent({
setup() {
const t = useI18n()
const toast = useToast()
const t = useI18n()
const toast = useToast()
const auth = useStream(
restAuth$,
{ authType: "none", authActive: true },
setRESTAuth
)
const props = defineProps<{
modelValue: HoppRESTAuthOAuth2
}>()
const oidcDiscoveryURL = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"oidcDiscoveryURL"
)
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuthOAuth2): void
}>()
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
const auth = ref(props.modelValue)
const accessTokenURL = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"accessTokenURL"
)
watch(
() => auth.value,
(val) => {
emit("update:modelValue", val)
}
)
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
const clientSecret = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"clientSecret"
)
const authURL = pluckRef(auth, "authURL")
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
const accessTokenURL = pluckRef(auth, "accessTokenURL")
const handleAccessTokenRequest = async () => {
if (
oidcDiscoveryURL.value === "" &&
(authURL.value === "" || accessTokenURL.value === "")
) {
toast.error(`${t("error.incomplete_config_urls")}`)
return
}
const envs = getCombinedEnvVariables()
const envVars = [...envs.selected, ...envs.global]
const clientID = pluckRef(auth, "clientID")
try {
const tokenReqParams = {
grantType: "code",
oidcDiscoveryUrl: parseTemplateString(
oidcDiscoveryURL.value,
envVars
),
authUrl: parseTemplateString(authURL.value, envVars),
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
clientId: parseTemplateString(clientID.value, envVars),
clientSecret: parseTemplateString(clientSecret.value, envVars),
scope: parseTemplateString(scope.value, envVars),
}
await tokenRequest(tokenReqParams)
} catch (e) {
toast.error(`${e}`)
}
// TODO: Fix this type error. currently there is no type for clientSecret
const clientSecret = pluckRef(auth, "clientSecret" as any)
const scope = pluckRef(auth, "scope")
const handleAccessTokenRequest = async () => {
if (
oidcDiscoveryURL.value === "" &&
(authURL.value === "" || accessTokenURL.value === "")
) {
toast.error(`${t("error.incomplete_config_urls")}`)
return
}
const envs = getCombinedEnvVariables()
const envVars = [...envs.selected, ...envs.global]
try {
const tokenReqParams = {
grantType: "code",
oidcDiscoveryUrl: parseTemplateString(oidcDiscoveryURL.value, envVars),
authUrl: parseTemplateString(authURL.value, envVars),
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
clientId: parseTemplateString(clientID.value, envVars),
clientSecret: parseTemplateString(clientSecret.value, envVars),
scope: parseTemplateString(scope.value, envVars),
}
return {
oidcDiscoveryURL,
authURL,
accessTokenURL,
clientID,
clientSecret,
scope,
handleAccessTokenRequest,
t,
}
},
})
await tokenRequest(tokenReqParams)
} catch (e) {
toast.error(`${e}`)
}
}
</script>

View File

@@ -179,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import IconWrapText from "~icons/lucide/wrap-text"
import { reactive, Ref, ref, watch } from "vue"
import { reactive, ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -198,10 +198,9 @@ import { useCodemirror } from "@composables/codemirror"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useStream } from "@composables/stream"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "@functional/error"
import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core"
const colorMode = useColorMode()
@@ -232,8 +231,16 @@ useCodemirror(
})
)
const props = defineProps<{
modelValue: HoppRESTParam[]
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: Array<HoppRESTParam>): void
}>()
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
const params = useVModel(props, "modelValue", emit)
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([

View File

@@ -66,16 +66,23 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconTrash2 from "~icons/lucide/trash-2"
import { reactive, ref } from "vue"
import { usePreRequestScript } from "~/newstore/RESTSession"
import snippets from "@helpers/preRequestScriptSnippets"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/preRequest"
import completer from "~/helpers/editor/completion/preRequest"
import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const preRequestScript = usePreRequestScript()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: string): void
}>()
const preRequestScript = useVModel(props, "modelValue", emit)
const preRequestEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)

View File

@@ -35,7 +35,7 @@
'application/hal+json',
'application/vnd.api+json',
'application/xml',
].includes(contentType)
].includes(body.contentType)
"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
@@ -74,16 +74,14 @@ import IconInfo from "~icons/lucide/info"
import { computed, reactive, Ref, ref, watch } from "vue"
import * as TO from "fp-ts/TaskOption"
import { pipe } from "fp-ts/function"
import { ValidContentTypes } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { HoppRESTReqBody, ValidContentTypes } from "@hoppscotch/data"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { getEditorLangForMimeType } from "@helpers/editorutils"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { isJSONContentType } from "~/helpers/utils/contenttypes"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files"
@@ -92,27 +90,35 @@ type PossibleContentTypes = Exclude<
"multipart/form-data" | "application/x-www-form-urlencoded"
>
type Body = HoppRESTReqBody & { contentType: PossibleContentTypes }
const props = defineProps<{
modelValue: Body
}>()
const emit = defineEmits<{
(e: "update:modelValue", val: Body): void
}>()
const body = useVModel(props, "modelValue", emit)
const t = useI18n()
const payload = ref<HTMLInputElement | null>(null)
const props = defineProps<{
contentType: PossibleContentTypes
}>()
const toast = useToast()
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
const rawParamsBody = pluckRef(body, "body")
const prettifyIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const rawInputEditorLang = computed(() =>
getEditorLangForMimeType(props.contentType)
getEditorLangForMimeType(body.value.contentType)
)
const langLinter = computed(() =>
isJSONContentType(props.contentType) ? jsonLinter : null
isJSONContentType(body.value.contentType) ? jsonLinter : null
)
const linewrapEnabled = ref(true)
@@ -175,10 +181,10 @@ const uploadPayload = async (e: Event) => {
const prettifyRequestBody = () => {
let prettifyBody = ""
try {
if (props.contentType.endsWith("json")) {
if (body.value.contentType.endsWith("json")) {
const jsonObj = JSON.parse(rawParamsBody.value as string)
prettifyBody = JSON.stringify(jsonObj, null, 2)
} else if (props.contentType == "application/xml") {
} else if (body.value.contentType == "application/xml") {
prettifyBody = prettifyXML(rawParamsBody.value as string)
}
rawParamsBody.value = prettifyBody

View File

@@ -17,10 +17,10 @@
<input
id="method"
class="flex px-4 py-2 font-semibold transition rounded-l cursor-pointer text-secondaryDark w-26 bg-primaryLight"
:value="newMethod"
:value="tab.document.request.method"
:readonly="!isCustomMethod"
:placeholder="`${t('request.method')}`"
@input="onSelectMethod($event.target.value)"
@input="onSelectMethod($event)"
/>
</span>
<template #content="{ hide }">
@@ -36,7 +36,7 @@
:label="method"
@click="
() => {
onSelectMethod(method)
updateMethod(method)
hide()
}
"
@@ -50,7 +50,7 @@
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
>
<SmartEnvInput
v-model="newEndpoint"
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`"
@enter="newSendRequest()"
@paste="onPasteUrl($event)"
@@ -161,12 +161,11 @@
ref="saveTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.s="saveRequestAction.$el.click()"
@keyup.escape="hide()"
>
<input
id="request-name"
v-model="requestName"
v-model="tab.document.request.name"
:placeholder="`${t('request.name')}`"
name="request-name"
type="text"
@@ -195,7 +194,6 @@
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
:icon="IconFolderPlus"
:shortcut="['S']"
@click="
() => {
showSaveRequestModal = true
@@ -227,55 +225,40 @@
</template>
<script setup lang="ts">
import IconShare2 from "~icons/lucide/share-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconFileCode from "~icons/lucide/file-code"
import IconCode2 from "~icons/lucide/code-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconLink2 from "~icons/lucide/link-2"
import IconFolderPlus from "~icons/lucide/folder-plus"
import { computed, ref, watch } from "vue"
import { isLeft, isRight } from "fp-ts/lib/Either"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import { refAutoReset } from "@vueuse/core"
import {
updateRESTResponse,
restEndpoint$,
setRESTEndpoint,
restMethod$,
updateRESTMethod,
resetRESTRequest,
useRESTRequestName,
getRESTSaveContext,
getRESTRequest,
restRequest$,
setRESTSaveContext,
} from "~/newstore/RESTSession"
import { editRESTRequest } from "~/newstore/collections"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import {
useStream,
useStreamSubscriber,
useReadonlyStream,
} from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useSetting } from "@composables/settings"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
import { refAutoReset, useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either"
import { isLeft, isRight } from "fp-ts/lib/Either"
import { computed, ref, watch } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "~/helpers/strategies/ExtensionStrategy"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { editRESTRequest } from "~/newstore/collections"
import IconCheck from "~icons/lucide/check"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconCode2 from "~icons/lucide/code-2"
import IconCopy from "~icons/lucide/copy"
import IconFileCode from "~icons/lucide/file-code"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconLink2 from "~icons/lucide/link-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
const t = useI18n()
@@ -296,9 +279,19 @@ const toast = useToast()
const { subscribeToStream } = useStreamSubscriber()
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
const props = defineProps<{ modelValue: HoppRESTTab }>()
const emit = defineEmits(["update:modelValue"])
const tab = useVModel(props, "modelValue", emit)
const newEndpoint = computed(() => {
return tab.value.document.request.endpoint
})
const newMethod = computed(() => {
return tab.value.document.request.method
})
const curlText = ref("")
const newMethod = useStream(restMethod$, "", updateRESTMethod)
const loading = ref(false)
@@ -327,6 +320,30 @@ watch(loading, () => {
}
})
// TODO: make this oAuthURL() work
// function oAuthURL() {
// const auth = useReadonlyStream(props.request.auth$, {
// authType: "none",
// authActive: true,
// })
// const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
// onBeforeMount(async () => {
// try {
// const tokenInfo = await oauthRedirect()
// if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
// if (typeof tokenInfo === "object") {
// oauth2Token.value = tokenInfo.access_token
// }
// }
// // eslint-disable-next-line no-empty
// } catch (_) {}
// })
// }
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
@@ -335,10 +352,14 @@ const newSendRequest = async () => {
ensureMethodInEndpoint()
console.log("Sending request", newEndpoint.value)
loading.value = true
// Double calling is because the function returns a TaskEither than should be executed
const streamResult = await runRESTRequest$()()
const streamResult = await runRESTRequest$(tab)()
console.log("Stream result", streamResult)
if (isRight(streamResult)) {
subscribeToStream(
@@ -380,9 +401,11 @@ const ensureMethodInEndpoint = () => {
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
setRESTEndpoint("http://" + newEndpoint.value)
tab.value.document.request.endpoint =
"http://" + tab.value.document.request.endpoint
} else {
setRESTEndpoint("https://" + newEndpoint.value)
tab.value.document.request.endpoint =
"https://" + tab.value.document.request.endpoint
}
}
}
@@ -395,7 +418,7 @@ const onPasteUrl = (e: { pastedValue: string; prevValue: string }) => {
if (isCURL(pastedData)) {
showCurlImportModal.value = true
curlText.value = pastedData
newEndpoint.value = e.prevValue
tab.value.document.request.endpoint = e.prevValue
}
}
@@ -412,15 +435,21 @@ const cancelRequest = () => {
}
const updateMethod = (method: string) => {
updateRESTMethod(method)
tab.value.document.request.method = method
}
const onSelectMethod = (method: string) => {
updateMethod(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.value)
}
const clearContent = () => {
resetRESTRequest()
tab.value.document.request = getDefaultRESTRequest()
}
const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.response = response
console.log("Updating response", response)
}
const copyLinkIcon = refAutoReset<
@@ -440,20 +469,13 @@ const shareButtonText = computed(() => {
}
})
const request = useReadonlyStream(restRequest$, getRESTRequest())
watch(request, () => {
shareLink.value = null
})
const copyRequest = async () => {
if (shareLink.value) {
copyShareLink(shareLink.value)
} else {
shareLink.value = ""
fetchingShareLink.value = true
const request = getRESTRequest()
const shortcodeResult = await createShortcode(request)()
const shortcodeResult = await createShortcode(tab.value.document.request)()
if (E.isLeft(shortcodeResult)) {
toast.error(`${shortcodeResult.left.error}`)
shareLink.value = `${t("error.something_went_wrong")}`
@@ -511,33 +533,26 @@ const cycleDownMethod = () => {
}
const saveRequest = () => {
const saveCtx = getRESTSaveContext()
const saveCtx = tab.value.document.saveContext
if (!saveCtx) {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "user-collection") {
const req = getRESTRequest()
const req = tab.value.document.request
try {
editRESTRequest(
saveCtx.folderPath,
saveCtx.requestIndex,
getRESTRequest()
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: saveCtx.folderPath,
requestIndex: saveCtx.requestIndex,
req: cloneDeep(req),
})
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
tab.value.document.isDirty = false
toast.success(`${t("request.saved")}`)
} catch (e) {
setRESTSaveContext(null)
tab.value.document.saveContext = undefined
saveRequest()
}
} else if (saveCtx.originLocation === "team-collection") {
const req = getRESTRequest()
const req = tab.value.document.request
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
try {
@@ -551,11 +566,8 @@ const saveRequest = () => {
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
} else {
setRESTSaveContext({
originLocation: "team-collection",
requestID: saveCtx.requestID,
req: cloneDeep(req),
})
tab.value.document.isDirty = false
toast.success(`${t("request.saved")}`)
}
})
@@ -587,10 +599,11 @@ defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
const isCustomMethod = computed(() => {
return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
return (
tab.value.document.request.method === "CUSTOM" ||
!methods.includes(newMethod.value)
)
})
const requestName = useRESTRequestName()
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
</script>

View File

@@ -9,51 +9,52 @@
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
>
<HttpParameters />
<HttpParameters v-model="request.params" />
</HoppSmartTab>
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
<HttpBody @change-tab="changeTab" />
<HttpBody
v-model:headers="request.headers"
v-model:body="request.body"
@change-tab="changeTab"
/>
</HoppSmartTab>
<HoppSmartTab
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders @change-tab="changeTab" />
<HttpHeaders v-model="request" @change-tab="changeTab" />
</HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<HttpAuthorization />
<HttpAuthorization v-model="request.auth" />
</HoppSmartTab>
<HoppSmartTab
:id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`"
:indicator="
preRequestScript && preRequestScript.length > 0 ? true : false
request.preRequestScript && request.preRequestScript.length > 0
? true
: false
"
>
<HttpPreRequestScript />
<HttpPreRequestScript v-model="request.preRequestScript" />
</HoppSmartTab>
<HoppSmartTab
:id="'tests'"
:label="`${t('tab.tests')}`"
:indicator="testScript && testScript.length > 0 ? true : false"
:indicator="
request.testScript && request.testScript.length > 0 ? true : false
"
>
<HttpTests />
<HttpTests v-model="request.testScript" />
</HoppSmartTab>
</HoppSmartTabs>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { map } from "rxjs/operators"
import { useReadonlyStream } from "@composables/stream"
import {
restActiveHeadersCount$,
restActiveParamsCount$,
usePreRequestScript,
useTestScript,
} from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { computed, ref, watch } from "vue"
export type RequestOptionTabs =
| "params"
@@ -63,33 +64,43 @@ export type RequestOptionTabs =
const t = useI18n()
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTRequest): void
}>()
const request = ref(props.modelValue)
watch(
() => request.value,
(newVal) => {
emit("update:modelValue", newVal)
},
{ deep: true }
)
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
const changeTab = (e: RequestOptionTabs) => {
selectedRealtimeTab.value = e
}
const newActiveParamsCount$ = useReadonlyStream(
restActiveParamsCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
const newActiveParamsCount$ = computed(() => {
const e = request.value.params.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
).length
const newActiveHeadersCount$ = useReadonlyStream(
restActiveHeadersCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
if (e === 0) return null
return `${e}`
})
const preRequestScript = usePreRequestScript()
const newActiveHeadersCount$ = computed(() => {
const e = request.value.headers.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
).length
const testScript = useTestScript()
if (e === 0) return null
return `${e}`
})
</script>

View File

@@ -0,0 +1,46 @@
<template>
<AppPaneLayout layout-id="rest-primary">
<template #primary>
<HttpRequest v-model="tab" />
<HttpRequestOptions v-model="tab.document.request" />
</template>
<template #secondary>
<HttpResponse v-model:tab="tab" />
</template>
</AppPaneLayout>
</template>
<script setup lang="ts">
import { watch } from "vue"
import { useVModel } from "@vueuse/core"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { cloneDeep } from "lodash-es"
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
// TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppRESTTab }>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppRESTTab): void
}>()
const tab = useVModel(props, "modelValue", emit)
// TODO: Come up with a better dirty check
let oldRequest = cloneDeep(tab.value.document.request)
watch(
() => tab.value.document.request,
(updatedValue) => {
if (
!tab.value.document.isDirty &&
!isEqualHoppRESTRequest(oldRequest, updatedValue)
) {
tab.value.document.isDirty = true
}
oldRequest = cloneDeep(updatedValue)
},
{ deep: true }
)
</script>

View File

@@ -1,34 +1,42 @@
<template>
<div class="flex flex-col flex-1">
<HttpResponseMeta :response="response" />
<HttpResponseMeta :response="tab.response" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"
v-model:selected-tab-preference="selectedTabPreference"
:response="response"
v-model:tab="tab"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { computed, ref, watch } from "vue"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import { useReadonlyStream } from "@composables/stream"
import { restResponse$ } from "~/newstore/RESTSession"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { useVModel } from "@vueuse/core"
const props = defineProps<{
tab: HoppRESTTab
}>()
const emit = defineEmits<{
(e: "update:tab", val: HoppRESTTab): void
}>()
const tab = useVModel(props, "tab", emit)
const selectedTabPreference = ref<string | null>(null)
const response = useReadonlyStream(restResponse$, null)
const hasResponse = computed(
() => response.value?.type === "success" || response.value?.type === "fail"
() =>
tab.value.response?.type === "success" ||
tab.value.response?.type === "fail"
)
const loading = computed(
() => response.value === null || response.value.type === "loading"
)
const loading = computed(() => tab.value.response?.type === "loading")
watch(response, () => {
if (response.value?.type === "loading") startPageProgress()
watch(loading, (isLoading) => {
if (isLoading) startPageProgress()
else completePageProgress()
})
</script>

View File

@@ -107,7 +107,7 @@ const t = useI18n()
const colorMode = useColorMode()
const props = defineProps<{
response: HoppRESTResponse | null
response: HoppRESTResponse | null | undefined
}>()
/**
@@ -119,6 +119,7 @@ const props = defineProps<{
const readableResponseSize = computed(() => {
if (
props.response === null ||
props.response === undefined ||
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail" ||
@@ -137,6 +138,7 @@ const readableResponseSize = computed(() => {
const statusCategory = computed(() => {
if (
props.response === null ||
props.response === undefined ||
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail" ||

View File

@@ -5,13 +5,6 @@
vertical
render-inactive-tabs
>
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'rest'" />
</HoppSmartTab>
<HoppSmartTab
:id="'collections'"
:icon="IconFolder"
@@ -26,6 +19,13 @@
>
<Environments />
</HoppSmartTab>
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'rest'" />
</HoppSmartTab>
</HoppSmartTabs>
</template>
@@ -40,5 +40,5 @@ const t = useI18n()
type RequestOptionTabs = "history" | "collections" | "env"
const selectedNavigationTab = ref<RequestOptionTabs>("history")
const selectedNavigationTab = ref<RequestOptionTabs>("collections")
</script>

View File

@@ -216,7 +216,6 @@ import {
setGlobalEnvVariables,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { restTestResults$, setRESTTestResults } from "~/newstore/RESTSession"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import IconTrash2 from "~icons/lucide/trash-2"
@@ -226,6 +225,17 @@ import IconCheck from "~icons/lucide/check"
import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core"
const props = defineProps<{
modelValue: HoppTestResult | null | undefined
}>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppTestResult | null | undefined): void
}>()
const testResults = useVModel(props, "modelValue", emit)
const t = useI18n()
const colorMode = useColorMode()
@@ -236,11 +246,6 @@ const displayModalAdd = (shouldDisplay: boolean) => {
showModalDetails.value = shouldDisplay
}
const testResults = useReadonlyStream(
restTestResults$,
null
) as Ref<HoppTestResult | null>
/**
* Get the "addition" environment variables
* @returns Array of objects with key-value pairs of arguments
@@ -250,7 +255,9 @@ const getAdditionVars = () =>
? testResults.value.envDiff.selected.additions
: []
const clearContent = () => setRESTTestResults(null)
const clearContent = () => {
testResults.value = null
}
const haveEnvVariables = computed(() => {
if (!testResults.value) return false

View File

@@ -66,17 +66,20 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconTrash2 from "~icons/lucide/trash-2"
import { reactive, ref } from "vue"
import { useTestScript } from "~/newstore/RESTSession"
import testSnippets from "~/helpers/testSnippets"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/testScript"
import completer from "~/helpers/editor/completion/testScript"
import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const testScript = useTestScript()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits(["update:modelValue"])
const testScript = useVModel(props, "modelValue", emit)
const testScriptEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)

View File

@@ -181,6 +181,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
import {
HoppRESTReqBody,
parseRawKeyValueEntries,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
@@ -194,13 +195,27 @@ import * as E from "fp-ts/Either"
import draggable from "vuedraggable-es"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { objRemoveKey } from "~/helpers/functional/object"
import { throwError } from "~/helpers/functional/error"
import { useVModel } from "@vueuse/core"
type Body = HoppRESTReqBody & {
contentType: "application/x-www-form-urlencoded"
}
const props = defineProps<{
modelValue: Body
}>()
const emit = defineEmits<{
(e: "update:modelValue", val: Body): void
}>()
const body = useVModel(props, "modelValue", emit)
const t = useI18n()
const toast = useToast()
@@ -231,7 +246,7 @@ useCodemirror(
)
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
const urlEncodedParamsRaw = pluckRef(useRESTRequestBody(), "body")
const urlEncodedParamsRaw = pluckRef(body, "body")
const urlEncodedParams = computed<RawKeyValueEntry[]>({
get() {

View File

@@ -0,0 +1,82 @@
<template>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.key" placeholder="Key" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.value" placeholder="Value" />
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => authTippyActions.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
:label="auth.addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="authTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="auth.addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="auth.addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
auth.addTo = 'Headers'
hide()
}
"
/>
<HoppSmartItem
:icon="auth.addTo === 'Query params' ? IconCircleDot : IconCircle"
:active="auth.addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
auth.addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</template>
<script setup lang="ts">
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"
import { useI18n } from "@composables/i18n"
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { ref } from "vue"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthAPIKey
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuthAPIKey): void
}>()
const auth = useVModel(props, "modelValue", emit)
const authTippyActions = ref<any | null>(null)
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.username"
:placeholder="t('authorization.username')"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.password"
:placeholder="t('authorization.password')"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { HoppRESTAuthBasic } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthBasic
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuthBasic): void
}>()
const auth = useVModel(props, "modelValue", emit)
</script>