RequestOptions.vue 15 KB

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