RequestOptions.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <template>
  2. <div>
  3. <SmartTabs styles="sticky bg-primary top-upperPrimaryStickyFold z-10">
  4. <template #actions>
  5. <ButtonSecondary
  6. :label="`${$t('request.run')}`"
  7. svg="play"
  8. class="rounded-none !text-accent"
  9. @click.native="runQuery()"
  10. />
  11. <ButtonSecondary
  12. ref="saveRequest"
  13. :label="`${$t('request.save')}`"
  14. class="rounded-none"
  15. @click.native="saveRequest"
  16. />
  17. </template>
  18. <SmartTab :id="'query'" :label="`${$t('tab.query')}`" :selected="true">
  19. <AppSection label="query">
  20. <div
  21. class="
  22. bg-primary
  23. border-b border-dividerLight
  24. flex flex-1
  25. top-upperSecondaryStickyFold
  26. pl-4
  27. z-10
  28. sticky
  29. items-center
  30. justify-between
  31. gqlRunQuery
  32. "
  33. >
  34. <label class="font-semibold text-secondaryLight">
  35. {{ $t("request.query") }}
  36. </label>
  37. <div class="flex">
  38. <ButtonSecondary
  39. v-tippy="{ theme: 'tooltip' }"
  40. to="https://docs.hoppscotch.io"
  41. blank
  42. :title="$t('app.wiki')"
  43. svg="help-circle"
  44. />
  45. <ButtonSecondary
  46. v-tippy="{ theme: 'tooltip' }"
  47. :title="$t('action.copy')"
  48. :svg="`${copyQueryIcon}`"
  49. @click.native="copyQuery"
  50. />
  51. <ButtonSecondary
  52. v-tippy="{ theme: 'tooltip' }"
  53. :title="$t('action.prettify')"
  54. :svg="`${prettifyQueryIcon}`"
  55. @click.native="prettifyQuery"
  56. />
  57. </div>
  58. </div>
  59. <div ref="queryEditor"></div>
  60. </AppSection>
  61. </SmartTab>
  62. <SmartTab :id="'variables'" :label="`${$t('tab.variables')}`">
  63. <AppSection label="variables">
  64. <div
  65. class="
  66. bg-primary
  67. border-b border-dividerLight
  68. flex flex-1
  69. top-upperSecondaryStickyFold
  70. pl-4
  71. z-10
  72. sticky
  73. items-center
  74. justify-between
  75. "
  76. >
  77. <label class="font-semibold text-secondaryLight">
  78. {{ $t("request.variables") }}
  79. </label>
  80. <div class="flex">
  81. <ButtonSecondary
  82. v-tippy="{ theme: 'tooltip' }"
  83. to="https://docs.hoppscotch.io"
  84. blank
  85. :title="$t('app.wiki')"
  86. svg="help-circle"
  87. />
  88. <ButtonSecondary
  89. v-tippy="{ theme: 'tooltip' }"
  90. :title="$t('action.copy')"
  91. :svg="`${copyVariablesIcon}`"
  92. @click.native="copyVariables"
  93. />
  94. </div>
  95. </div>
  96. <div ref="variableEditor"></div>
  97. </AppSection>
  98. </SmartTab>
  99. <SmartTab :id="'headers'" :label="`${$t('tab.headers')}`">
  100. <AppSection label="headers">
  101. <div
  102. class="
  103. bg-primary
  104. border-b border-dividerLight
  105. flex flex-1
  106. top-upperSecondaryStickyFold
  107. pl-4
  108. z-10
  109. sticky
  110. items-center
  111. justify-between
  112. "
  113. >
  114. <label class="font-semibold text-secondaryLight">
  115. {{ $t("tab.headers") }}
  116. </label>
  117. <div class="flex">
  118. <ButtonSecondary
  119. v-tippy="{ theme: 'tooltip' }"
  120. to="https://docs.hoppscotch.io"
  121. blank
  122. :title="$t('app.wiki')"
  123. svg="help-circle"
  124. />
  125. <ButtonSecondary
  126. v-tippy="{ theme: 'tooltip' }"
  127. :title="$t('action.clear_all')"
  128. svg="trash-2"
  129. :disabled="Boolean(bulkMode)"
  130. @click.native="headers = []"
  131. />
  132. <ButtonSecondary
  133. v-tippy="{ theme: 'tooltip' }"
  134. :title="$t('state.bulk_mode')"
  135. svg="edit"
  136. :class="{ '!text-accent': bulkMode }"
  137. @click.native="bulkMode = !bulkMode"
  138. />
  139. <ButtonSecondary
  140. v-tippy="{ theme: 'tooltip' }"
  141. :title="$t('add.new')"
  142. svg="plus"
  143. :disabled="Boolean(bulkMode)"
  144. @click.native="addRequestHeader"
  145. />
  146. </div>
  147. </div>
  148. <div v-if="bulkMode" ref="bulkEditor"></div>
  149. <div v-else>
  150. <div
  151. v-for="(header, index) in headers"
  152. :key="`header-${String(index)}`"
  153. class="
  154. divide-x divide-dividerLight
  155. border-b border-dividerLight
  156. flex
  157. "
  158. >
  159. <SmartAutoComplete
  160. :placeholder="`${$t('count.header', { count: index + 1 })}`"
  161. :source="commonHeaders"
  162. :spellcheck="false"
  163. :value="header.key"
  164. autofocus
  165. styles="
  166. bg-transparent
  167. flex
  168. flex-1
  169. py-1
  170. px-4
  171. truncate
  172. focus:outline-none
  173. "
  174. @input="
  175. updateGQLHeader(index, {
  176. key: $event,
  177. value: header.value,
  178. active: header.active,
  179. })
  180. "
  181. />
  182. <input
  183. class="bg-transparent flex flex-1 py-2 px-4"
  184. :placeholder="`${$t('count.value', { count: index + 1 })}`"
  185. :name="`value ${String(index)}`"
  186. :value="header.value"
  187. autofocus
  188. @change="
  189. updateGQLHeader(index, {
  190. key: header.key,
  191. value: $event.target.value,
  192. active: header.active,
  193. })
  194. "
  195. />
  196. <span>
  197. <ButtonSecondary
  198. v-tippy="{ theme: 'tooltip' }"
  199. :title="
  200. header.hasOwnProperty('active')
  201. ? header.active
  202. ? $t('action.turn_off')
  203. : $t('action.turn_on')
  204. : $t('action.turn_off')
  205. "
  206. :svg="
  207. header.hasOwnProperty('active')
  208. ? header.active
  209. ? 'check-circle'
  210. : 'circle'
  211. : 'check-circle'
  212. "
  213. color="green"
  214. @click.native="
  215. updateGQLHeader(index, {
  216. key: header.key,
  217. value: header.value,
  218. active: !header.active,
  219. })
  220. "
  221. />
  222. </span>
  223. <span>
  224. <ButtonSecondary
  225. v-tippy="{ theme: 'tooltip' }"
  226. :title="$t('action.remove')"
  227. svg="trash"
  228. color="red"
  229. @click.native="removeRequestHeader(index)"
  230. />
  231. </span>
  232. </div>
  233. <div
  234. v-if="headers.length === 0"
  235. class="
  236. flex flex-col
  237. text-secondaryLight
  238. p-4
  239. items-center
  240. justify-center
  241. "
  242. >
  243. <img
  244. :src="`/images/states/${$colorMode.value}/add_category.svg`"
  245. loading="lazy"
  246. class="
  247. flex-col
  248. my-4
  249. object-contain object-center
  250. h-16
  251. w-16
  252. inline-flex
  253. "
  254. />
  255. <span class="text-center pb-4">
  256. {{ $t("empty.headers") }}
  257. </span>
  258. <ButtonSecondary
  259. :label="`${$t('add.new')}`"
  260. filled
  261. svg="plus"
  262. @click.native="addRequestHeader"
  263. />
  264. </div>
  265. </div>
  266. </AppSection>
  267. </SmartTab>
  268. </SmartTabs>
  269. <CollectionsSaveRequest
  270. mode="graphql"
  271. :show="Boolean(showSaveRequestModal)"
  272. @hide-modal="hideRequestModal"
  273. />
  274. </div>
  275. </template>
  276. <script setup lang="ts">
  277. import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
  278. import clone from "lodash/clone"
  279. import * as gql from "graphql"
  280. import { copyToClipboard } from "~/helpers/utils/clipboard"
  281. import {
  282. useNuxt,
  283. useReadonlyStream,
  284. useStream,
  285. } from "~/helpers/utils/composables"
  286. import {
  287. addGQLHeader,
  288. gqlHeaders$,
  289. gqlQuery$,
  290. gqlResponse$,
  291. gqlURL$,
  292. gqlVariables$,
  293. removeGQLHeader,
  294. setGQLHeaders,
  295. setGQLQuery,
  296. setGQLResponse,
  297. setGQLVariables,
  298. updateGQLHeader,
  299. } from "~/newstore/GQLSession"
  300. import { commonHeaders } from "~/helpers/headers"
  301. import { GQLConnection } from "~/helpers/GQLConnection"
  302. import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
  303. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  304. import { getCurrentStrategyID } from "~/helpers/network"
  305. import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
  306. import { useCodemirror } from "~/helpers/editor/codemirror"
  307. import "codemirror/mode/javascript/javascript"
  308. import "~/helpers/editor/modes/graphql"
  309. import jsonLinter from "~/helpers/editor/linting/json"
  310. import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
  311. import queryCompleter from "~/helpers/editor/completion/gqlQuery"
  312. const props = defineProps<{
  313. conn: GQLConnection
  314. }>()
  315. const {
  316. $toast,
  317. app: { i18n },
  318. } = useContext()
  319. const t = i18n.t.bind(i18n)
  320. const nuxt = useNuxt()
  321. const bulkMode = ref(false)
  322. const bulkHeaders = ref("")
  323. watch(bulkHeaders, () => {
  324. try {
  325. const transformation = bulkHeaders.value.split("\n").map((item) => ({
  326. key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
  327. value: item.substring(item.indexOf(":") + 1).trim(),
  328. active: !item.trim().startsWith("//"),
  329. }))
  330. setGQLHeaders(transformation)
  331. } catch (e) {
  332. $toast.error(`${t("error.something_went_wrong")}`, {
  333. icon: "error_outline",
  334. })
  335. console.error(e)
  336. }
  337. })
  338. const url = useReadonlyStream(gqlURL$, "")
  339. const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
  340. const variableString = useStream(gqlVariables$, "", setGQLVariables)
  341. const headers = useStream(gqlHeaders$, [], setGQLHeaders)
  342. const bulkEditor = ref<any | null>(null)
  343. useCodemirror(bulkEditor, bulkHeaders, {
  344. extendedEditorConfig: {
  345. mode: "text/x-yaml",
  346. placeholder: `${t("state.bulk_mode_placeholder")}`,
  347. },
  348. linter: null,
  349. completer: null,
  350. })
  351. const variableEditor = ref<any | null>(null)
  352. useCodemirror(variableEditor, variableString, {
  353. extendedEditorConfig: {
  354. mode: "application/ld+json",
  355. placeholder: `${t("request.variables")}`,
  356. },
  357. linter: jsonLinter,
  358. completer: null,
  359. })
  360. const queryEditor = ref<any | null>(null)
  361. const schemaString = useReadonlyStream(props.conn.schema$, null)
  362. useCodemirror(queryEditor, gqlQueryString, {
  363. extendedEditorConfig: {
  364. mode: "graphql",
  365. placeholder: `${t("request.query")}`,
  366. },
  367. linter: createGQLQueryLinter(schemaString),
  368. completer: queryCompleter(schemaString),
  369. })
  370. const copyQueryIcon = ref("copy")
  371. const prettifyQueryIcon = ref("align-left")
  372. const copyVariablesIcon = ref("copy")
  373. const showSaveRequestModal = ref(false)
  374. watch(
  375. headers,
  376. () => {
  377. if (
  378. (headers.value[headers.value.length - 1]?.key !== "" ||
  379. headers.value[headers.value.length - 1]?.value !== "") &&
  380. headers.value.length
  381. )
  382. addRequestHeader()
  383. },
  384. { deep: true }
  385. )
  386. onMounted(() => {
  387. if (!headers.value?.length) {
  388. addRequestHeader()
  389. }
  390. })
  391. const copyQuery = () => {
  392. copyToClipboard(gqlQueryString.value)
  393. copyQueryIcon.value = "check"
  394. setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
  395. }
  396. const response = useStream(gqlResponse$, "", setGQLResponse)
  397. const runQuery = async () => {
  398. const startTime = Date.now()
  399. nuxt.value.$loading.start()
  400. response.value = "loading"
  401. try {
  402. const runURL = clone(url.value)
  403. const runHeaders = clone(headers.value)
  404. const runQuery = clone(gqlQueryString.value)
  405. const runVariables = clone(variableString.value)
  406. const responseText = await props.conn.runQuery(
  407. runURL,
  408. runHeaders,
  409. runQuery,
  410. runVariables
  411. )
  412. const duration = Date.now() - startTime
  413. nuxt.value.$loading.finish()
  414. response.value = JSON.stringify(JSON.parse(responseText), null, 2)
  415. addGraphqlHistoryEntry(
  416. makeGQLHistoryEntry({
  417. request: makeGQLRequest({
  418. name: "",
  419. url: runURL,
  420. query: runQuery,
  421. headers: runHeaders,
  422. variables: runVariables,
  423. }),
  424. response: response.value,
  425. star: false,
  426. })
  427. )
  428. $toast.success(`${t("state.finished_in", { duration })}`, {
  429. icon: "done",
  430. })
  431. } catch (e: any) {
  432. response.value = `${e}`
  433. nuxt.value.$loading.finish()
  434. $toast.error(
  435. `${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
  436. {
  437. icon: "error_outline",
  438. }
  439. )
  440. console.error(e)
  441. }
  442. logHoppRequestRunToAnalytics({
  443. platform: "graphql-query",
  444. strategy: getCurrentStrategyID(),
  445. })
  446. }
  447. const hideRequestModal = () => {
  448. showSaveRequestModal.value = false
  449. }
  450. const prettifyQuery = () => {
  451. try {
  452. gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
  453. } catch (e) {
  454. $toast.error(`${t("error.gql_prettify_invalid_query")}`, {
  455. icon: "error_outline",
  456. })
  457. }
  458. prettifyQueryIcon.value = "check"
  459. setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
  460. }
  461. const saveRequest = () => {
  462. showSaveRequestModal.value = true
  463. }
  464. const copyVariables = () => {
  465. copyToClipboard(variableString.value)
  466. copyVariablesIcon.value = "check"
  467. setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
  468. }
  469. const addRequestHeader = () => {
  470. addGQLHeader({
  471. key: "",
  472. value: "",
  473. active: true,
  474. })
  475. }
  476. const removeRequestHeader = (index: number) => {
  477. removeGQLHeader(index)
  478. }
  479. </script>