2
0

RequestOptions.vue 16 KB

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