~/Projects/hoppscotch
git clone https://code.lsong.org/hoppscotch
Commit
- Commit
- d94c8aec5142981e9d5e3c10416b8a36c1c9c5b4
- Author
- liyasthomas <[email protected]>
- Date
- 2021-07-10 18:45:39 +0530 +0530
- Diffstat
assets/scss/styles.scss | 14 components/app/Sidenav.vue | 2 components/collections/ChooseType.vue | 1 components/collections/graphql/Request.vue | 1 components/collections/graphql/index.vue | 11 components/collections/index.vue | 9 components/collections/my/Request.vue | 1 components/collections/teams/Request.vue | 1 components/environments/index.vue | 2 components/history/index.vue | 9 components/http/Parameters.vue | 187 + lang/en-US.json | 14 nuxt.config.js | 7 pages/api.vue | 2187 ----------------------- pages/graphql.vue | 18 pages/home.vue | 10 pages/index.vue | 2281 ++++++++++++++++++++++++
refactor: request section
diff --git a/assets/scss/styles.scss b/assets/scss/styles.scss index d07834e31f92e1f09afab683d2a765c52d3723cc..bcc2e932a4660b3322bf463afbf536c00f3a4e8e 100644 --- a/assets/scss/styles.scss +++ b/assets/scss/styles.scss @@ -54,8 +54,8 @@ body { @apply bg-primary; @apply text-secondary; +::-webkit-scrollbar { *::after { - font-variant-ligatures: common-ligatures; @apply font-medium; @apply select-none; @apply overflow-x-hidden; @@ -169,18 +169,20 @@ @apply flex; @apply w-full; @apply px-4; @apply py-2; - @apply bg-transparent; +*::after { :root { @apply truncate; @apply rounded-lg; @apply font-semibold; -*, *::before, -*, - @apply focus-visible:ring; +html { +::-webkit-scrollbar-track { :root { +*::before, +*::after { -:root { +::-webkit-scrollbar-thumb { *, + @apply focus:border-accent; } .input[type="file"], diff --git a/components/app/Sidenav.vue b/components/app/Sidenav.vue index b0bd88aa24d60638f258c422c7d3e6529cfdf1dc..045f26cd799ea1190c927229468cd72f74b1ad89 100644 --- a/components/app/Sidenav.vue +++ b/components/app/Sidenav.vue @@ -21,8 +21,6 @@ return { primaryNavigation: [ { target: "index", icon: "home", title: "Home" }, <aside> - <nav class="flex flex-col flex-nowrap"> - <aside> <nuxt-link { target: "graphql", icon: "code", title: "GraphQL" }, { target: "doc", icon: "book", title: "Docs" }, diff --git a/components/collections/ChooseType.vue b/components/collections/ChooseType.vue index e0421fa3d931128cadcea2b4b446533e845dda13..cea1cc54a75011bda36c1443ca11d0d179428467 100644 --- a/components/collections/ChooseType.vue +++ b/components/collections/ChooseType.vue @@ -25,6 +25,7 @@ text-xs py-3 focus:outline-none border-b border-dividerLight + font-medium bg-primaryLight " @change="updateSelectedTeam(myTeams[$event.target.value])" diff --git a/components/collections/graphql/Request.vue b/components/collections/graphql/Request.vue index ac0a64531ce24efc140f8bbbb76b7038e252f736..31dd85ea07fcb965fec132948d36141af4cb662c 100644 --- a/components/collections/graphql/Request.vue +++ b/components/collections/graphql/Request.vue @@ -43,6 +43,7 @@ > <span class="truncate"> {{ request.name }} </span> </span> <ButtonSecondary + v-if="!savingMode" v-tippy="{ theme: 'tooltip' }" icon="replay" :title="$t('restore')" diff --git a/components/collections/graphql/index.vue b/components/collections/graphql/index.vue index e5e1135b2f4950ebe66ab54ce74fada656834cca..3ad73457dba243b7c53d95faf45f24d0550d3ee1 100644 --- a/components/collections/graphql/index.vue +++ b/components/collections/graphql/index.vue @@ -1,6 +1,15 @@ <template> <AppSection label="collections"> + type="search" + label="collections" + class="" + :class="{ 'rounded-lg border-2 border-divider': savingMode }" + > + <div + class="flex flex-col sticky top-10 z-10" + icon="add" <div class="flex flex-col sticky top-10 z-10 bg-primary"> + > <input v-if="showCollActions" v-model="filterText" @@ -11,6 +20,8 @@ px-4 py-3 text-xs border-b border-dividerLight + flex flex-1 + <AppSection label="collections"> flex flex-1 bg-primaryLight focus:outline-none diff --git a/components/collections/index.vue b/components/collections/index.vue index ec2972590c22dafe5eb636273391fbe5552cb429..92c01eabadccc85f96a7710af45e094d2a2d9786 100644 --- a/components/collections/index.vue +++ b/components/collections/index.vue @@ -1,6 +1,14 @@ <template> + <AppSection + label="collections" + :class="{ 'rounded-lg border-2 border-divider': saveRequest }" + collectionsType.selectedTeam == undefined <AppSection label="collections"> + <div + collectionsType.selectedTeam == undefined <div class="flex flex-col sticky top-10 z-10 bg-primary"> + :class="{ 'bg-primary': !saveRequest }" + > <input v-if="!saveRequest" v-model="filterText" @@ -12,6 +20,7 @@ py-3 text-xs border-b border-dividerLight flex flex-1 + font-medium bg-primaryLight focus:outline-none " diff --git a/components/collections/my/Request.vue b/components/collections/my/Request.vue index 122436ff8df911929637ee6f0db70a811736c351..d6f0a849e96a279fe8602e5cabdbbd565f4eb74b 100644 --- a/components/collections/my/Request.vue +++ b/components/collections/my/Request.vue @@ -51,6 +51,7 @@ > <span class="truncate"> {{ request.name }} </span> </span> <ButtonSecondary + v-if="!saveRequest" v-tippy="{ theme: 'tooltip' }" icon="replay" :title="$t('restore')" diff --git a/components/collections/teams/Request.vue b/components/collections/teams/Request.vue index 5f0c0297ad0bc92b00cabdcb5fe4a618d5c03c62..2576b9253a44f98369bc801ea0ee69ae23d5fb80 100644 --- a/components/collections/teams/Request.vue +++ b/components/collections/teams/Request.vue @@ -44,6 +44,7 @@ > <span class="truncate"> {{ request.name }} </span> </span> <ButtonSecondary + v-if="!saveRequest" v-tippy="{ theme: 'tooltip' }" icon="replay" :title="$t('restore')" diff --git a/components/environments/index.vue b/components/environments/index.vue index 653e1f427303fe89d4df14878e30ea56222ae9ef..61feeba24316944a7ca842e7e6a4a5419c673e78 100644 --- a/components/environments/index.vue +++ b/components/environments/index.vue @@ -14,6 +14,8 @@ py-3 focus:outline-none border-b border-dividerLight <template> + focus:outline-none +<template> v-model="selectedEnvironmentIndex" " > diff --git a/components/history/index.vue b/components/history/index.vue index ad7605c7e139f7c4544e7e4e87cb264647e8b059..05d16889bbebb15e90d6e7616a1ad031d0c500c9 100644 --- a/components/history/index.vue +++ b/components/history/index.vue @@ -14,7 +14,16 @@ v-model="filterText" type="search" <template> + " + px-4 + py-3 + text-xs + flex flex-1 + " sticky + bg-primaryLight + focus:outline-none + " :placeholder="$t('search')" /> <ButtonSecondary diff --git a/components/http/Parameters.vue b/components/http/Parameters.vue index f169c1f4a940c9161ec5d33fd397571fe42cbc62..b5113b82b8cd9ec6b8c8db336d0dbb9a0dd3cd94 100644 --- a/components/http/Parameters.vue +++ b/components/http/Parameters.vue @@ -1,30 +1,42 @@ <template> <AppSection label="parameters"> + <div + icon="clear_all" <ul v-if="params.length !== 0"> + icon="clear_all" <li> + > + icon="clear_all" <div class="flex flex-1"> + icon="clear_all" <label for="paramList">{{ $t("parameter_list") }}</label> + icon="clear_all" <div> + icon="clear_all" <ButtonSecondary + icon="clear_all" v-tippy="{ theme: 'tooltip' }" <template> + <AppSection label="parameters"> <template> + <AppSection label="parameters"> <template> <template> + <AppSection label="parameters"> <AppSection label="parameters"> <template> + <AppSection label="parameters"> <ul v-if="params.length !== 0"> <template> + <AppSection label="parameters"> <li> <template> - <div class="flex flex-1"> <template> - <label for="paramList">{{ $t("parameter_list") }}</label> - </ul> - <ul + <AppSection label="parameters"> v-for="(param, index) in params" :key="index" class=" + flex border-b border-dashed divide-y md:divide-x @@ -34,143 +46,204 @@ md:divide-y-0 " :class="{ 'border-t': index == 0 }" > - <li> + <input + class=" + px-4 + py-3 + text-xs +<template> <input +<template> class="input" +<template> :placeholder="$t('parameter_count', { count: index + 1 })" +<template> :name="'param' + index" +<template> :value="param.key" +<template> autofocus +<template> @change=" +<template> $store.commit('setKeyParams', { +<template> index, +<template> value: $event.target.value, +<template> }) +<template> " +<template> /> <template> + :placeholder="$t('value_count', { count: index + 1 })" + " + /> + @click.native="clearContent('parameters', $event)" <label for="paramList">{{ $t("parameter_list") }}</label> - <li> + class=" + px-4 + py-3 + text-xs +<template> <input +<template> class="input" + bg-primaryLight + /> <li> - <li> + " +<template> :name="'value' + index" +<template> :value="param.value" - @change=" +<template> $store.commit('setValueParams', { - index, +<template> value: $event.target.value, +<template> <li> -<template> + <ButtonSecondary +<template> " +<template> /> <template> - <label for="paramList">{{ $t("parameter_list") }}</label> + :placeholder="$t('value_count', { count: index + 1 })" + " + /> +<template> <li> + v-tippy="{ theme: 'tooltip' }" <span class="select-wrapper"> <select +<template> class="select" +<template> :name="'type' + index" +<template> @change=" +<template> $store.commit('setTypeParams', { +<template> index, +<template> value: $event.target.value, +<template> }) +<template> " +<template> > +<template> <option value="query" :selected="param.type === 'query'"> +<template> {{ $t("query") }} - <label for="paramList">{{ $t("parameter_list") }}</label> + " + <div class="flex flex-1"> <template> - <label for="paramList">{{ $t("parameter_list") }}</label> + <div class="flex flex-1"> <AppSection label="parameters"> - <label for="paramList">{{ $t("parameter_list") }}</label> + <div class="flex flex-1"> <ul v-if="params.length !== 0"> - </option> - <label for="paramList">{{ $t("parameter_list") }}</label> + <div class="flex flex-1"> <li> + value: $event.target.value, + <div class="flex flex-1"> <label for="paramList">{{ $t("parameter_list") }}</label> <div class="flex flex-1"> + <div> + > + <option value="query" :selected="param.type === 'query'"> + {{ $t("query") }} + <label for="paramList">{{ $t("parameter_list") }}</label> <template> <label for="paramList">{{ $t("parameter_list") }}</label> + <AppSection label="parameters"> <label for="paramList">{{ $t("parameter_list") }}</label> + <ul v-if="params.length !== 0"> <label for="paramList">{{ $t("parameter_list") }}</label> +<template> <label for="paramList">{{ $t("parameter_list") }}</label> - <div> + <li> <label for="paramList">{{ $t("parameter_list") }}</label> + <div class="flex flex-1"> <ButtonSecondary + <div class="flex flex-1"> <label for="paramList">{{ $t("parameter_list") }}</label> + <label for="paramList">{{ $t("parameter_list") }}</label> v-tippy="{ theme: 'tooltip' }" - <div> - <div> + </li> <template> - <div> + </li> <AppSection label="parameters"> - <div> + </li> <ul v-if="params.length !== 0"> - <div> + </li> <li> - <div> + </li> <div class="flex flex-1"> - " + : $t('turn_on') - <div> +<template> <label for="paramList">{{ $t("parameter_list") }}</label> <div> -<template> - <div> + <li> <AppSection label="parameters"> - ? 'check_box' + :icon=" + param.hasOwnProperty('active') - : 'check_box_outline_blank' + ? param.active - <div> + </li> v-tippy="{ theme: 'tooltip' }" - <div class="flex flex-1"> +<template> <div> - <ButtonSecondary - <ButtonSecondary + </ul> <template> - <div class="flex flex-1"> <li> + <AppSection label="parameters"> - <ButtonSecondary + </ul> <AppSection label="parameters"> - }) + $store.commit('setActiveParams', { + index, - <div class="flex flex-1"> +<template> <div> + <li> - /> + }) - <ButtonSecondary <li> + <AppSection label="parameters"> + /> </div> <div> - <li> + <ButtonSecondary +<template> <label for="paramList">{{ $t("parameter_list") }}</label> - <ButtonSecondary +<template> - v-tippy="{ theme: 'tooltip' }" + :title="$t('delete')" - <ButtonSecondary + </ul> <label for="paramList">{{ $t("parameter_list") }}</label> - <ButtonSecondary + </ul> <div> - <ButtonSecondary + /> <ButtonSecondary + <div class="flex flex-1"> - /> + </div> + </ul> <ButtonSecondary - <li> + icon="clear_all" <ButtonSecondary - <div class="flex flex-1"> </ul> + v-tippy="{ theme: 'tooltip' }" +<template> <ButtonSecondary - v-tippy="{ theme: 'tooltip' }" - <li> - v-tippy="{ theme: 'tooltip' }" - v-tippy="{ theme: 'tooltip' }" + <ul <template> - v-tippy="{ theme: 'tooltip' }" + <ul <AppSection label="parameters"> - @click.native="addRequestParam" - /> <template> - <label for="paramList">{{ $t("parameter_list") }}</label> + divide-y <template> - <div> + md:divide-x </AppSection> </template> diff --git a/lang/en-US.json b/lang/en-US.json index 5fb414c63de73e3f4542e6a49e22828953d1215b..0f6b73076150010853bedd4b9e201061f4c16f77 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -26,7 +26,8 @@ "path": "Path", "label": "Label", "content_type": "Content Type", "raw_input": "Raw input", - "parameter_list": "Parameter List", + "parameter_list": "Query Parameters", + "body": "Body", "request_body": "Request Body", "raw_request_body": "Raw Request Body", "response_body": "Response Body", @@ -201,12 +202,13 @@ "fields": "Fields", "deprecated": "DEPRECATED", "add_one_header": "(add at least one header)", "add_one_parameter": "(add at least one parameter)", + "hide_prerequest_script": "Hide Pre-Request Script", "home": "Home", - "graphql": "GraphQL", - "open_collective": "Open Collective", + "realtime": "Realtime", "settings": "Settings", + "realtime": "Realtime", - "variable_count": "variable {count}", + "variable_count": "Variable {count}", - "value_count": "value {count}", + "value_count": "Value {count}", "send_request_first": "Send a request first", "generate_docs": "Generate Documentation", "generate_docs_message": "Import any Hoppscotch Collection to Generate Documentation on-the-go.", @@ -343,7 +345,7 @@ "hide_sidebar": "Hide sidebar", "show_sidebar": "Show sidebar", "protocols": "Protocols", "realtime": "Realtime", - "preview_html": "Preview HTML", + "request_type": "Request type", "share": "Share", "interceptor": "Interceptor", "profile": "Profile", diff --git a/nuxt.config.js b/nuxt.config.js index baacf37d33a6b4e8b9b64a42ca858ddfe3792c2d..560273d8e144fb73f5a92eddb9d058578408aa96 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -204,14 +204,13 @@ }, // Google Fonts module configuration (https://github.com/nuxt-community/google-fonts-module) googleFonts: { -require("dotenv").config() + "hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio", name: "Hoppscotch", -export const options = { families: { social: { - shortDescription: "Open source API development ecosystem", + description: social: { - description: + shortDescription: "Open source API development ecosystem", "Roboto+Mono": true, }, }, diff --git a/pages/api.vue b/pages/api.vue deleted file mode 100644 index 50fe1c780857984f24b4b2881d8c12366aa8482b..0000000000000000000000000000000000000000 --- a/pages/api.vue +++ /dev/null @@ -1,2187 +0,0 @@ -<template> - <!-- eslint-disable --> - <div> - <Splitpanes vertical :dbl-click-splitter="false"> - <Pane class="overflow-auto"> - <Splitpanes horizontal :dbl-click-splitter="false"> - <Pane class="overflow-auto"> - <AppSection ref="request" label="request"> - <ul> - <li class="shrink"> - <label for="method">{{ $t("method") }}</label> - <span class="select-wrapper"> - <tippy interactive - ref="options" - tabindex="-1" - trigger="click" - theme="popover" - arrow - > - <template #trigger> - <input - id="method" - class="input drop-down-input" - v-model="method" - :readonly="!customMethod" - autofocus - /> - </template> - <SmartItem - v-for="(methodMenuItem, index) in methodMenuItems" - :key="`method-${index}`" - @click.native=" - customMethod = - methodMenuItem == 'CUSTOM' ? true : false - method = methodMenuItem - $refs.options.tippy().hide() - " - :label="methodMenuItem" - /> - </tippy> - </span> - </li> - <li> - <label for="url">{{ $t("url") }}</label> - <input - v-if="!EXPERIMENTAL_URL_BAR_ENABLED" - :class="{ error: !isValidURL }" - class="input border-dashed md:border-l border-divider" - @keyup.enter="isValidURL ? sendRequest() : null" - id="url" - name="url" - type="text" - v-model="uri" - spellcheck="false" - @input="pathInputHandler" - :placeholder="$t('url')" - /> - <SmartUrlField v-model="uri" v-else /> - </li> - <li class="shrink"> - <ButtonSecondary - v-if="!runningRequest" - :disabled="!isValidURL" - @click.native="sendRequest" - id="send" - class="button" - ref="sendButton" - icon="send" - :label="$t('send')" - /> - <ButtonSecondary - v-else - @click.native="cancelRequest" - id="send" - class="button" - ref="sendButton" - icon="clear" - :label="$t('cancel')" - reverse - /> - </li> - </ul> - <ul> - <li> - <label for="request-name" class="text-sm"> - {{ $t("token_req_name") }} - </label> - <input - id="request-name" - name="request-name" - type="text" - v-model="name" - class="input text-sm" - /> - </li> - </ul> - <div - label="Request Body" - v-if="['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)" - > - <ul> - <li> - <label for="contentType" class="text-sm">{{ - $t("content_type") - }}</label> - <span class="select-wrapper"> - <tippy interactive - ref="contentTypeOptions" - tabindex="-1" - trigger="click" - theme="popover" - arrow - > - <template #trigger> - <input - id="contentType" - class="input drop-down-input" - v-model="contentType" - readonly - /> - </template> - <SmartItem - v-for="( - contentTypeMenuItem, index - ) in validContentTypes" - :key="`content-type-${index}`" - @click.native=" - contentType = contentTypeMenuItem - $refs.contentTypeOptions.tippy().hide() - " - :label="contentTypeMenuItem" - /> - </tippy> - </span> - <!-- <SmartAutoComplete - :source="validContentTypes" - :spellcheck="false" - v-model="contentType" - styles="text-sm" - /> --> - </li> - </ul> - <ul> - <li> - <div class="flex flex-1"> - <span> - <SmartToggle - :on="rawInput" - </SmartToggle> - </span> - </div> - </li> - </ul> - <HttpBodyParameters - v-if="!rawInput" - :bodyParams="bodyParams" - @clear-content="clearContent" - @set-route-query-state="setRouteQueryState" - @remove-request-body-param="removeRequestBodyParam" - @add-request-body-param="addRequestBodyParam" - /> - <HttpRawBody - v-else - :rawParams="rawParams" - :contentType="contentType" - :rawInput="rawInput" - @clear-content="clearContent" - @update-raw-body="updateRawBody" - @update-raw-input=" - updateRawInput = (value) => (rawInput = value) - " - /> - </div> - <div class="flex flex-1"> - <ButtonSecondary - :title="$t('import_curl')" - icon="import_export" - @click.native="showCurlImportModal = !showCurlImportModal" - v-tippy="{ theme: 'tooltip' }" - /> - <ButtonSecondary - @click.native="showCodegenModal = !showCodegenModal" - :disabled="!isValidURL" - v-tippy="{ theme: 'tooltip' }" - :title="$t('show_code')" - icon="code" - /> - </span> - <span> - <ButtonSecondary - @click.native="copyRequest" - ref="copyRequest" - :disabled="!isValidURL" - v-tippy="{ theme: 'tooltip' }" - :title="$t('copy_request_link')" - :icon="navigatorShare ? 'share' : 'content_copy'" - /> - <ButtonSecondary - @click.native="saveRequest" - ref="saveRequest" - :disabled="!isValidURL" - v-tippy="{ theme: 'tooltip' }" - :title="$t('save_to_collections')" - icon="create_new_folder" - /> - <ButtonSecondary - @click.native="clearContent('', $event)" - v-tippy="{ theme: 'tooltip' }" - :title="$t('clear_all')" - ref="clearAll" - icon="clear_all" - /> - </span> - </div> - </AppSection> - <section id="options"> - <SmartTabs> - <SmartTab - :id="'params'" - :label=" - $t('parameters') + - `${ - params.length !== 0 ? ' \xA0 • \xA0 ' + params.length : '' - }` - " - :selected="true" - > - <HttpParameters - :params="params" - @clear-content="clearContent" - @remove-request-param="removeRequestParam" - @add-request-param="addRequestParam" - /> - </SmartTab> - - <SmartTab - :id="'headers'" - :label=" - $t('headers') + - `${ - headers.length !== 0 - ? ' \xA0 • \xA0 ' + headers.length - : '' - }` - " - > - <HttpHeaders - :headers="headers" - @clear-content="clearContent" - @set-route-query-state="setRouteQueryState" - @remove-request-header="removeRequestHeader" - @add-request-header="addRequestHeader" - /> - </SmartTab> - - <SmartTab :id="'authentication'" :label="$t('authentication')"> - <AppSection label="authentication"> - <ul> - <li> - <div class="flex flex-1"> - <label for="auth">{{ $t("authentication") }}</label> - <div> - <ButtonSecondary - @click.native="clearContent('auth', $event)" - v-tippy="{ theme: 'tooltip' }" - :title="$t('clear')" - icon="clear_all" - /> - </div> - </div> - <span class="select-wrapper"> - <select class="select" id="auth" v-model="auth"> - <option>None</option> - <option>Basic Auth</option> - <option>Bearer Token</option> - <option>OAuth 2.0</option> - </select> - </span> - </li> - </ul> - <ul v-if="auth === 'Basic Auth'"> - <li> - <input - class="input" - placeholder="User" - name="http_basic_user" - v-model="httpUser" - /> - </li> - <li> - <input - class="input" - placeholder="Password" - name="http_basic_passwd" - :type="passwordFieldType" - v-model="httpPassword" - /> - </li> - <div> - <li> - <ButtonSecondary - ref="switchVisibility" - @click.native="switchVisibility" - :icon=" - passwordFieldType === 'text' - ? 'visibility' - : 'visibility_off' - " - /> - </li> - </div> - </ul> - <ul v-if="auth === 'Bearer Token' || auth === 'OAuth 2.0'"> - <li> - <div class="flex flex-1"> - <input - class="input" - placeholder="Token" - name="bearer_token" - v-model="bearerToken" - /> - <ButtonSecondary - v-if="auth === 'OAuth 2.0'" - @click.native=" - showTokenListModal = !showTokenListModal - " - v-tippy="{ theme: 'tooltip' }" - :title="$t('use_token')" - icon="open_in_new" - /> - <ButtonSecondary - v-if="auth === 'OAuth 2.0'" - @click.native="showTokenRequest = !showTokenRequest" - v-tippy="{ theme: 'tooltip' }" - :title="$t('get_token')" - icon="vpn_key" - /> - </div> - </li> - </ul> - <div class="flex flex-1"> - <SmartToggle - :on="!URL_EXCLUDES.auth" - @change="setExclude('auth', !$event)" - > - {{ $t("include_in_url") }} - </SmartToggle> - </div> - </AppSection> - - <AppSection - v-if="showTokenRequest" - label="accessTokenRequest" - > - <ul> - <li> - <div class="flex flex-1"> - <label for="token-name">{{ $t("token_name") }}</label> - <div> - <ButtonSecondary - @click.native="showTokenRequestList = true" - v-tippy="{ theme: 'tooltip' }" - :title="$t('manage_token_req')" - icon="library_add" - /> - <ButtonSecondary - @click.native=" - clearContent('access_token', $event) - " - v-tippy="{ theme: 'tooltip' }" - :title="$t('clear')" - icon="clear_all" - /> - <ButtonSecondary - @click.native="showTokenRequest = false" - v-tippy="{ theme: 'tooltip' }" - :title="$t('close')" - icon="close" - /> - </div> - </div> - <input - class="input" - id="token-name" - :placeholder="$t('optional')" - name="token_name" - v-model="accessTokenName" - type="text" - /> - </li> - </ul> - <ul> - <li> - <label for="oidc-discovery-url"> - {{ $t("oidc_discovery_url") }} - </label> - <input - class="input" - :disabled=" - this.authUrl !== '' || this.accessTokenUrl !== '' - " - id="oidc-discovery-url" - name="oidc_discovery_url" - type="url" - v-model="oidcDiscoveryUrl" - placeholder="https://example.com/.well-known/openid-configuration" - /> - </li> - </ul> - <ul> - <li> - <label for="auth-url">{{ $t("auth_url") }}</label> - <input - class="input" - :disabled="this.oidcDiscoveryUrl !== ''" - id="auth-url" - name="auth_url" - type="url" - v-model="authUrl" - placeholder="https://example.com/login/oauth/authorize" - /> - </li> - </ul> - <ul> - <li> - <label for="access-token-url"> - {{ $t("access_token_url") }} - </label> - <input - class="input" - :disabled="this.oidcDiscoveryUrl !== ''" - id="access-token-url" - name="access_token_url" - type="url" - v-model="accessTokenUrl" - placeholder="https://example.com/login/oauth/access_token" - /> - </li> - </ul> - <ul> - <li> - <label for="client-id">{{ $t("client_id") }}</label> - <input - class="input" - id="client-id" - name="client_id" - type="text" - v-model="clientId" - placeholder="Client ID" - /> - </li> - </ul> - <ul> - <li> - <label for="scope">{{ $t("scope") }}</label> - <input - class="input" - id="scope" - name="scope" - type="text" - v-model="scope" - placeholder="e.g. read:org" - /> - </li> - </ul> - <ul> - <li> - <ButtonSecondary - @click.native="handleAccessTokenRequest" - icon="vpn_key" - :label="$t('request_token')" - /> - </li> - </ul> - </AppSection> - </SmartTab> - - <SmartTab - :id="'pre_request_script'" - :label="$t('pre_request_script')" - > - <AppSection v-if="showPreRequestScript" label="preRequest"> - <ul> - <li> - <div class="flex flex-1"> - <label>{{ $t("javascript_code") }}</label> - <div> - <ButtonSecondary - to="https://github.com/hoppscotch/hoppscotch/wiki/Pre-Request-Scripts" - blank - v-tippy="{ theme: 'tooltip' }" - :title="$t('wiki')" - icon="help_outline" - /> - </div> - </div> - <SmartJsEditor - v-model="preRequestScript" - :options="{ - maxLines: '16', - minLines: '8', - fontSize: '15px', - autoScrollEditorIntoView: true, - showPrintMargin: false, - useWorker: false, - }" - styles="rounded-b-lg" - completeMode="pre" - /> - </li> - </ul> - </AppSection> - </SmartTab> - - <SmartTab :id="'tests'" :label="$t('tests')"> - <AppSection v-if="testsEnabled" label="postRequestTests"> - <ul> - <li> - <div class="flex flex-1"> - <label>{{ $t("javascript_code") }}</label> - <div> - <ButtonSecondary - to="https://github.com/hoppscotch/hoppscotch/wiki/Post-Request-Tests" - blank - v-tippy="{ theme: 'tooltip' }" - :title="$t('wiki')" - icon="help_outline" - /> - </div> - </div> - <SmartJsEditor - v-model="testScript" - :options="{ - maxLines: '16', - minLines: '8', - fontSize: '15px', - autoScrollEditorIntoView: true, - showPrintMargin: false, - useWorker: false, - }" - styles="rounded-b-lg" - completeMode="test" - /> - <div v-if="testReports.length !== 0"> - <div class="flex flex-1"> - <label>Test Reports</label> - <div> - <ButtonSecondary - @click.native="clearContent('tests', $event)" - v-tippy="{ theme: 'tooltip' }" - :title="$t('clear')" - icon="clear_all" - /> - </div> - </div> - <div - v-for="(testReport, index) in testReports" - :key="index" - > - <div v-if="testReport.startBlock"> - <hr /> - <h4 class="heading"> - {{ testReport.startBlock }} - </h4> - </div> - <p - v-else-if="testReport.result" - class="flex flex-1 info" - > - <span :class="testReport.styles.class"> - <i class="material-icons"> - {{ testReport.styles.icon }} - </i> - <span> {{ testReport.result }}</span> - <span v-if="testReport.message"> - <label - > • - {{ testReport.message }}</label - > - </span> - </span> - </p> - <div v-else-if="testReport.endBlock"><hr /></div> - </div> - </div> - </li> - </ul> - </AppSection> - </SmartTab> - </SmartTabs> - </section> - </Pane> - <Pane class="overflow-auto"> - <HttpResponse - :response="response" - :active="runningRequest" - ref="response" - /> - </Pane> - </Splitpanes> - </Pane> - <Pane max-size="30" size="25" min-size="20" class="overflow-auto hide-scrollbar"> - <aside class="h-full"> - <SmartTabs styles="sticky z-10 top-0"> - <SmartTab :id="'history'" :label="$t('history')" :selected="true"> - <History - :page="'rest'" - @useHistory="handleUseHistory" - ref="historyComponent" - /> - </SmartTab> - - <SmartTab :id="'collections'" :label="$t('collections')"> - <Collections /> - </SmartTab> - - <SmartTab :id="'env'" :label="$t('environments')"> - <Environments /> - </SmartTab> - </SmartTabs> - </aside> - </Pane> - </Splitpanes> - - <CollectionsSaveRequest - mode="rest" - :show="showSaveRequestModal" - @hide-modal="hideRequestModal" - :editing-request="editRequest" - /> - - <HttpImportCurl - :show="showCurlImportModal" - @hide-modal="showCurlImportModal = false" - @handle-import="handleImport" - /> - - <HttpCodegenModal - :show="showCodegenModal" - :requestTypeProp="requestType" - :requestCode="requestCode" - @hide-modal="showCodegenModal = false" - @set-request-type="setRequestType" - /> - - <HttpTokenList - :show="showTokenListModal" - :tokens="tokens" - @clear-content="clearContent" - @use-oauth-token="useOAuthToken" - @remove-oauth-token="removeOAuthToken" - @hide-modal="showTokenListModal = false" - /> - - <SmartModal - v-if="showTokenRequestList" - @close="showTokenRequestList = false" - > - <template #header> - <h3 class="heading">{{ $t("manage_token_req") }}</h3> - <div> - <ButtonSecondary - @click.native="showTokenRequestList = false" - icon="close" - /> - </div> - </template> - <template #body> - <div class="flex flex-1"> - <label for="token-req-list">{{ $t("token_req_list") }}</label> - <div> - <ButtonSecondary - :disabled="this.tokenReqs.length === 0" - @click.native="showTokenRequestList = false" - v-tippy="{ theme: 'tooltip' }" - :title="$t('use_token_req')" - icon="input" - /> - <ButtonSecondary - :disabled="this.tokenReqs.length === 0" - @click.native="removeOAuthTokenReq" - v-tippy="{ theme: 'tooltip' }" - :title="$t('delete')" - icon="delete" - /> - </div> - </div> - <ul> - <li> - <span class="select-wrapper"> - <select - id="token-req-list" - class="select" - v-model="tokenReqSelect" - :disabled="this.tokenReqs.length === 0" - @change="tokenReqChange($event)" - > - <option - v-for="(req, index) in tokenReqs" - :key="index" - :value="req.name" - > - {{ req.name }} - </option> - </select> - </span> - </li> - </ul> - <label for="token-req-name">{{ $t("token_req_name") }}</label> - <input class="input" v-model="tokenReqName" /> - <label for="token-req-details"> - {{ $t("token_req_details") }} - </label> - <textarea - id="token-req-details" - class="textarea" - readonly - rows="7" - v-model="tokenReqDetails" - ></textarea> - </template> - <template #footer> - <span></span> - <span> - <ButtonPrimary @click.native="addOAuthTokenReq" /> - {{ $t("save_token_req") }} - </span> - </template> - </SmartModal> - </div> -</template> - -<script> -/* eslint-disable */ -import { Splitpanes, Pane } from "splitpanes" -import "splitpanes/dist/splitpanes.css" - -import url from "url" -import querystring from "querystring" -import parseCurlCommand from "~/helpers/curlparser" -import getEnvironmentVariablesFromScript from "~/helpers/preRequest" -import runTestScriptWithVariables from "~/helpers/postwomanTesting" -import parseTemplateString from "~/helpers/templating" -import { tokenRequest, oauthRedirect } from "~/helpers/oauth" -import { cancelRunningRequest, sendNetworkRequest } from "~/helpers/network" -import { - hasPathParams, - addPathParamsToVariables, - getQueryParams, -} from "~/helpers/requestParams" -import { parseUrlAndPath } from "~/helpers/utils/uri" -import { httpValid } from "~/helpers/utils/valid" -import { - knownContentTypes, - isJSONContentType, -} from "~/helpers/utils/contenttypes" -import { generateCodeWithGenerator } from "~/helpers/codegen/codegen" -import { getSettingSubject, applySetting } from "~/newstore/settings" -import { addRESTHistoryEntry } from "~/newstore/history" -import clone from "lodash/clone" - -export default { - components: { Splitpanes, Pane }, - - data() { - return { - showCurlImportModal: false, - showPreRequestScript: true, - testsEnabled: true, - testScript: "// pw.expect('variable').toBe('value');", - preRequestScript: "// pw.env.set('variable', 'value');", - testReports: [], - copyButton: '<i class="material-icons">content_copy</i>', - downloadButton: '<i class="material-icons">save_alt</i>', - doneButton: '<i class="material-icons">done</i>', - showCodegenModal: false, - response: { - status: "", - headers: "", - body: "", - duration: 0, - size: 0, - }, - validContentTypes: knownContentTypes, - paramsWatchEnabled: true, - showTokenListModal: false, - showTokenRequest: false, - showTokenRequestList: false, - showSaveRequestModal: false, - editRequest: {}, - customMethod: false, - files: [], - filenames: "", - navigatorShare: navigator.share, - runningRequest: false, - currentMethodIndex: 0, - methodMenuItems: [ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "CONNECT", - "OPTIONS", - "TRACE", - "PATCH", - "CUSTOM", - ], - } - }, - subscriptions() { - return { - SCROLL_INTO_ENABLED: getSettingSubject("SCROLL_INTO_ENABLED"), - PROXY_ENABLED: getSettingSubject("PROXY_ENABLED"), - URL_EXCLUDES: getSettingSubject("URL_EXCLUDES"), - EXPERIMENTAL_URL_BAR_ENABLED: getSettingSubject( - "EXPERIMENTAL_URL_BAR_ENABLED" - ), - } - }, - watch: { - canListParameters: { - immediate: true, - handler(canListParameters) { - if (canListParameters) { - this.$nextTick(() => { - this.rawInput = Boolean(this.rawParams && this.rawParams !== "{}") - }) - } else { - this.rawInput = true - } - }, - }, - contentType(contentType, oldContentType) { - const getDefaultParams = (contentType) => { - if (isJSONContentType(contentType)) return "{}" - switch (contentType) { - case "application/xml": - return "<?xml version='1.0' encoding='utf-8'?>" - case "text/html": - return "<!doctype html>" - } - return "" - } - if ( - !this.rawParams || - this.rawParams === getDefaultParams(oldContentType) - ) { - this.rawParams = getDefaultParams(contentType) - } - this.setRouteQueryState() - }, - params: { - handler(newValue) { - if (!this.paramsWatchEnabled) { - this.paramsWatchEnabled = true - return - } - let path = this.path - let queryString = getQueryParams(newValue) - .map(({ key, value }) => `${key.trim()}=${value.trim()}`) - .join("&") - queryString = queryString === "" ? "" : `?${encodeURI(queryString)}` - if (path.includes("?")) { - path = path.slice(0, path.indexOf("?")) + queryString - } else { - path = path + queryString - } - this.path = path - this.setRouteQueryState() - }, - deep: true, - }, - selectedRequest(newValue) { - // @TODO: Convert all variables to single request variable - if (!newValue) return - this.uri = newValue.url + newValue.path - this.url = newValue.url - this.path = newValue.path - this.method = newValue.method - this.auth = newValue.auth - this.httpUser = newValue.httpUser - this.httpPassword = newValue.httpPassword - this.passwordFieldType = newValue.passwordFieldType - this.bearerToken = newValue.bearerToken - this.headers = newValue.headers - this.params = newValue.params - this.bodyParams = newValue.bodyParams - this.rawParams = newValue.rawParams - this.rawInput = newValue.rawInput - this.contentType = newValue.contentType - this.requestType = newValue.requestType - if (newValue.preRequestScript) { - this.showPreRequestScript = true - this.preRequestScript = newValue.preRequestScript - } - if (newValue.testScript) { - this.testsEnabled = true - this.testScript = newValue.testScript - } - this.name = newValue.name - }, - method() { - this.contentType = ["POST", "PUT", "PATCH", "DELETE"].includes( - this.method - ) - ? this.contentType - : "application/json" - }, - preRequestScript(val, oldVal) { - this.uri = this.uri - }, - }, - computed: { - /** - * Check content types that can be automatically - * serialized by postwoman. - */ - canListParameters() { - return ( - this.contentType === "application/x-www-form-urlencoded" || - this.contentType === "multipart/form-data" || - isJSONContentType(this.contentType) - ) - }, - uri: { - get() { - return this.$store.state.request.uri - ? this.$store.state.request.uri - : this.url + this.path - }, - set(value) { - this.$store.commit("setState", { value, attribute: "uri" }) - let url = value - if ( - (this.preRequestScript && this.showPreRequestScript) || - hasPathParams(this.params) - ) { - let environmentVariables = getEnvironmentVariablesFromScript( - this.preRequestScript - ) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - url = parseTemplateString(value, environmentVariables) - } - let result = parseUrlAndPath(url) - this.url = result.url - this.path = result.path - }, - }, - url: { - get() { - return this.$store.state.request.url - }, - set(value) { - this.$store.commit("setState", { value, attribute: "url" }) - }, - }, - method: { - get() { - return this.$store.state.request.method - }, - set(value) { - this.$store.commit("setState", { value, attribute: "method" }) - }, - }, - path: { - get() { - return this.$store.state.request.path - }, - set(value) { - this.$store.commit("setState", { value, attribute: "path" }) - }, - }, - name: { - get() { - return this.$store.state.request.name - }, - set(value) { - this.$store.commit("setState", { value, attribute: "name" }) - }, - }, - auth: { - get() { - return this.$store.state.request.auth - }, - set(value) { - this.$store.commit("setState", { value, attribute: "auth" }) - }, - }, - httpUser: { - get() { - return this.$store.state.request.httpUser - }, - set(value) { - this.$store.commit("setState", { value, attribute: "httpUser" }) - }, - }, - httpPassword: { - get() { - return this.$store.state.request.httpPassword - }, - set(value) { - this.$store.commit("setState", { value, attribute: "httpPassword" }) - }, - }, - bearerToken: { - get() { - return this.$store.state.request.bearerToken - }, - set(value) { - this.$store.commit("setState", { value, attribute: "bearerToken" }) - }, - }, - tokens: { - get() { - return this.$store.state.oauth2.tokens - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "tokens" }) - }, - }, - tokenReqs: { - get() { - return this.$store.state.oauth2.tokenReqs - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "tokenReqs" }) - }, - }, - tokenReqSelect: { - get() { - return this.$store.state.oauth2.tokenReqSelect - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "tokenReqSelect" }) - }, - }, - tokenReqName: { - get() { - return this.$store.state.oauth2.tokenReqName - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "tokenReqName" }) - }, - }, - accessTokenName: { - get() { - return this.$store.state.oauth2.accessTokenName - }, - set(value) { - this.$store.commit("setOAuth2", { - value, - attribute: "accessTokenName", - }) - }, - }, - oidcDiscoveryUrl: { - get() { - return this.$store.state.oauth2.oidcDiscoveryUrl - }, - set(value) { - this.$store.commit("setOAuth2", { - value, - attribute: "oidcDiscoveryUrl", - }) - }, - }, - authUrl: { - get() { - return this.$store.state.oauth2.authUrl - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "authUrl" }) - }, - }, - accessTokenUrl: { - get() { - return this.$store.state.oauth2.accessTokenUrl - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "accessTokenUrl" }) - }, - }, - clientId: { - get() { - return this.$store.state.oauth2.clientId - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "clientId" }) - }, - }, - scope: { - get() { - return this.$store.state.oauth2.scope - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "scope" }) - }, - }, - state: { - get() { - return this.$store.state.oauth2.state - }, - set(value) { - this.$store.commit("setOAuth2", { value, attribute: "state" }) - }, - }, - headers: { - get() { - return this.$store.state.request.headers - }, - set(value) { - this.$store.commit("setState", { value, attribute: "headers" }) - }, - }, - params: { - get() { - return this.$store.state.request.params - }, - set(value) { - this.$store.commit("setState", { value, attribute: "params" }) - }, - }, - bodyParams: { - get() { - return this.$store.state.request.bodyParams - }, - set(value) { - this.$store.commit("setState", { value, attribute: "bodyParams" }) - }, - }, - rawParams: { - get() { - return this.$store.state.request.rawParams - }, - set(value) { - this.$store.commit("setState", { value, attribute: "rawParams" }) - // Convert the rawParams to bodyParams format - try { - const valueObj = JSON.parse(value) - const params = Object.keys(valueObj).map((key) => { - if (typeof valueObj[key] !== "function") { - return { - active: true, - key, - value: valueObj[key], - } - } - }) - this.$store.commit("setBodyParams", { params }) - } catch {} - }, - }, - rawInput: { - get() { - return this.$store.state.request.rawInput - }, - set(value) { - this.$store.commit("setState", { value, attribute: "rawInput" }) - }, - }, - requestType: { - get() { - return this.$store.state.request.requestType - }, - set(value) { - this.$store.commit("setState", { value, attribute: "requestType" }) - }, - }, - contentType: { - get() { - return this.$store.state.request.contentType - }, - set(value) { - this.$store.commit("setState", { value, attribute: "contentType" }) - }, - }, - passwordFieldType: { - get() { - return this.$store.state.request.passwordFieldType - }, - set(value) { - this.$store.commit("setState", { - value, - attribute: "passwordFieldType", - }) - }, - }, - selectedRequest() { - return this.$store.state.postwoman.selectedRequest - }, - requestName() { - return this.name - }, - isValidURL() { - // if showPreRequestScript, we cannot determine if a URL is valid because the full string is not known ahead of time - return this.showPreRequestScript || httpValid(this.url) - }, - hasRequestBody() { - return ["POST", "PUT", "PATCH", "DELETE"].includes(this.method) - }, - pathName() { - return this.path.match(/^([^?]*)\??/)[1] - }, - rawRequestBody() { - const { bodyParams, contentType } = this - if (isJSONContentType(contentType)) { - try { - const obj = JSON.parse( - `{${bodyParams - .filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - ) - .filter(({ key }) => !!key) - .map(({ key, value }) => `"${key}": "${value}"`) - .join()}}` - ) - return JSON.stringify(obj, null, 2) - } catch (ex) { - console.log(ex) - this.$toast.clear() - this.$toast.error( - "Parameter value must be a string, switch to Raw input for other formats", - { - icon: "error", - } - ) - return "invalid" - } - } else { - return bodyParams - .filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - ) - .filter(({ key }) => !!key) - .map( - ({ key, value }) => - `${encodeURIComponent(key)}=${encodeURIComponent(value)}` - ) - .join("&") - } - }, - queryString() { - const result = getQueryParams(this.params) - .map( - ({ key, value }) => - `${encodeURIComponent(key)}=${encodeURIComponent(value)}` - ) - .join("&") - return result === "" ? "" : `?${result}` - }, - requestCode() { - let headers = [] - if (this.preRequestScript || hasPathParams(this.params)) { - let environmentVariables = getEnvironmentVariablesFromScript( - this.preRequestScript - ) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - for (let k of this.headers.filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - )) { - const kParsed = parseTemplateString(k.key, environmentVariables) - const valParsed = parseTemplateString(k.value, environmentVariables) - headers.push({ key: kParsed, value: valParsed }) - } - } - - return generateCodeWithGenerator(this.requestType, { - auth: this.auth, - method: this.method, - url: this.url, - pathName: this.pathName, - queryString: this.queryString, - httpUser: this.httpUser, - httpPassword: this.httpPassword, - bearerToken: this.bearerToken, - headers, - rawInput: this.rawInput, - rawParams: this.rawParams, - rawRequestBody: this.rawRequestBody, - contentType: this.contentType, - }) - }, - tokenReqDetails() { - const details = { - oidcDiscoveryUrl: this.oidcDiscoveryUrl, - authUrl: this.authUrl, - accessTokenUrl: this.accessTokenUrl, - clientId: this.clientId, - scope: this.scope, - } - return JSON.stringify(details, null, 2) - }, - }, - methods: { - scrollInto(view) { - this.$refs[view].$el.scrollIntoView({ - behavior: "smooth", - }) - }, - handleUseHistory(entry) { - this.name = entry.name - this.method = entry.method - this.uri = entry.url + entry.path - this.url = entry.url - this.path = entry.path - this.showPreRequestScript = entry.usesPreScripts - this.preRequestScript = entry.preRequestScript - this.auth = entry.auth - this.httpUser = entry.httpUser - this.httpPassword = entry.httpPassword - this.bearerToken = entry.bearerToken - this.headers = entry.headers - this.params = entry.params - this.bodyParams = entry.bodyParams - this.rawParams = entry.rawParams - this.rawInput = entry.rawInput - this.contentType = entry.contentType - this.requestType = entry.requestType - this.testScript = entry.testScript - this.testsEnabled = entry.usesPostScripts - if (this.SCROLL_INTO_ENABLED) this.scrollInto("request") - }, - async makeRequest(auth, headers, requestBody, preRequestScript) { - const requestOptions = { - method: this.method, - url: this.url + this.pathName + this.queryString, - auth, - headers, - data: requestBody, - credentials: true, - } - - if (preRequestScript || hasPathParams(this.params)) { - let environmentVariables = - getEnvironmentVariablesFromScript(preRequestScript) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - requestOptions.url = parseTemplateString( - requestOptions.url, - environmentVariables - ) - if (!(requestOptions.data instanceof FormData)) { - // TODO: Parse env variables for form data too - requestOptions.data = parseTemplateString( - requestOptions.data, - environmentVariables - ) - } - for (let k in requestOptions.headers) { - const kParsed = parseTemplateString(k, environmentVariables) - const valParsed = parseTemplateString( - requestOptions.headers[k], - environmentVariables - ) - delete requestOptions.headers[k] - requestOptions.headers[kParsed] = valParsed - } - } - if (typeof requestOptions.data === "string") { - requestOptions.data = parseTemplateString(requestOptions.data) - } - return await sendNetworkRequest(requestOptions) - }, - cancelRequest() { - cancelRunningRequest() - }, - async sendRequest() { - this.$toast.clear() - if (this.SCROLL_INTO_ENABLED) this.scrollInto("response") - if (!this.isValidURL) { - this.$toast.error(this.$t("url_invalid_format"), { - icon: "error", - }) - return - } - // Start showing the loading bar as soon as possible. - // The nuxt axios module will hide it when the request is made. - this.$nuxt.$loading.start() - this.response = { - duration: 0, - size: 0, - status: this.$t("fetching"), - body: this.$t("loading"), - } - const auth = - this.auth === "Basic Auth" - ? { - username: this.httpUser, - password: this.httpPassword, - } - : null - let headers = {} - let headersObject = {} - Object.keys(headers).forEach((id) => { - if (headers[id].key) headersObject[headers[id].key] = headers[id].value - }) - headers = headersObject - // If the request has a body, we want to ensure Content-Type is sent. - let requestBody - if (this.hasRequestBody) { - requestBody = this.rawInput ? this.rawParams : this.rawRequestBody - Object.assign(headers, { - "Content-Type": `${this.contentType}; charset=utf-8`, - }) - } - requestBody = requestBody ? requestBody.toString() : null - if (this.contentType === "multipart/form-data") { - const formData = new FormData() - for (const bodyParam of this.bodyParams.filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - )) { - if (bodyParam?.value?.[0] instanceof File) { - for (const file of bodyParam.value) { - formData.append(bodyParam.key, file) - } - } else { - formData.append(bodyParam.key, bodyParam.value) - } - } - requestBody = formData - } - // If the request uses a token for auth, we want to make sure it's sent here. - if (this.auth === "Bearer Token" || this.auth === "OAuth 2.0") - headers["Authorization"] = `Bearer ${this.bearerToken}` - headers = Object.assign( - // Clone the app headers object first, we don't want to - // mutate it with the request headers added by default. - Object.assign( - {}, - this.headers.filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - ) - ) - // We make our temporary headers object the source so - // that you can override the added headers if you - // specify them. - // headers - ) - Object.keys(headers).forEach((id) => { - if (headers[id].key) headersObject[headers[id].key] = headers[id].value - }) - headers = headersObject - const startTime = new Date().getTime() - try { - this.runningRequest = true - const payload = await this.makeRequest( - auth, - headers, - requestBody, - this.showPreRequestScript && this.preRequestScript - ) - this.runningRequest = false - const duration = new Date().getTime() - startTime - this.response.duration = duration - this.response.size = payload.headers["content-length"] - ;(() => { - this.response.status = payload.status - this.response.headers = payload.headers - // We don't need to bother parsing JSON, axios already handles it for us! - this.response.body = payload.data - // Addition of an entry to the history component. - const entry = { - name: this.requestName, - status: this.response.status, - date: new Date().toLocaleDateString(), - time: new Date().toLocaleTimeString(), - updatedOn: new Date(), - method: this.method, - url: this.url, - path: this.path, - usesPreScripts: this.showPreRequestScript, - preRequestScript: this.preRequestScript, - duration, - star: false, - auth: this.auth, - httpUser: this.httpUser, - httpPassword: this.httpPassword, - bearerToken: this.bearerToken, - headers: this.headers, - params: this.params, - bodyParams: this.bodyParams, - rawParams: this.rawParams, - rawInput: this.rawInput, - contentType: this.contentType, - requestType: this.requestType, - testScript: this.testScript, - usesPostScripts: this.testsEnabled, - } - - if ( - (this.preRequestScript && this.showPreRequestScript) || - hasPathParams(this.params) - ) { - let environmentVariables = getEnvironmentVariablesFromScript( - this.preRequestScript - ) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - entry.path = parseTemplateString(entry.path, environmentVariables) - entry.url = parseTemplateString(entry.url, environmentVariables) - } - - addRESTHistoryEntry(entry) - })() - } catch (error) { - this.runningRequest = false - // If the error is caused by cancellation, do nothing - if (error === "cancellation") { - this.response.status = this.$t("cancelled") - this.response.body = this.$t("cancelled") - } else { - console.log(error) - const duration = new Date().getTime() - startTime - this.response.duration = duration - this.response.size = Buffer.byteLength(JSON.stringify(error)) - if (error.response) { - this.response.headers = error.response.headers - this.response.status = error.response.status - this.response.body = error.response.data - // Addition of an entry to the history component. - const entry = { - name: this.requestName, - status: this.response.status, - date: new Date().toLocaleDateString(), - time: new Date().toLocaleTimeString(), - updatedOn: new Date(), - method: this.method, - url: this.url, - path: this.path, - usesPreScripts: this.showPreRequestScript, - preRequestScript: this.preRequestScript, - star: false, - auth: this.auth, - httpUser: this.httpUser, - httpPassword: this.httpPassword, - bearerToken: this.bearerToken, - headers: this.headers, - params: this.params, - bodyParams: this.bodyParams, - rawParams: this.rawParams, - rawInput: this.rawInput, - contentType: this.contentType, - requestType: this.requestType, - testScript: this.testScript, - usesPostScripts: this.testsEnabled, - } - - if ( - (this.preRequestScript && this.showPreRequestScript) || - hasPathParams(this.params) - ) { - let environmentVariables = getEnvironmentVariablesFromScript( - this.preRequestScript - ) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - entry.path = parseTemplateString(entry.path, environmentVariables) - entry.url = parseTemplateString(entry.url, environmentVariables) - } - - addRESTHistoryEntry(entry) - return - } else { - this.response.status = error.message - this.response.body = `${error}. ${this.$t("check_console_details")}` - this.$toast.error(`${error} ${this.$t("f12_details")}`, { - icon: "error", - }) - if (!this.PROXY_ENABLED) { - this.$toast.info(this.$t("enable_proxy"), { - icon: "help", - duration: 8000, - action: { - text: this.$t("yes"), - onClick: (e, toastObject) => { - this.$router.push({ path: "/settings" }) - }, - }, - }) - } - } - } - } - // tests - const syntheticResponse = { - status: this.response.status, - body: this.response.body, - headers: this.response.headers, - } - - // Parse JSON body - if ( - syntheticResponse.headers["content-type"] && - isJSONContentType(syntheticResponse.headers["content-type"]) - ) { - try { - syntheticResponse.body = JSON.parse( - new TextDecoder("utf-8").decode( - new Uint8Array(syntheticResponse.body) - ) - ) - } catch (_e) {} - } - - const { testResults } = runTestScriptWithVariables(this.testScript, { - response: syntheticResponse, - }) - this.testReports = testResults - }, - getQueryStringFromPath() { - const pathParsed = url.parse(this.uri) - return pathParsed.query ? pathParsed.query : "" - }, - queryStringToArray(queryString) { - const queryParsed = querystring.parse(queryString) - return Object.keys(queryParsed).map((key) => ({ - key, - value: queryParsed[key], - active: true, - })) - }, - pathInputHandler() { - if (this.uri.includes("?")) { - const queryString = this.getQueryStringFromPath() - let environmentVariables = getEnvironmentVariablesFromScript( - this.preRequestScript - ) - environmentVariables = addPathParamsToVariables( - this.params, - environmentVariables - ) - const params = this.queryStringToArray(queryString) - let parsedParams = [] - for (let k of params.filter((item) => - item.hasOwnProperty("active") ? item.active == true : true - )) { - const kParsed = parseTemplateString(k.key, environmentVariables) - const valParsed = parseTemplateString(k.value, environmentVariables) - parsedParams.push({ key: kParsed, value: valParsed, active: true }) - } - this.paramsWatchEnabled = false - this.params = parsedParams - } - }, - addRequestHeader() { - this.$store.commit("addHeaders", { - key: "", - value: "", - active: true, - }) - return false - }, - removeRequestHeader(index) { - // .slice() gives us an entirely new array rather than giving us just the reference - const oldHeaders = this.headers.slice() - this.$store.commit("removeHeaders", index) - this.$toast.error(this.$t("deleted"), { - icon: "delete", - action: { - text: this.$t("undo"), - onClick: (e, toastObject) => { - this.headers = oldHeaders - toastObject.remove() - }, - }, - }) - }, - addRequestParam() { - this.$store.commit("addParams", { - key: "", - value: "", - type: "query", - active: true, - }) - return false - }, - removeRequestParam(index) { - // .slice() gives us an entirely new array rather than giving us just the reference - const oldParams = this.params.slice() - this.$store.commit("removeParams", index) - this.$toast.error(this.$t("deleted"), { - icon: "delete", - action: { - text: this.$t("undo"), - onClick: (e, toastObject) => { - this.params = oldParams - toastObject.remove() - }, - }, - }) - }, - addRequestBodyParam() { - this.$store.commit("addBodyParams", { key: "", value: "", active: true }) - return false - }, - removeRequestBodyParam(index) { - // .slice() gives us an entirely new array rather than giving us just the reference - const oldBodyParams = this.bodyParams.slice() - this.$store.commit("removeBodyParams", index) - this.$toast.error(this.$t("deleted"), { - icon: "delete", - action: { - text: this.$t("undo"), - onClick: (e, toastObject) => { - this.bodyParams = oldBodyParams - toastObject.remove() - }, - }, - }) - }, - copyRequest() { - if (navigator.share) { - const time = new Date().toLocaleTimeString() - const date = new Date().toLocaleDateString() - navigator - .share({ - title: "Hoppscotch", - text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`, - url: window.location.href, - }) - .then(() => {}) - .catch(() => {}) - } else { - const dummy = document.createElement("input") - document.body.appendChild(dummy) - dummy.value = window.location.href - dummy.select() - document.execCommand("copy") - document.body.removeChild(dummy) - this.$refs.copyRequest.innerHTML = this.doneButton - this.$toast.info(this.$t("copied_to_clipboard"), { - icon: "done", - }) - setTimeout( - () => (this.$refs.copyRequest.innerHTML = this.copyButton), - 1000 - ) - } - }, - setRouteQueryState() { - const flat = (key) => (this[key] !== "" ? `${key}=${this[key]}&` : "") - const deep = (key) => { - const haveItems = [...this[key]].length - if (haveItems && this[key]["value"] !== "") { - // Exclude files fro query params - const filesRemoved = this[key].filter( - (item) => !(item?.value?.[0] instanceof File) - ) - return `${key}=${JSON.stringify(filesRemoved)}&` - } - return "" - } - let flats = [ - "method", - "url", - "path", - !this.URL_EXCLUDES.auth ? "auth" : null, - !this.URL_EXCLUDES.httpUser ? "httpUser" : null, - !this.URL_EXCLUDES.httpPassword ? "httpPassword" : null, - !this.URL_EXCLUDES.bearerToken ? "bearerToken" : null, - "contentType", - ] - .filter((item) => item !== null) - .map((item) => flat(item)) - const deeps = ["headers", "params"].map((item) => deep(item)) - const bodyParams = this.rawInput - ? [flat("rawParams")] - : [deep("bodyParams")] - history.replaceState( - window.location.href, - "", - `${this.$router.options.base}?${encodeURI( - flats.concat(deeps, bodyParams).join("").slice(0, -1) - )}` - ) - }, - setRouteQueries(queries) { - if (typeof queries !== "object") - throw new Error("Route query parameters must be a Object") - for (const key in queries) { - if (["headers", "params", "bodyParams"].includes(key)) - this[key] = JSON.parse(decodeURI(encodeURI(queries[key]))) - if (key === "rawParams") { - this.rawInput = true - this.rawParams = queries["rawParams"] - } else if (typeof this[key] === "string") { - this[key] = queries[key] - } - } - }, - // observeRequestButton() { - // const requestElement = this.$refs.request - // const sendButtonElement = this.$refs.sendButton - // const observer = new IntersectionObserver( - // (entries, observer) => { - // entries.forEach(({ isIntersecting }) => { - // if (isIntersecting) sendButtonElement.classList.remove("show") - // // The button should float when it is no longer visible on screen. - // // This is done by adding the show class to the button. - // else sendButtonElement.classList.add("show") - // }) - // }, - // { - // rootMargin: "0px", - // threshold: [0], - // } - // ) - // observer.observe(requestElement) - // }, - handleImport() { - const { value: text } = document.getElementById("import-curl") - try { - const parsedCurl = parseCurlCommand(text) - const { origin, pathname } = new URL( - parsedCurl.url.replace(/"/g, "").replace(/'/g, "") - ) - this.url = origin - this.path = pathname - this.uri = this.url + this.path - this.headers = [] - if (parsedCurl.query) { - for (const key of Object.keys(parsedCurl.query)) { - this.$store.commit("addParams", { - key, - value: parsedCurl.query[key], - type: "query", - active: true, - }) - } - } - if (parsedCurl.headers) { - for (const key of Object.keys(parsedCurl.headers)) { - this.$store.commit("addHeaders", { - key, - value: parsedCurl.headers[key], - }) - } - } - this.method = parsedCurl.method.toUpperCase() - if (parsedCurl["data"]) { - this.rawInput = true - this.rawParams = parsedCurl["data"] - } - this.showCurlImportModal = false - } catch (error) { - this.showCurlImportModal = false - this.$toast.error(this.$t("curl_invalid_format"), { - icon: "error", - }) - } - }, - switchVisibility() { - this.passwordFieldType = - this.passwordFieldType === "password" ? "text" : "password" - }, - clearContent(name, { target }) { - switch (name) { - case "bodyParams": - this.bodyParams = [] - this.files = [] - break - case "rawParams": - this.rawParams = "{}" - break - case "parameters": - this.params = [] - break - case "auth": - this.auth = "None" - this.httpUser = "" - this.httpPassword = "" - this.bearerToken = "" - this.showTokenRequest = false - this.tokens = [] - this.tokenReqs = [] - break - case "access_token": - this.accessTokenName = "" - this.oidcDiscoveryUrl = "" - this.authUrl = "" - this.accessTokenUrl = "" - this.clientId = "" - this.scope = "" - break - case "headers": - this.headers = [] - break - case "tests": - this.testReports = [] - break - case "tokens": - this.tokens = [] - break - default: - this.method = "GET" - this.url = "https://httpbin.org" - this.path = "/get" - this.uri = this.url + this.path - this.name = "Untitled request" - this.bodyParams = [] - this.rawParams = "{}" - this.files = [] - this.params = [] - this.auth = "None" - this.httpUser = "" - this.httpPassword = "" - this.bearerToken = "" - this.showTokenRequest = false - this.tokens = [] - this.tokenReqs = [] - this.accessTokenName = "" - this.oidcDiscoveryUrl = "" - this.authUrl = "" - this.accessTokenUrl = "" - this.clientId = "" - this.scope = "" - this.headers = [] - this.testReports = [] - } - target.innerHTML = this.doneButton - this.$toast.info(this.$t("cleared"), { - icon: "clear_all", - }) - setTimeout( - () => (target.innerHTML = '<i class="material-icons">clear_all</i>'), - 1000 - ) - }, - saveRequest() { - let urlAndPath = parseUrlAndPath(this.uri) - this.editRequest = { - url: decodeURI(urlAndPath.url), - path: decodeURI(urlAndPath.path), - method: this.method, - auth: this.auth, - httpUser: this.httpUser, - httpPassword: this.httpPassword, - passwordFieldType: this.passwordFieldType, - bearerToken: this.bearerToken, - headers: this.headers, - params: this.params, - bodyParams: this.bodyParams, - rawParams: this.rawParams, - rawInput: this.rawInput, - contentType: this.contentType, - requestType: this.requestType, - preRequestScript: - this.showPreRequestScript == true ? this.preRequestScript : null, - testScript: this.testsEnabled == true ? this.testScript : null, - name: this.requestName, - } - this.showSaveRequestModal = true - }, - hideRequestModal() { - this.showSaveRequestModal = false - this.editRequest = {} - }, - setExclude(excludedField, excluded) { - const update = clone(this.URL_EXCLUDES) - - if (excludedField === "auth") { - update.auth = excluded - update.httpUser = excluded - update.httpPassword = excluded - update.bearerToken = excluded - } else { - update[excludedField] = excluded - } - - applySetting("URL_EXCLUDES", update) - - this.setRouteQueryState() - }, - updateRawBody(rawParams) { - this.rawParams = rawParams - }, - async handleAccessTokenRequest() { - if ( - this.oidcDiscoveryUrl === "" && - (this.authUrl === "" || this.accessTokenUrl === "") - ) { - this.$toast.error(this.$t("complete_config_urls"), { - icon: "error", - }) - return - } - try { - const tokenReqParams = { - grantType: "code", - oidcDiscoveryUrl: this.oidcDiscoveryUrl, - authUrl: this.authUrl, - accessTokenUrl: this.accessTokenUrl, - clientId: this.clientId, - scope: this.scope, - } - await tokenRequest(tokenReqParams) - } catch (e) { - this.$toast.error(e, { - icon: "code", - }) - } - }, - async oauthRedirectReq() { - const tokenInfo = await oauthRedirect() - if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) { - this.bearerToken = tokenInfo.access_token - this.addOAuthToken({ - name: this.accessTokenName, - value: tokenInfo.access_token, - }) - } - }, - addOAuthToken({ name, value }) { - this.$store.commit("addOAuthToken", { - name, - value, - }) - return false - }, - removeOAuthToken(index) { - const oldTokens = this.tokens.slice() - this.$store.commit("removeOAuthToken", index) - this.$toast.error(this.$t("deleted"), { - icon: "delete", - action: { - text: this.$t("undo"), - onClick: (e, toastObject) => { - this.tokens = oldTokens - toastObject.remove() - }, - }, - }) - }, - useOAuthToken(value) { - this.bearerToken = value - this.showTokenListModal = false - }, - addOAuthTokenReq() { - try { - const name = this.tokenReqName - const details = JSON.parse(this.tokenReqDetails) - this.$store.commit("addOAuthTokenReq", { - name, - details, - }) - this.$toast.info(this.$t("token_request_saved")) - this.showTokenRequestList = false - } catch (e) { - this.$toast.error(e, { - icon: "code", - }) - } - }, - removeOAuthTokenReq(index) { - const oldTokenReqs = this.tokenReqs.slice() - const targetReqIndex = this.tokenReqs.findIndex( - ({ name }) => name === this.tokenReqName - ) - if (targetReqIndex < 0) return - this.$store.commit("removeOAuthTokenReq", targetReqIndex) - this.$toast.error(this.$t("deleted"), { - icon: "delete", - action: { - text: this.$t("undo"), - onClick: (e, toastObject) => { - this.tokenReqs = oldTokenReqs - toastObject.remove() - }, - }, - }) - }, - tokenReqChange({ target }) { - const { details, name } = this.tokenReqs.find( - ({ name }) => name === target.value - ) - const { oidcDiscoveryUrl, authUrl, accessTokenUrl, clientId, scope } = - details - this.tokenReqName = name - this.oidcDiscoveryUrl = oidcDiscoveryUrl - this.authUrl = authUrl - this.accessTokenUrl = accessTokenUrl - this.clientId = clientId - this.scope = scope - }, - setRequestType(val) { - this.requestType = val - }, - }, - async mounted() { - // this.observeRequestButton() - this._keyListener = function (e) { - if (e.key === "g" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - if (!this.runningRequest) { - this.sendRequest() - } else { - this.cancelRequest() - } - } - if (e.key === "s" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - this.saveRequest() - } - if (e.key === "k" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - this.copyRequest() - } - if (e.key === "i" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - this.$refs.clearAll.click() - } - if ((e.key === "g" || e.key === "G") && e.altKey) { - this.method = "GET" - } - if ((e.key === "h" || e.key === "H") && e.altKey) { - this.method = "HEAD" - } - if ((e.key === "p" || e.key === "P") && e.altKey) { - this.method = "POST" - } - if ((e.key === "u" || e.key === "U") && e.altKey) { - this.method = "PUT" - } - if ((e.key === "x" || e.key === "X") && e.altKey) { - this.method = "DELETE" - } - if (e.key == "ArrowUp" && e.altKey && this.currentMethodIndex > 0) { - this.method = - this.methodMenuItems[ - --this.currentMethodIndex % this.methodMenuItems.length - ] - } else if ( - e.key == "ArrowDown" && - e.altKey && - this.currentMethodIndex < 9 - ) { - this.method = - this.methodMenuItems[ - ++this.currentMethodIndex % this.methodMenuItems.length - ] - } - } - document.addEventListener("keydown", this._keyListener.bind(this)) - await this.oauthRedirectReq() - }, - created() { - if (Object.keys(this.$route.query).length) - this.setRouteQueries(this.$route.query) - this.$watch( - (vm) => [ - vm.name, - vm.method, - vm.url, - vm.auth, - vm.path, - vm.httpUser, - vm.httpPassword, - vm.bearerToken, - vm.headers, - vm.params, - vm.bodyParams, - vm.contentType, - vm.rawParams, - ], - (val) => { - this.setRouteQueryState() - } - ) - }, - beforeDestroy() { - document.removeEventListener("keydown", this._keyListener) - }, -} -</script> diff --git a/pages/graphql.vue b/pages/graphql.vue index c8447daef7356b27e618f0e43e1e447274538319..e7e6aa3f8e84909b800041130f4d4d1ff6d79833 100644 --- a/pages/graphql.vue +++ b/pages/graphql.vue @@ -343,6 +343,7 @@ px-4 py-3 text-xs flex flex-1 + font-medium bg-primaryLight focus:outline-none " @@ -420,19 +421,32 @@ /> </SmartTab> </div> </SmartTabs> + <AppSection label="endpoint"> <div> - value: header.hasOwnProperty('active') +<template> v-if=" queryFields.length === 0 && mutationFields.length === 0 && subscriptionFields.length === 0 && graphqlTypes.length === 0 " + class=" + flex + items-center + text-secondaryLight + flex-col + p-4 + justify-center + " > + <i class="material-icons opacity-50 pb-2">description</i> + <span class="text-xs"> + <AppSection label="endpoint"> @click.native="onPollSchemaClick" + <input <template> <Splitpanes vertical :dbl-click-splitter="false"> - <div> + <Splitpanes vertical :dbl-click-splitter="false"> </AppSection> </SmartTab> diff --git a/pages/home.vue b/pages/home.vue new file mode 100644 index 0000000000000000000000000000000000000000..9d8b63a9f39386642504c5b8942251a2df08d168 --- /dev/null +++ b/pages/home.vue @@ -0,0 +1,10 @@ +<template> + <div class="flex flex-col space-y-16"> + <LandingHero /> + <LandingStats /> + <LandingUsers /> + <LandingFeatures /> + <LandingCTA /> + <LandingFooter /> + </div> +</template> diff --git a/pages/index.vue b/pages/index.vue index 9d8b63a9f39386642504c5b8942251a2df08d168..0022ef8cfaaab5429a2458487676d7a473f14697 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,10 +1,2291 @@ <template> + <!-- eslint-disable --> +<template> <div class="flex flex-col space-y-16"> +<template> <LandingHero /> +<template> <LandingStats /> +<template> <LandingUsers /> +<template> <LandingFeatures /> +<template> <LandingCTA /> +<template> <LandingFooter /> +<template> </div> + <tippy + interactive + ref="options" + tabindex="-1" + trigger="click" + theme="popover" + arrow + > + <template #trigger> + <input + id="method" + class=" + flex + rounded-l-lg + bg-primaryLight + font-mono + w-32 + px-4 + py-2 + truncate + font-semibold + border border-divider + transition + focus:outline-none focus:border-accent + " + v-model="method" + :readonly="!customMethod" + autofocus + /> + </template> + <SmartItem + v-for="(methodMenuItem, index) in methodMenuItems" + :key="`method-${index}`" + @click.native=" + customMethod = methodMenuItem == 'CUSTOM' ? true : false + method = methodMenuItem + $refs.options.tippy().hide() + " + :label="methodMenuItem" + /> + </tippy> + </span> + </div> + <div class="flex-1 inline-flex"> + <input + v-if="!EXPERIMENTAL_URL_BAR_ENABLED" + :class="{ error: !isValidURL }" + class=" + w-full + font-mono font-semibold + truncate + px-4 + py-2 + border border-divider + bg-primaryLight + transition + focus:outline-none focus:border-accent + " + @keyup.enter="isValidURL ? sendRequest() : null" + id="url" + name="url" + type="text" + v-model="uri" + spellcheck="false" + @input="pathInputHandler" + :placeholder="$t('url')" + /> + <SmartUrlField v-model="uri" v-else /> + </div> + <div class="flex"> + <span + id="send" + :disabled="!isValidURL" + @click="sendRequest" + v-if="!runningRequest" + class=" + px-4 + py-2 + border border-accent + font-mono + flex + items-center + justify-center + truncate + font-semibold + bg-accent + text-white + " + > + {{ $t("send") }} + </span> + <span + id="cancel" + @click="cancelRequest" </template> + <LandingFeatures /> + class=" + px-4 + py-2 + border border-accent + font-mono + flex + items-center + justify-center + truncate + font-semibold + bg-accent + text-white + " + > + {{ $t("cancel") }} + </span> + <tippy + ref="sendOptions" + interactive + tabindex="-1" + trigger="click" + theme="popover" + arrow + > + <template #trigger> + <span + class=" + px-1 + py-2 + border border-accent + font-mono + flex + items-center + justify-center + truncate + font-semibold + bg-accent + text-white + rounded-r-lg + " + > + <i class="material-icons">keyboard_arrow_down</i> + </span> + </template> + <SmartItem + :label="$t('import_curl')" + icon="import_export" + @click.native=" + showCurlImportModal = !showCurlImportModal + $refs.sendOptions.tippy().hide() + " + /> + <SmartItem + @click.native=" + showCodegenModal = !showCodegenModal + $refs.sendOptions.tippy().hide() + " + :disabled="!isValidURL" + :label="$t('show_code')" + icon="code" + /> + <SmartItem + @click.native=" + clearContent('', $event) + $refs.sendOptions.tippy().hide() + " + :label="$t('clear_all')" + ref="clearAll" + icon="clear_all" + /> + </tippy> + <span + class=" + ml-4 + px-4 + py-2 + border border-divider + font-mono + flex + items-center + justify-center + truncate + font-semibold + rounded-l-lg + " + > + Save + </span> + <tippy + ref="saveOptions" + interactive + tabindex="-1" + trigger="click" + theme="popover" + arrow + > + <template #trigger> + <span + class=" + px-1 + py-2 + border border-divider + font-mono + flex + items-center + justify-center + truncate + font-semibold + rounded-r-lg + " + > + <i class="material-icons">keyboard_arrow_down</i> + </span> + </template> + <SmartItem :description="$t('token_req_name')" /> + <input + id="request-name" + name="request-name" + type="text" + v-model="name" + class="input text-sm" + /> + <SmartItem + @click.native=" + copyRequest + $refs.saveOptions.tippy().hide() + " + ref="copyRequest" + :disabled="!isValidURL" + :label="$t('copy_request_link')" + :icon="navigatorShare ? 'share' : 'content_copy'" + /> + <SmartItem + @click.native=" + saveRequest + $refs.saveOptions.tippy().hide() + " + ref="saveRequest" + :disabled="!isValidURL" + :label="$t('save_to_collections')" + icon="create_new_folder" + /> + </tippy> + </div> + </div> + <SmartTabs styles="sticky top-62px z-10"> + <SmartTab + :id="'params'" + :label=" + $t('parameters') + + `${ + params.length !== 0 ? ' \xA0 • \xA0 ' + params.length : '' + }` + " + :selected="true" + > + <HttpParameters + :params="params" + @clear-content="clearContent" + @remove-request-param="removeRequestParam" + @add-request-param="addRequestParam" + /> + </SmartTab> + + <SmartTab + :id="'bodyParams'" + :label=" + $t('body') + + `${ + bodyParams.length !== 0 + ? ' \xA0 • \xA0 ' + bodyParams.length + : '' + }` + " + > + <ul> + <li> + <label for="contentType" class="text-sm">{{ + $t("content_type") + }}</label> + <span class="select-wrapper"> + <tippy + interactive + ref="contentTypeOptions" + tabindex="-1" + trigger="click" + theme="popover" + arrow + > + <template #trigger> + <input + id="contentType" + class="input" + v-model="contentType" + readonly + /> + </template> + <SmartItem + v-for="( + contentTypeMenuItem, index + ) in validContentTypes" + :key="`content-type-${index}`" + @click.native=" + contentType = contentTypeMenuItem + $refs.contentTypeOptions.tippy().hide() + " + :label="contentTypeMenuItem" + /> + </tippy> + </span> + <!-- <SmartAutoComplete + :source="validContentTypes" + :spellcheck="false" + v-model="contentType" + styles="text-sm" + /> --> + </li> + </ul> + <ul> + <li> + <div class="flex flex-1"> + <span> + <SmartToggle :on="rawInput" /> + </span> + </div> + </li> + </ul> + <HttpBodyParameters + v-if="!rawInput" + :bodyParams="bodyParams" + @clear-content="clearContent" + @set-route-query-state="setRouteQueryState" + @remove-request-body-param="removeRequestBodyParam" + @add-request-body-param="addRequestBodyParam" + /> + <HttpRawBody + v-else + :rawParams="rawParams" + :contentType="contentType" + :rawInput="rawInput" + @clear-content="clearContent" + @update-raw-body="updateRawBody" + @update-raw-input=" + updateRawInput = (value) => (rawInput = value) + " + /> + </SmartTab> + + <SmartTab + :id="'headers'" + :label=" + $t('headers') + + `${ + headers.length !== 0 ? ' \xA0 • \xA0 ' + headers.length : '' + }` + " + > + <HttpHeaders + :headers="headers" + @clear-content="clearContent" + @set-route-query-state="setRouteQueryState" + @remove-request-header="removeRequestHeader" + @add-request-header="addRequestHeader" + /> + </SmartTab> + + <SmartTab :id="'authentication'" :label="$t('authentication')"> + <AppSection label="authentication"> + <ul> + <li> + <div class="flex flex-1"> + <label for="auth">{{ $t("authentication") }}</label> + <div> + <ButtonSecondary + @click.native="clearContent('auth', $event)" + v-tippy="{ theme: 'tooltip' }" + :title="$t('clear')" + icon="clear_all" + /> + </div> + </div> + <span class="select-wrapper"> + <select class="select" id="auth" v-model="auth"> + <option>None</option> + <option>Basic Auth</option> + <option>Bearer Token</option> + <option>OAuth 2.0</option> + </select> + </span> + </li> + </ul> + <ul v-if="auth === 'Basic Auth'"> + <li> + <input + class="input" + placeholder="User" + name="http_basic_user" + v-model="httpUser" + /> + </li> + <li> + <input + class="input" + placeholder="Password" + name="http_basic_passwd" + :type="passwordFieldType" + v-model="httpPassword" + /> + </li> + <div> + <li> + <ButtonSecondary + ref="switchVisibility" + @click.native="switchVisibility" + :icon=" + passwordFieldType === 'text' + ? 'visibility' + : 'visibility_off' + " + /> + </li> + </div> + </ul> + <ul v-if="auth === 'Bearer Token' || auth === 'OAuth 2.0'"> + <li> + <div class="flex flex-1"> + <input + class="input" + placeholder="Token" + name="bearer_token" + v-model="bearerToken" + /> + <ButtonSecondary + v-if="auth === 'OAuth 2.0'" + @click.native=" + showTokenListModal = !showTokenListModal + " + v-tippy="{ theme: 'tooltip' }" + :title="$t('use_token')" + icon="open_in_new" + /> + <ButtonSecondary + v-if="auth === 'OAuth 2.0'" + @click.native="showTokenRequest = !showTokenRequest" + v-tippy="{ theme: 'tooltip' }" + :title="$t('get_token')" + icon="vpn_key" + /> + </div> + </li> + </ul> + <div class="flex flex-1"> + <SmartToggle + :on="!URL_EXCLUDES.auth" + @change="setExclude('auth', !$event)" + > + {{ $t("include_in_url") }} + </SmartToggle> + </div> + </AppSection> + + <AppSection v-if="showTokenRequest" label="accessTokenRequest"> + <ul> + <li> + <div class="flex flex-1"> + <label for="token-name">{{ $t("token_name") }}</label> + <div> + <ButtonSecondary + @click.native="showTokenRequestList = true" + v-tippy="{ theme: 'tooltip' }" + :title="$t('manage_token_req')" + icon="library_add" + /> + <ButtonSecondary + @click.native="clearContent('access_token', $event)" + v-tippy="{ theme: 'tooltip' }" + :title="$t('clear')" + icon="clear_all" + /> + <ButtonSecondary + @click.native="showTokenRequest = false" + v-tippy="{ theme: 'tooltip' }" + :title="$t('close')" + icon="close" + /> + </div> + </div> + <input + class="input" + id="token-name" + :placeholder="$t('optional')" + name="token_name" + v-model="accessTokenName" + type="text" + /> + </li> + </ul> + <ul> + <li> + <label for="oidc-discovery-url"> + {{ $t("oidc_discovery_url") }} + </label> + <input + class="input" + :disabled=" + this.authUrl !== '' || this.accessTokenUrl !== '' + " + id="oidc-discovery-url" + name="oidc_discovery_url" + type="url" + v-model="oidcDiscoveryUrl" + placeholder="https://example.com/.well-known/openid-configuration" + /> + </li> + </ul> + <ul> + <li> + <label for="auth-url">{{ $t("auth_url") }}</label> + <input + class="input" + :disabled="this.oidcDiscoveryUrl !== ''" + id="auth-url" + name="auth_url" + type="url" + v-model="authUrl" + placeholder="https://example.com/login/oauth/authorize" + /> + </li> + </ul> + <ul> + <li> + <label for="access-token-url"> + {{ $t("access_token_url") }} + </label> + <input + class="input" + :disabled="this.oidcDiscoveryUrl !== ''" + id="access-token-url" + name="access_token_url" + type="url" + v-model="accessTokenUrl" + placeholder="https://example.com/login/oauth/access_token" + /> + </li> + </ul> + <ul> + <li> + <label for="client-id">{{ $t("client_id") }}</label> + <input + class="input" + id="client-id" + name="client_id" + type="text" + v-model="clientId" + placeholder="Client ID" + /> + </li> + </ul> + <ul> + <li> + <label for="scope">{{ $t("scope") }}</label> + <input + class="input" + id="scope" + name="scope" + type="text" + v-model="scope" + placeholder="e.g. read:org" + /> + </li> + </ul> + <ul> + <li> + <ButtonSecondary + @click.native="handleAccessTokenRequest" + icon="vpn_key" + :label="$t('request_token')" + /> + </li> + </ul> + </AppSection> + </SmartTab> + + <SmartTab + :id="'pre_request_script'" + :label="$t('pre_request_script')" + > + <AppSection v-if="showPreRequestScript" label="preRequest"> + <ul> + <li> + <div class="flex flex-1"> + <label>{{ $t("javascript_code") }}</label> + <div> + <ButtonSecondary + to="https://github.com/hoppscotch/hoppscotch/wiki/Pre-Request-Scripts" + blank + v-tippy="{ theme: 'tooltip' }" + :title="$t('wiki')" + icon="help_outline" + /> + </div> + </div> + <SmartJsEditor + v-model="preRequestScript" + :options="{ + maxLines: '16', + minLines: '8', + fontSize: '15px', + autoScrollEditorIntoView: true, + showPrintMargin: false, + useWorker: false, + }" + styles="rounded-b-lg" + completeMode="pre" + /> + </li> + </ul> + </AppSection> + </SmartTab> + + <SmartTab :id="'tests'" :label="$t('tests')"> + <AppSection v-if="testsEnabled" label="postRequestTests"> + <ul> + <li> + <div class="flex flex-1"> + <label>{{ $t("javascript_code") }}</label> + <div> + <ButtonSecondary + to="https://github.com/hoppscotch/hoppscotch/wiki/Post-Request-Tests" + blank + v-tippy="{ theme: 'tooltip' }" + :title="$t('wiki')" + icon="help_outline" + /> + </div> + </div> + <SmartJsEditor + v-model="testScript" + :options="{ + maxLines: '16', + minLines: '8', + fontSize: '15px', + autoScrollEditorIntoView: true, + showPrintMargin: false, + useWorker: false, + }" + styles="rounded-b-lg" + completeMode="test" + /> + <div v-if="testReports.length !== 0"> + <div class="flex flex-1"> + <label>Test Reports</label> + <div> + <ButtonSecondary + @click.native="clearContent('tests', $event)" + v-tippy="{ theme: 'tooltip' }" + :title="$t('clear')" + icon="clear_all" + /> + </div> + </div> + <div + v-for="(testReport, index) in testReports" + :key="index" + > + <div v-if="testReport.startBlock"> + <hr /> + <h4 class="heading"> + {{ testReport.startBlock }} + </h4> + </div> + <p + v-else-if="testReport.result" + class="flex flex-1 info" + > + <span :class="testReport.styles.class"> + <i class="material-icons"> + {{ testReport.styles.icon }} + </i> + <span> {{ testReport.result }}</span> + <span v-if="testReport.message"> + <label + > • + {{ testReport.message }}</label + > + </span> + </span> + </p> + <div v-else-if="testReport.endBlock"><hr /></div> + </div> + </div> + </li> + </ul> + </AppSection> + </SmartTab> + </SmartTabs> + </Pane> + <Pane class="overflow-auto"> + <HttpResponse + :response="response" + :active="runningRequest" + ref="response" + /> + </Pane> + </Splitpanes> + </Pane> + <Pane + max-size="30" + size="25" + min-size="20" + class="overflow-auto hide-scrollbar" + > + <aside class="h-full"> + <SmartTabs styles="sticky z-10 top-0"> + <SmartTab :id="'history'" :label="$t('history')" :selected="true"> + <History + :page="'rest'" + @useHistory="handleUseHistory" + ref="historyComponent" + /> + </SmartTab> + + <SmartTab :id="'collections'" :label="$t('collections')"> + <Collections /> + </SmartTab> + + <SmartTab :id="'env'" :label="$t('environments')"> + <Environments /> + </SmartTab> + </SmartTabs> + </aside> + </Pane> + </Splitpanes> + + <CollectionsSaveRequest + mode="rest" + :show="showSaveRequestModal" + @hide-modal="hideRequestModal" + :editing-request="editRequest" + /> + + <HttpImportCurl + :show="showCurlImportModal" + @hide-modal="showCurlImportModal = false" + @handle-import="handleImport" + /> + + <HttpCodegenModal + :show="showCodegenModal" + :requestTypeProp="requestType" + :requestCode="requestCode" + @hide-modal="showCodegenModal = false" + @set-request-type="setRequestType" + /> + + <HttpTokenList + :show="showTokenListModal" + :tokens="tokens" + @clear-content="clearContent" + @use-oauth-token="useOAuthToken" + @remove-oauth-token="removeOAuthToken" + @hide-modal="showTokenListModal = false" + /> + + <SmartModal + v-if="showTokenRequestList" + @close="showTokenRequestList = false" + > + <template #header> + <h3 class="heading">{{ $t("manage_token_req") }}</h3> + <div> + <ButtonSecondary + @click.native="showTokenRequestList = false" + icon="close" + /> + </div> + </template> + <template #body> + <div class="flex flex-1"> + <label for="token-req-list">{{ $t("token_req_list") }}</label> + <div> + <ButtonSecondary + :disabled="this.tokenReqs.length === 0" + @click.native="showTokenRequestList = false" + v-tippy="{ theme: 'tooltip' }" + :title="$t('use_token_req')" + icon="input" + /> + <ButtonSecondary + :disabled="this.tokenReqs.length === 0" + @click.native="removeOAuthTokenReq" + v-tippy="{ theme: 'tooltip' }" + :title="$t('delete')" + icon="delete" + /> + </div> + </div> + <ul> + <li> + <span class="select-wrapper"> + <select + id="token-req-list" + class="select" + v-model="tokenReqSelect" + :disabled="this.tokenReqs.length === 0" + @change="tokenReqChange($event)" + > + <option + v-for="(req, index) in tokenReqs" + :key="index" + :value="req.name" + > + {{ req.name }} + </option> + </select> + </span> + </li> + </ul> + <label for="token-req-name">{{ $t("token_req_name") }}</label> + <input class="input" v-model="tokenReqName" /> + <label for="token-req-details"> + {{ $t("token_req_details") }} + </label> + <textarea + id="token-req-details" + class="textarea" + readonly + rows="7" + v-model="tokenReqDetails" + ></textarea> + </template> + <template #footer> + <span></span> + <span> + <ButtonPrimary @click.native="addOAuthTokenReq" /> + {{ $t("save_token_req") }} + </span> + </template> + </SmartModal> + </div> +</template> + +<script> +/* eslint-disable */ +import { Splitpanes, Pane } from "splitpanes" +import "splitpanes/dist/splitpanes.css" + +import url from "url" +import querystring from "querystring" +import parseCurlCommand from "~/helpers/curlparser" +import getEnvironmentVariablesFromScript from "~/helpers/preRequest" +import runTestScriptWithVariables from "~/helpers/postwomanTesting" +import parseTemplateString from "~/helpers/templating" +import { tokenRequest, oauthRedirect } from "~/helpers/oauth" +import { cancelRunningRequest, sendNetworkRequest } from "~/helpers/network" +import { + hasPathParams, + addPathParamsToVariables, + getQueryParams, +} from "~/helpers/requestParams" +import { parseUrlAndPath } from "~/helpers/utils/uri" +import { httpValid } from "~/helpers/utils/valid" +import { + knownContentTypes, + isJSONContentType, +} from "~/helpers/utils/contenttypes" +import { generateCodeWithGenerator } from "~/helpers/codegen/codegen" +import { getSettingSubject, applySetting } from "~/newstore/settings" +import { addRESTHistoryEntry } from "~/newstore/history" +import clone from "lodash/clone" + +export default { + components: { Splitpanes, Pane }, + + data() { + return { + showCurlImportModal: false, + showPreRequestScript: true, + testsEnabled: true, + testScript: "// pw.expect('variable').toBe('value');", + preRequestScript: "// pw.env.set('variable', 'value');", + testReports: [], + copyButton: '<i class="material-icons">content_copy</i>', + downloadButton: '<i class="material-icons">save_alt</i>', + doneButton: '<i class="material-icons">done</i>', + showCodegenModal: false, + response: { + status: "", + headers: "", + body: "", + duration: 0, + size: 0, + }, + validContentTypes: knownContentTypes, + paramsWatchEnabled: true, + showTokenListModal: false, + showTokenRequest: false, + showTokenRequestList: false, + showSaveRequestModal: false, + editRequest: {}, + customMethod: false, + files: [], + filenames: "", + navigatorShare: navigator.share, + runningRequest: false, + currentMethodIndex: 0, + methodMenuItems: [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + "CUSTOM", + ], + } + }, + subscriptions() { + return { + SCROLL_INTO_ENABLED: getSettingSubject("SCROLL_INTO_ENABLED"), + PROXY_ENABLED: getSettingSubject("PROXY_ENABLED"), + URL_EXCLUDES: getSettingSubject("URL_EXCLUDES"), + EXPERIMENTAL_URL_BAR_ENABLED: getSettingSubject( + "EXPERIMENTAL_URL_BAR_ENABLED" + ), + } + }, + watch: { + canListParameters: { + immediate: true, + handler(canListParameters) { + if (canListParameters) { + this.$nextTick(() => { + this.rawInput = Boolean(this.rawParams && this.rawParams !== "{}") + }) + } else { + this.rawInput = true + } + }, + }, + contentType(contentType, oldContentType) { + const getDefaultParams = (contentType) => { + if (isJSONContentType(contentType)) return "{}" + switch (contentType) { + case "application/xml": + return "<?xml version='1.0' encoding='utf-8'?>" + case "text/html": + return "<!doctype html>" + } + return "" + } + if ( + !this.rawParams || + this.rawParams === getDefaultParams(oldContentType) + ) { + this.rawParams = getDefaultParams(contentType) + } + this.setRouteQueryState() + }, + params: { + handler(newValue) { + if (!this.paramsWatchEnabled) { + this.paramsWatchEnabled = true + return + } + let path = this.path + let queryString = getQueryParams(newValue) + .map(({ key, value }) => `${key.trim()}=${value.trim()}`) + .join("&") + queryString = queryString === "" ? "" : `?${encodeURI(queryString)}` + if (path.includes("?")) { + path = path.slice(0, path.indexOf("?")) + queryString + } else { + path = path + queryString + } + this.path = path + this.setRouteQueryState() + }, + deep: true, + }, + selectedRequest(newValue) { + // @TODO: Convert all variables to single request variable + if (!newValue) return + this.uri = newValue.url + newValue.path + this.url = newValue.url + this.path = newValue.path + this.method = newValue.method + this.auth = newValue.auth + this.httpUser = newValue.httpUser + this.httpPassword = newValue.httpPassword + this.passwordFieldType = newValue.passwordFieldType + this.bearerToken = newValue.bearerToken + this.headers = newValue.headers + this.params = newValue.params + this.bodyParams = newValue.bodyParams + this.rawParams = newValue.rawParams + this.rawInput = newValue.rawInput + this.contentType = newValue.contentType + this.requestType = newValue.requestType + if (newValue.preRequestScript) { + this.showPreRequestScript = true + this.preRequestScript = newValue.preRequestScript + } + if (newValue.testScript) { + this.testsEnabled = true + this.testScript = newValue.testScript + } + this.name = newValue.name + }, + method() { + this.contentType = ["POST", "PUT", "PATCH", "DELETE"].includes( + this.method + ) + ? this.contentType + : "application/json" + }, + preRequestScript(val, oldVal) { + this.uri = this.uri + }, + }, + computed: { + /** + * Check content types that can be automatically + * serialized by postwoman. + */ + canListParameters() { + return ( + this.contentType === "application/x-www-form-urlencoded" || + this.contentType === "multipart/form-data" || + isJSONContentType(this.contentType) + ) + }, + uri: { + get() { + return this.$store.state.request.uri + ? this.$store.state.request.uri + : this.url + this.path + }, + set(value) { + this.$store.commit("setState", { value, attribute: "uri" }) + let url = value + if ( + (this.preRequestScript && this.showPreRequestScript) || + hasPathParams(this.params) + ) { + let environmentVariables = getEnvironmentVariablesFromScript( + this.preRequestScript + ) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + url = parseTemplateString(value, environmentVariables) + } + let result = parseUrlAndPath(url) + this.url = result.url + this.path = result.path + }, + }, + url: { + get() { + return this.$store.state.request.url + }, + set(value) { + this.$store.commit("setState", { value, attribute: "url" }) + }, + }, + method: { + get() { + return this.$store.state.request.method + }, + set(value) { + this.$store.commit("setState", { value, attribute: "method" }) + }, + }, + path: { + get() { + return this.$store.state.request.path + }, + set(value) { + this.$store.commit("setState", { value, attribute: "path" }) + }, + }, + name: { + get() { + return this.$store.state.request.name + }, + set(value) { + this.$store.commit("setState", { value, attribute: "name" }) + }, + }, + auth: { + get() { + return this.$store.state.request.auth + }, + set(value) { + this.$store.commit("setState", { value, attribute: "auth" }) + }, + }, + httpUser: { + get() { + return this.$store.state.request.httpUser + }, + set(value) { + this.$store.commit("setState", { value, attribute: "httpUser" }) + }, + }, + httpPassword: { + get() { + return this.$store.state.request.httpPassword + }, + set(value) { + this.$store.commit("setState", { value, attribute: "httpPassword" }) + }, + }, + bearerToken: { + get() { + return this.$store.state.request.bearerToken + }, + set(value) { + this.$store.commit("setState", { value, attribute: "bearerToken" }) + }, + }, + tokens: { + get() { + return this.$store.state.oauth2.tokens + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "tokens" }) + }, + }, + tokenReqs: { + get() { + return this.$store.state.oauth2.tokenReqs + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "tokenReqs" }) + }, + }, + tokenReqSelect: { + get() { + return this.$store.state.oauth2.tokenReqSelect + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "tokenReqSelect" }) + }, + }, + tokenReqName: { + get() { + return this.$store.state.oauth2.tokenReqName + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "tokenReqName" }) + }, + }, + accessTokenName: { + get() { + return this.$store.state.oauth2.accessTokenName + }, + set(value) { + this.$store.commit("setOAuth2", { + value, + attribute: "accessTokenName", + }) + }, + }, + oidcDiscoveryUrl: { + get() { + return this.$store.state.oauth2.oidcDiscoveryUrl + }, + set(value) { + this.$store.commit("setOAuth2", { + value, + attribute: "oidcDiscoveryUrl", + }) + }, + }, + authUrl: { + get() { + return this.$store.state.oauth2.authUrl + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "authUrl" }) + }, + }, + accessTokenUrl: { + get() { + return this.$store.state.oauth2.accessTokenUrl + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "accessTokenUrl" }) + }, + }, + clientId: { + get() { + return this.$store.state.oauth2.clientId + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "clientId" }) + }, + }, + scope: { + get() { + return this.$store.state.oauth2.scope + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "scope" }) + }, + }, + state: { + get() { + return this.$store.state.oauth2.state + }, + set(value) { + this.$store.commit("setOAuth2", { value, attribute: "state" }) + }, + }, + headers: { + get() { + return this.$store.state.request.headers + }, + set(value) { + this.$store.commit("setState", { value, attribute: "headers" }) + }, + }, + params: { + get() { + return this.$store.state.request.params + }, + set(value) { + this.$store.commit("setState", { value, attribute: "params" }) + }, + }, + bodyParams: { + get() { + return this.$store.state.request.bodyParams + }, + set(value) { + this.$store.commit("setState", { value, attribute: "bodyParams" }) + }, + }, + rawParams: { + get() { + return this.$store.state.request.rawParams + }, + set(value) { + this.$store.commit("setState", { value, attribute: "rawParams" }) + // Convert the rawParams to bodyParams format + try { + const valueObj = JSON.parse(value) + const params = Object.keys(valueObj).map((key) => { + if (typeof valueObj[key] !== "function") { + return { + active: true, + key, + value: valueObj[key], + } + } + }) + this.$store.commit("setBodyParams", { params }) + } catch {} + }, + }, + rawInput: { + get() { + return this.$store.state.request.rawInput + }, + set(value) { + this.$store.commit("setState", { value, attribute: "rawInput" }) + }, + }, + requestType: { + get() { + return this.$store.state.request.requestType + }, + set(value) { + this.$store.commit("setState", { value, attribute: "requestType" }) + }, + }, + contentType: { + get() { + return this.$store.state.request.contentType + }, + set(value) { + this.$store.commit("setState", { value, attribute: "contentType" }) + }, + }, + passwordFieldType: { + get() { + return this.$store.state.request.passwordFieldType + }, + set(value) { + this.$store.commit("setState", { + value, + attribute: "passwordFieldType", + }) + }, + }, + selectedRequest() { + return this.$store.state.postwoman.selectedRequest + }, + requestName() { + return this.name + }, + isValidURL() { + // if showPreRequestScript, we cannot determine if a URL is valid because the full string is not known ahead of time + return this.showPreRequestScript || httpValid(this.url) + }, + hasRequestBody() { + return ["POST", "PUT", "PATCH", "DELETE"].includes(this.method) + }, + pathName() { + return this.path.match(/^([^?]*)\??/)[1] + }, + rawRequestBody() { + const { bodyParams, contentType } = this + if (isJSONContentType(contentType)) { + try { + const obj = JSON.parse( + `{${bodyParams + .filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + ) + .filter(({ key }) => !!key) + .map(({ key, value }) => `"${key}": "${value}"`) + .join()}}` + ) + return JSON.stringify(obj, null, 2) + } catch (ex) { + console.log(ex) + this.$toast.clear() + this.$toast.error( + "Parameter value must be a string, switch to Raw input for other formats", + { + icon: "error", + } + ) + return "invalid" + } + } else { + return bodyParams + .filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + ) + .filter(({ key }) => !!key) + .map( + ({ key, value }) => + `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + ) + .join("&") + } + }, + queryString() { + const result = getQueryParams(this.params) + .map( + ({ key, value }) => + `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + ) + .join("&") + return result === "" ? "" : `?${result}` + }, + requestCode() { + let headers = [] + if (this.preRequestScript || hasPathParams(this.params)) { + let environmentVariables = getEnvironmentVariablesFromScript( + this.preRequestScript + ) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + for (let k of this.headers.filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + )) { + const kParsed = parseTemplateString(k.key, environmentVariables) + const valParsed = parseTemplateString(k.value, environmentVariables) + headers.push({ key: kParsed, value: valParsed }) + } + } + + return generateCodeWithGenerator(this.requestType, { + auth: this.auth, + method: this.method, + url: this.url, + pathName: this.pathName, + queryString: this.queryString, + httpUser: this.httpUser, + httpPassword: this.httpPassword, + bearerToken: this.bearerToken, + headers, + rawInput: this.rawInput, + rawParams: this.rawParams, + rawRequestBody: this.rawRequestBody, + contentType: this.contentType, + }) + }, + tokenReqDetails() { + const details = { + oidcDiscoveryUrl: this.oidcDiscoveryUrl, + authUrl: this.authUrl, + accessTokenUrl: this.accessTokenUrl, + clientId: this.clientId, + scope: this.scope, + } + return JSON.stringify(details, null, 2) + }, + }, + methods: { + scrollInto(view) { + this.$refs[view].$el.scrollIntoView({ + behavior: "smooth", + }) + }, + handleUseHistory(entry) { + this.name = entry.name + this.method = entry.method + this.uri = entry.url + entry.path + this.url = entry.url + this.path = entry.path + this.showPreRequestScript = entry.usesPreScripts + this.preRequestScript = entry.preRequestScript + this.auth = entry.auth + this.httpUser = entry.httpUser + this.httpPassword = entry.httpPassword + this.bearerToken = entry.bearerToken + this.headers = entry.headers + this.params = entry.params + this.bodyParams = entry.bodyParams + this.rawParams = entry.rawParams + this.rawInput = entry.rawInput + this.contentType = entry.contentType + this.requestType = entry.requestType + this.testScript = entry.testScript + this.testsEnabled = entry.usesPostScripts + if (this.SCROLL_INTO_ENABLED) this.scrollInto("request") + }, + async makeRequest(auth, headers, requestBody, preRequestScript) { + const requestOptions = { + method: this.method, + url: this.url + this.pathName + this.queryString, + auth, + headers, + data: requestBody, + credentials: true, + } + + if (preRequestScript || hasPathParams(this.params)) { + let environmentVariables = + getEnvironmentVariablesFromScript(preRequestScript) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + requestOptions.url = parseTemplateString( + requestOptions.url, + environmentVariables + ) + if (!(requestOptions.data instanceof FormData)) { + // TODO: Parse env variables for form data too + requestOptions.data = parseTemplateString( + requestOptions.data, + environmentVariables + ) + } + for (let k in requestOptions.headers) { + const kParsed = parseTemplateString(k, environmentVariables) + const valParsed = parseTemplateString( + requestOptions.headers[k], + environmentVariables + ) + delete requestOptions.headers[k] + requestOptions.headers[kParsed] = valParsed + } + } + if (typeof requestOptions.data === "string") { + requestOptions.data = parseTemplateString(requestOptions.data) + } + return await sendNetworkRequest(requestOptions) + }, + cancelRequest() { + cancelRunningRequest() + }, + async sendRequest() { + this.$toast.clear() + if (this.SCROLL_INTO_ENABLED) this.scrollInto("response") + if (!this.isValidURL) { + this.$toast.error(this.$t("url_invalid_format"), { + icon: "error", + }) + return + } + // Start showing the loading bar as soon as possible. + // The nuxt axios module will hide it when the request is made. + this.$nuxt.$loading.start() + this.response = { + duration: 0, + size: 0, + status: this.$t("fetching"), + body: this.$t("loading"), + } + const auth = + this.auth === "Basic Auth" + ? { + username: this.httpUser, + password: this.httpPassword, + } + : null + let headers = {} + let headersObject = {} + Object.keys(headers).forEach((id) => { + if (headers[id].key) headersObject[headers[id].key] = headers[id].value + }) + headers = headersObject + // If the request has a body, we want to ensure Content-Type is sent. + let requestBody + if (this.hasRequestBody) { + requestBody = this.rawInput ? this.rawParams : this.rawRequestBody + Object.assign(headers, { + "Content-Type": `${this.contentType}; charset=utf-8`, + }) + } + requestBody = requestBody ? requestBody.toString() : null + if (this.contentType === "multipart/form-data") { + const formData = new FormData() + for (const bodyParam of this.bodyParams.filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + )) { + if (bodyParam?.value?.[0] instanceof File) { + for (const file of bodyParam.value) { + formData.append(bodyParam.key, file) + } + } else { + formData.append(bodyParam.key, bodyParam.value) + } + } + requestBody = formData + } + // If the request uses a token for auth, we want to make sure it's sent here. + if (this.auth === "Bearer Token" || this.auth === "OAuth 2.0") + headers["Authorization"] = `Bearer ${this.bearerToken}` + headers = Object.assign( + // Clone the app headers object first, we don't want to + // mutate it with the request headers added by default. + Object.assign( + {}, + this.headers.filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + ) + ) + // We make our temporary headers object the source so + // that you can override the added headers if you + // specify them. + // headers + ) + Object.keys(headers).forEach((id) => { + if (headers[id].key) headersObject[headers[id].key] = headers[id].value + }) + headers = headersObject + const startTime = new Date().getTime() + try { + this.runningRequest = true + const payload = await this.makeRequest( + auth, + headers, + requestBody, + this.showPreRequestScript && this.preRequestScript + ) + this.runningRequest = false + const duration = new Date().getTime() - startTime + this.response.duration = duration + this.response.size = payload.headers["content-length"] + ;(() => { + this.response.status = payload.status + this.response.headers = payload.headers + // We don't need to bother parsing JSON, axios already handles it for us! + this.response.body = payload.data + // Addition of an entry to the history component. + const entry = { + name: this.requestName, + status: this.response.status, + date: new Date().toLocaleDateString(), + time: new Date().toLocaleTimeString(), + updatedOn: new Date(), + method: this.method, + url: this.url, + path: this.path, + usesPreScripts: this.showPreRequestScript, + preRequestScript: this.preRequestScript, + duration, + star: false, + auth: this.auth, + httpUser: this.httpUser, + httpPassword: this.httpPassword, + bearerToken: this.bearerToken, + headers: this.headers, + params: this.params, + bodyParams: this.bodyParams, + rawParams: this.rawParams, + rawInput: this.rawInput, + contentType: this.contentType, + requestType: this.requestType, + testScript: this.testScript, + usesPostScripts: this.testsEnabled, + } + + if ( + (this.preRequestScript && this.showPreRequestScript) || + hasPathParams(this.params) + ) { + let environmentVariables = getEnvironmentVariablesFromScript( + this.preRequestScript + ) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + entry.path = parseTemplateString(entry.path, environmentVariables) + entry.url = parseTemplateString(entry.url, environmentVariables) + } + + addRESTHistoryEntry(entry) + })() + } catch (error) { + this.runningRequest = false + // If the error is caused by cancellation, do nothing + if (error === "cancellation") { + this.response.status = this.$t("cancelled") + this.response.body = this.$t("cancelled") + } else { + console.log(error) + const duration = new Date().getTime() - startTime + this.response.duration = duration + this.response.size = Buffer.byteLength(JSON.stringify(error)) + if (error.response) { + this.response.headers = error.response.headers + this.response.status = error.response.status + this.response.body = error.response.data + // Addition of an entry to the history component. + const entry = { + name: this.requestName, + status: this.response.status, + date: new Date().toLocaleDateString(), + time: new Date().toLocaleTimeString(), + updatedOn: new Date(), + method: this.method, + url: this.url, + path: this.path, + usesPreScripts: this.showPreRequestScript, + preRequestScript: this.preRequestScript, + star: false, + auth: this.auth, + httpUser: this.httpUser, + httpPassword: this.httpPassword, + bearerToken: this.bearerToken, + headers: this.headers, + params: this.params, + bodyParams: this.bodyParams, + rawParams: this.rawParams, + rawInput: this.rawInput, + contentType: this.contentType, + requestType: this.requestType, + testScript: this.testScript, + usesPostScripts: this.testsEnabled, + } + + if ( + (this.preRequestScript && this.showPreRequestScript) || + hasPathParams(this.params) + ) { + let environmentVariables = getEnvironmentVariablesFromScript( + this.preRequestScript + ) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + entry.path = parseTemplateString(entry.path, environmentVariables) + entry.url = parseTemplateString(entry.url, environmentVariables) + } + + addRESTHistoryEntry(entry) + return + } else { + this.response.status = error.message + this.response.body = `${error}. ${this.$t("check_console_details")}` + this.$toast.error(`${error} ${this.$t("f12_details")}`, { + icon: "error", + }) + if (!this.PROXY_ENABLED) { + this.$toast.info(this.$t("enable_proxy"), { + icon: "help", + duration: 8000, + action: { + text: this.$t("yes"), + onClick: (e, toastObject) => { + this.$router.push({ path: "/settings" }) + }, + }, + }) + } + } + } + } + // tests + const syntheticResponse = { + status: this.response.status, + body: this.response.body, + headers: this.response.headers, + } + + // Parse JSON body + if ( + syntheticResponse.headers["content-type"] && + isJSONContentType(syntheticResponse.headers["content-type"]) + ) { + try { + syntheticResponse.body = JSON.parse( + new TextDecoder("utf-8").decode( + new Uint8Array(syntheticResponse.body) + ) + ) + } catch (_e) {} + } + + const { testResults } = runTestScriptWithVariables(this.testScript, { + response: syntheticResponse, + }) + this.testReports = testResults + }, + getQueryStringFromPath() { + const pathParsed = url.parse(this.uri) + return pathParsed.query ? pathParsed.query : "" + }, + queryStringToArray(queryString) { + const queryParsed = querystring.parse(queryString) + return Object.keys(queryParsed).map((key) => ({ + key, + value: queryParsed[key], + active: true, + })) + }, + pathInputHandler() { + if (this.uri.includes("?")) { + const queryString = this.getQueryStringFromPath() + let environmentVariables = getEnvironmentVariablesFromScript( + this.preRequestScript + ) + environmentVariables = addPathParamsToVariables( + this.params, + environmentVariables + ) + const params = this.queryStringToArray(queryString) + let parsedParams = [] + for (let k of params.filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + )) { + const kParsed = parseTemplateString(k.key, environmentVariables) + const valParsed = parseTemplateString(k.value, environmentVariables) + parsedParams.push({ key: kParsed, value: valParsed, active: true }) + } + this.paramsWatchEnabled = false + this.params = parsedParams + } + }, + addRequestHeader() { + this.$store.commit("addHeaders", { + key: "", + value: "", + active: true, + }) + return false + }, + removeRequestHeader(index) { + // .slice() gives us an entirely new array rather than giving us just the reference + const oldHeaders = this.headers.slice() + this.$store.commit("removeHeaders", index) + this.$toast.error(this.$t("deleted"), { + icon: "delete", + action: { + text: this.$t("undo"), + onClick: (e, toastObject) => { + this.headers = oldHeaders + toastObject.remove() + }, + }, + }) + }, + addRequestParam() { + this.$store.commit("addParams", { + key: "", + value: "", + type: "query", + active: true, + }) + return false + }, + removeRequestParam(index) { + // .slice() gives us an entirely new array rather than giving us just the reference + const oldParams = this.params.slice() + this.$store.commit("removeParams", index) + this.$toast.error(this.$t("deleted"), { + icon: "delete", + action: { + text: this.$t("undo"), + onClick: (e, toastObject) => { + this.params = oldParams + toastObject.remove() + }, + }, + }) + }, + addRequestBodyParam() { + this.$store.commit("addBodyParams", { key: "", value: "", active: true }) + return false + }, + removeRequestBodyParam(index) { + // .slice() gives us an entirely new array rather than giving us just the reference + const oldBodyParams = this.bodyParams.slice() + this.$store.commit("removeBodyParams", index) + this.$toast.error(this.$t("deleted"), { + icon: "delete", + action: { + text: this.$t("undo"), + onClick: (e, toastObject) => { + this.bodyParams = oldBodyParams + toastObject.remove() + }, + }, + }) + }, + copyRequest() { + if (navigator.share) { + const time = new Date().toLocaleTimeString() + const date = new Date().toLocaleDateString() + navigator + .share({ + title: "Hoppscotch", + text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`, + url: window.location.href, + }) + .then(() => {}) + .catch(() => {}) + } else { + const dummy = document.createElement("input") + document.body.appendChild(dummy) + dummy.value = window.location.href + dummy.select() + document.execCommand("copy") + document.body.removeChild(dummy) + this.$refs.copyRequest.innerHTML = this.doneButton + this.$toast.info(this.$t("copied_to_clipboard"), { + icon: "done", + }) + setTimeout( + () => (this.$refs.copyRequest.innerHTML = this.copyButton), + 1000 + ) + } + }, + setRouteQueryState() { + const flat = (key) => (this[key] !== "" ? `${key}=${this[key]}&` : "") + const deep = (key) => { + const haveItems = [...this[key]].length + if (haveItems && this[key]["value"] !== "") { + // Exclude files fro query params + const filesRemoved = this[key].filter( + (item) => !(item?.value?.[0] instanceof File) + ) + return `${key}=${JSON.stringify(filesRemoved)}&` + } + return "" + } + let flats = [ + "method", + "url", + "path", + !this.URL_EXCLUDES.auth ? "auth" : null, + !this.URL_EXCLUDES.httpUser ? "httpUser" : null, + !this.URL_EXCLUDES.httpPassword ? "httpPassword" : null, + !this.URL_EXCLUDES.bearerToken ? "bearerToken" : null, + "contentType", + ] + .filter((item) => item !== null) + .map((item) => flat(item)) + const deeps = ["headers", "params"].map((item) => deep(item)) + const bodyParams = this.rawInput + ? [flat("rawParams")] + : [deep("bodyParams")] + history.replaceState( + window.location.href, + "", + `${this.$router.options.base}?${encodeURI( + flats.concat(deeps, bodyParams).join("").slice(0, -1) + )}` + ) + }, + setRouteQueries(queries) { + if (typeof queries !== "object") + throw new Error("Route query parameters must be a Object") + for (const key in queries) { + if (["headers", "params", "bodyParams"].includes(key)) + this[key] = JSON.parse(decodeURI(encodeURI(queries[key]))) + if (key === "rawParams") { + this.rawInput = true + this.rawParams = queries["rawParams"] + } else if (typeof this[key] === "string") { + this[key] = queries[key] + } + } + }, + handleImport() { + const { value: text } = document.getElementById("import-curl") + try { + const parsedCurl = parseCurlCommand(text) + const { origin, pathname } = new URL( + parsedCurl.url.replace(/"/g, "").replace(/'/g, "") + ) + this.url = origin + this.path = pathname + this.uri = this.url + this.path + this.headers = [] + if (parsedCurl.query) { + for (const key of Object.keys(parsedCurl.query)) { + this.$store.commit("addParams", { + key, + value: parsedCurl.query[key], + type: "query", + active: true, + }) + } + } + if (parsedCurl.headers) { + for (const key of Object.keys(parsedCurl.headers)) { + this.$store.commit("addHeaders", { + key, + value: parsedCurl.headers[key], + }) + } + } + this.method = parsedCurl.method.toUpperCase() + if (parsedCurl["data"]) { + this.rawInput = true + this.rawParams = parsedCurl["data"] + } + this.showCurlImportModal = false + } catch (error) { + this.showCurlImportModal = false + this.$toast.error(this.$t("curl_invalid_format"), { + icon: "error", + }) + } + }, + switchVisibility() { + this.passwordFieldType = + this.passwordFieldType === "password" ? "text" : "password" + }, + clearContent(name, { target }) { + switch (name) { + case "bodyParams": + this.bodyParams = [] + this.files = [] + break + case "rawParams": + this.rawParams = "{}" + break + case "parameters": + this.params = [] + break + case "auth": + this.auth = "None" + this.httpUser = "" + this.httpPassword = "" + this.bearerToken = "" + this.showTokenRequest = false + this.tokens = [] + this.tokenReqs = [] + break + case "access_token": + this.accessTokenName = "" + this.oidcDiscoveryUrl = "" + this.authUrl = "" + this.accessTokenUrl = "" + this.clientId = "" + this.scope = "" + break + case "headers": + this.headers = [] + break + case "tests": + this.testReports = [] + break + case "tokens": + this.tokens = [] + break + default: + this.method = "GET" + this.url = "https://httpbin.org" + this.path = "/get" + this.uri = this.url + this.path + this.name = "Untitled request" + this.bodyParams = [] + this.rawParams = "{}" + this.files = [] + this.params = [] + this.auth = "None" + this.httpUser = "" + this.httpPassword = "" + this.bearerToken = "" + this.showTokenRequest = false + this.tokens = [] + this.tokenReqs = [] + this.accessTokenName = "" + this.oidcDiscoveryUrl = "" + this.authUrl = "" + this.accessTokenUrl = "" + this.clientId = "" + this.scope = "" + this.headers = [] + this.testReports = [] + } + target.innerHTML = this.doneButton + this.$toast.info(this.$t("cleared"), { + icon: "clear_all", + }) + setTimeout( + () => (target.innerHTML = '<i class="material-icons">clear_all</i>'), + 1000 + ) + }, + saveRequest() { + let urlAndPath = parseUrlAndPath(this.uri) + this.editRequest = { + url: decodeURI(urlAndPath.url), + path: decodeURI(urlAndPath.path), + method: this.method, + auth: this.auth, + httpUser: this.httpUser, + httpPassword: this.httpPassword, + passwordFieldType: this.passwordFieldType, + bearerToken: this.bearerToken, + headers: this.headers, + params: this.params, + bodyParams: this.bodyParams, + rawParams: this.rawParams, + rawInput: this.rawInput, + contentType: this.contentType, + requestType: this.requestType, + preRequestScript: + this.showPreRequestScript == true ? this.preRequestScript : null, + testScript: this.testsEnabled == true ? this.testScript : null, + name: this.requestName, + } + this.showSaveRequestModal = true + }, + hideRequestModal() { + this.showSaveRequestModal = false + this.editRequest = {} + }, + setExclude(excludedField, excluded) { + const update = clone(this.URL_EXCLUDES) + + if (excludedField === "auth") { + update.auth = excluded + update.httpUser = excluded + update.httpPassword = excluded + update.bearerToken = excluded + } else { + update[excludedField] = excluded + } + + applySetting("URL_EXCLUDES", update) + + this.setRouteQueryState() + }, + updateRawBody(rawParams) { + this.rawParams = rawParams + }, + async handleAccessTokenRequest() { + if ( + this.oidcDiscoveryUrl === "" && + (this.authUrl === "" || this.accessTokenUrl === "") + ) { + this.$toast.error(this.$t("complete_config_urls"), { + icon: "error", + }) + return + } + try { + const tokenReqParams = { + grantType: "code", + oidcDiscoveryUrl: this.oidcDiscoveryUrl, + authUrl: this.authUrl, + accessTokenUrl: this.accessTokenUrl, + clientId: this.clientId, + scope: this.scope, + } + await tokenRequest(tokenReqParams) + } catch (e) { + this.$toast.error(e, { + icon: "code", + }) + } + }, + async oauthRedirectReq() { + const tokenInfo = await oauthRedirect() + if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) { + this.bearerToken = tokenInfo.access_token + this.addOAuthToken({ + name: this.accessTokenName, + value: tokenInfo.access_token, + }) + } + }, + addOAuthToken({ name, value }) { + this.$store.commit("addOAuthToken", { + name, + value, + }) + return false + }, + removeOAuthToken(index) { + const oldTokens = this.tokens.slice() + this.$store.commit("removeOAuthToken", index) + this.$toast.error(this.$t("deleted"), { + icon: "delete", + action: { + text: this.$t("undo"), + onClick: (e, toastObject) => { + this.tokens = oldTokens + toastObject.remove() + }, + }, + }) + }, + useOAuthToken(value) { + this.bearerToken = value + this.showTokenListModal = false + }, + addOAuthTokenReq() { + try { + const name = this.tokenReqName + const details = JSON.parse(this.tokenReqDetails) + this.$store.commit("addOAuthTokenReq", { + name, + details, + }) + this.$toast.info(this.$t("token_request_saved")) + this.showTokenRequestList = false + } catch (e) { + this.$toast.error(e, { + icon: "code", + }) + } + }, + removeOAuthTokenReq(index) { + const oldTokenReqs = this.tokenReqs.slice() + const targetReqIndex = this.tokenReqs.findIndex( + ({ name }) => name === this.tokenReqName + ) + if (targetReqIndex < 0) return + this.$store.commit("removeOAuthTokenReq", targetReqIndex) + this.$toast.error(this.$t("deleted"), { + icon: "delete", + action: { + text: this.$t("undo"), + onClick: (e, toastObject) => { + this.tokenReqs = oldTokenReqs + toastObject.remove() + }, + }, + }) + }, + tokenReqChange({ target }) { + const { details, name } = this.tokenReqs.find( + ({ name }) => name === target.value + ) + const { oidcDiscoveryUrl, authUrl, accessTokenUrl, clientId, scope } = + details + this.tokenReqName = name + this.oidcDiscoveryUrl = oidcDiscoveryUrl + this.authUrl = authUrl + this.accessTokenUrl = accessTokenUrl + this.clientId = clientId + this.scope = scope + }, + setRequestType(val) { + this.requestType = val + }, + }, + async mounted() { + this._keyListener = function (e) { + if (e.key === "g" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + if (!this.runningRequest) { + this.sendRequest() + } else { + this.cancelRequest() + } + } + if (e.key === "s" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + this.saveRequest() + } + if (e.key === "k" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + this.copyRequest() + } + if (e.key === "i" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + this.$refs.clearAll.click() + } + if ((e.key === "g" || e.key === "G") && e.altKey) { + this.method = "GET" + } + if ((e.key === "h" || e.key === "H") && e.altKey) { + this.method = "HEAD" + } + if ((e.key === "p" || e.key === "P") && e.altKey) { + this.method = "POST" + } + if ((e.key === "u" || e.key === "U") && e.altKey) { + this.method = "PUT" + } + if ((e.key === "x" || e.key === "X") && e.altKey) { + this.method = "DELETE" + } + if (e.key == "ArrowUp" && e.altKey && this.currentMethodIndex > 0) { + this.method = + this.methodMenuItems[ + --this.currentMethodIndex % this.methodMenuItems.length + ] + } else if ( + e.key == "ArrowDown" && + e.altKey && + this.currentMethodIndex < 9 + ) { + this.method = + this.methodMenuItems[ + ++this.currentMethodIndex % this.methodMenuItems.length + ] + } + } + document.addEventListener("keydown", this._keyListener.bind(this)) + await this.oauthRedirectReq() + }, + created() { + if (Object.keys(this.$route.query).length) + this.setRouteQueries(this.$route.query) + this.$watch( + (vm) => [ + vm.name, + vm.method, + vm.url, + vm.auth, + vm.path, + vm.httpUser, + vm.httpPassword, + vm.bearerToken, + vm.headers, + vm.params, + vm.bodyParams, + vm.contentType, + vm.rawParams, + ], + (val) => { + this.setRouteQueryState() + } + ) + }, + beforeDestroy() { + document.removeEventListener("keydown", this._keyListener) + }, +} +</script>