RequestOptions.vue 14 KB

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