RequestOptions.vue 15 KB

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