RequestOptions.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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/graphql/#queries"
  41. blank
  42. :title="t('app.wiki')"
  43. svg="help-circle"
  44. />
  45. <ButtonSecondary
  46. v-tippy="{ theme: 'tooltip' }"
  47. :title="t('action.prettify')"
  48. :svg="`${prettifyQueryIcon}`"
  49. @click.native="prettifyQuery"
  50. />
  51. <ButtonSecondary
  52. v-tippy="{ theme: 'tooltip' }"
  53. :title="t('action.copy')"
  54. :svg="`${copyQueryIcon}`"
  55. @click.native="copyQuery"
  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/graphql/#queries"
  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/graphql/#headers"
  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. @click.native="clearContent()"
  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-${String(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. "
  172. class="!flex flex-1"
  173. @input="
  174. updateRequestHeader(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 ${String(index)}`"
  185. :value="header.value"
  186. autofocus
  187. @change="
  188. updateRequestHeader(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. updateRequestHeader(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. <img
  243. :src="`/images/states/${$colorMode.value}/add_category.svg`"
  244. loading="lazy"
  245. class="
  246. flex-col
  247. my-4
  248. object-contain object-center
  249. h-16
  250. w-16
  251. inline-flex
  252. "
  253. :alt="`${t('empty.headers')}`"
  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. class="mb-4"
  263. @click.native="addRequestHeader"
  264. />
  265. </div>
  266. </div>
  267. </AppSection>
  268. </SmartTab>
  269. </SmartTabs>
  270. <CollectionsSaveRequest
  271. mode="graphql"
  272. :show="showSaveRequestModal"
  273. @hide-modal="hideRequestModal"
  274. />
  275. </div>
  276. </template>
  277. <script setup lang="ts">
  278. import { onMounted, ref, watch } from "@nuxtjs/composition-api"
  279. import clone from "lodash/clone"
  280. import * as gql from "graphql"
  281. import { copyToClipboard } from "~/helpers/utils/clipboard"
  282. import {
  283. useNuxt,
  284. useReadonlyStream,
  285. useStream,
  286. useI18n,
  287. useToast,
  288. } from "~/helpers/utils/composables"
  289. import {
  290. addGQLHeader,
  291. gqlHeaders$,
  292. gqlQuery$,
  293. gqlResponse$,
  294. gqlURL$,
  295. gqlVariables$,
  296. removeGQLHeader,
  297. setGQLHeaders,
  298. setGQLQuery,
  299. setGQLResponse,
  300. setGQLVariables,
  301. updateGQLHeader,
  302. } from "~/newstore/GQLSession"
  303. import { commonHeaders } from "~/helpers/headers"
  304. import { GQLConnection } from "~/helpers/GQLConnection"
  305. import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
  306. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  307. import { getCurrentStrategyID } from "~/helpers/network"
  308. import { GQLHeader, makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
  309. import { useCodemirror } from "~/helpers/editor/codemirror"
  310. import jsonLinter from "~/helpers/editor/linting/json"
  311. import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
  312. import queryCompleter from "~/helpers/editor/completion/gqlQuery"
  313. const t = useI18n()
  314. const props = defineProps<{
  315. conn: GQLConnection
  316. }>()
  317. const toast = useToast()
  318. const nuxt = useNuxt()
  319. const bulkMode = ref(false)
  320. const bulkHeaders = ref("")
  321. watch(bulkHeaders, () => {
  322. try {
  323. const transformation = bulkHeaders.value.split("\n").map((item) => ({
  324. key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
  325. value: item.substring(item.indexOf(":") + 1).trim(),
  326. active: !item.trim().startsWith("//"),
  327. }))
  328. setGQLHeaders(transformation as GQLHeader[])
  329. } catch (e) {
  330. toast.error(`${t("error.something_went_wrong")}`)
  331. console.error(e)
  332. }
  333. })
  334. const url = useReadonlyStream(gqlURL$, "")
  335. const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
  336. const variableString = useStream(gqlVariables$, "", setGQLVariables)
  337. const headers = useStream(gqlHeaders$, [], setGQLHeaders)
  338. const bulkEditor = ref<any | null>(null)
  339. useCodemirror(bulkEditor, bulkHeaders, {
  340. extendedEditorConfig: {
  341. mode: "text/x-yaml",
  342. placeholder: `${t("state.bulk_mode_placeholder")}`,
  343. },
  344. linter: null,
  345. completer: null,
  346. })
  347. const variableEditor = ref<any | null>(null)
  348. useCodemirror(variableEditor, variableString, {
  349. extendedEditorConfig: {
  350. mode: "application/ld+json",
  351. placeholder: `${t("request.variables")}`,
  352. },
  353. linter: jsonLinter,
  354. completer: null,
  355. })
  356. const queryEditor = ref<any | null>(null)
  357. const schemaString = useReadonlyStream(props.conn.schema$, null)
  358. useCodemirror(queryEditor, gqlQueryString, {
  359. extendedEditorConfig: {
  360. mode: "graphql",
  361. placeholder: `${t("request.query")}`,
  362. },
  363. linter: createGQLQueryLinter(schemaString),
  364. completer: queryCompleter(schemaString),
  365. })
  366. const copyQueryIcon = ref("copy")
  367. const prettifyQueryIcon = ref("wand")
  368. const copyVariablesIcon = ref("copy")
  369. const showSaveRequestModal = ref(false)
  370. watch(
  371. headers,
  372. () => {
  373. if (!bulkMode.value)
  374. if (
  375. (headers.value[headers.value.length - 1]?.key !== "" ||
  376. headers.value[headers.value.length - 1]?.value !== "") &&
  377. headers.value.length
  378. )
  379. addRequestHeader()
  380. },
  381. { deep: true }
  382. )
  383. const editBulkHeadersLine = (index: number, item?: GQLHeader | null) => {
  384. bulkHeaders.value = headers.value
  385. .reduce((all, header, pIndex) => {
  386. const current =
  387. index === pIndex && item != null
  388. ? `${item.active ? "" : "//"}${item.key}: ${item.value}`
  389. : `${header.active ? "" : "//"}${header.key}: ${header.value}`
  390. return [...all, current]
  391. }, [])
  392. .join("\n")
  393. }
  394. const clearBulkEditor = () => {
  395. bulkHeaders.value = ""
  396. }
  397. onMounted(() => {
  398. if (!headers.value?.length) {
  399. addRequestHeader()
  400. }
  401. })
  402. const copyQuery = () => {
  403. copyToClipboard(gqlQueryString.value)
  404. copyQueryIcon.value = "check"
  405. toast.success(`${t("state.copied_to_clipboard")}`)
  406. setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
  407. }
  408. const response = useStream(gqlResponse$, "", setGQLResponse)
  409. const runQuery = async () => {
  410. const startTime = Date.now()
  411. nuxt.value.$loading.start()
  412. response.value = "loading"
  413. try {
  414. const runURL = clone(url.value)
  415. const runHeaders = clone(headers.value)
  416. const runQuery = clone(gqlQueryString.value)
  417. const runVariables = clone(variableString.value)
  418. const responseText = await props.conn.runQuery(
  419. runURL,
  420. runHeaders,
  421. runQuery,
  422. runVariables
  423. )
  424. const duration = Date.now() - startTime
  425. nuxt.value.$loading.finish()
  426. response.value = JSON.stringify(JSON.parse(responseText), null, 2)
  427. addGraphqlHistoryEntry(
  428. makeGQLHistoryEntry({
  429. request: makeGQLRequest({
  430. name: "",
  431. url: runURL,
  432. query: runQuery,
  433. headers: runHeaders,
  434. variables: runVariables,
  435. }),
  436. response: response.value,
  437. star: false,
  438. })
  439. )
  440. toast.success(`${t("state.finished_in", { duration })}`)
  441. } catch (e: any) {
  442. response.value = `${e}`
  443. nuxt.value.$loading.finish()
  444. toast.error(
  445. `${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
  446. {}
  447. )
  448. console.error(e)
  449. }
  450. logHoppRequestRunToAnalytics({
  451. platform: "graphql-query",
  452. strategy: getCurrentStrategyID(),
  453. })
  454. }
  455. const hideRequestModal = () => {
  456. showSaveRequestModal.value = false
  457. }
  458. const prettifyQuery = () => {
  459. try {
  460. gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
  461. prettifyQueryIcon.value = "check"
  462. } catch (e) {
  463. toast.error(`${t("error.gql_prettify_invalid_query")}`)
  464. prettifyQueryIcon.value = "info"
  465. }
  466. setTimeout(() => (prettifyQueryIcon.value = "wand"), 1000)
  467. }
  468. const saveRequest = () => {
  469. showSaveRequestModal.value = true
  470. }
  471. const copyVariables = () => {
  472. copyToClipboard(variableString.value)
  473. copyVariablesIcon.value = "check"
  474. toast.success(`${t("state.copied_to_clipboard")}`)
  475. setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
  476. }
  477. const addRequestHeader = () => {
  478. const empty = { key: "", value: "", active: true }
  479. const index = headers.value.length
  480. addGQLHeader(empty)
  481. editBulkHeadersLine(index, empty)
  482. }
  483. const updateRequestHeader = (
  484. index: number,
  485. item: { key: string; value: string; active: boolean }
  486. ) => {
  487. updateGQLHeader(index, item)
  488. editBulkHeadersLine(index, item)
  489. }
  490. const removeRequestHeader = (index: number) => {
  491. const headersBeforeDeletion = headers.value
  492. removeGQLHeader(index)
  493. editBulkHeadersLine(index, null)
  494. const deletedItem = headersBeforeDeletion[index]
  495. if (deletedItem.key || deletedItem.value) {
  496. toast.success(`${t("state.deleted")}`, {
  497. action: [
  498. {
  499. text: `${t("action.undo")}`,
  500. onClick: (_, toastObject) => {
  501. setGQLHeaders(headersBeforeDeletion as GQLHeader[])
  502. editBulkHeadersLine(index, deletedItem)
  503. toastObject.goAway(0)
  504. },
  505. },
  506. ],
  507. })
  508. }
  509. }
  510. const clearContent = () => {
  511. headers.value = []
  512. clearBulkEditor()
  513. }
  514. </script>