RequestOptions.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  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"
  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"
  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"
  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="bulkMode ? clearBulkEditor() : 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, useContext, 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. } from "~/helpers/utils/composables"
  287. import {
  288. addGQLHeader,
  289. gqlHeaders$,
  290. gqlQuery$,
  291. gqlResponse$,
  292. gqlURL$,
  293. gqlVariables$,
  294. removeGQLHeader,
  295. setGQLHeaders,
  296. setGQLQuery,
  297. setGQLResponse,
  298. setGQLVariables,
  299. updateGQLHeader,
  300. } from "~/newstore/GQLSession"
  301. import { commonHeaders } from "~/helpers/headers"
  302. import { GQLConnection } from "~/helpers/GQLConnection"
  303. import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
  304. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  305. import { getCurrentStrategyID } from "~/helpers/network"
  306. import { GQLHeader, makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
  307. import { useCodemirror } from "~/helpers/editor/codemirror"
  308. import jsonLinter from "~/helpers/editor/linting/json"
  309. import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
  310. import queryCompleter from "~/helpers/editor/completion/gqlQuery"
  311. const props = defineProps<{
  312. conn: GQLConnection
  313. }>()
  314. const {
  315. $toast,
  316. app: { i18n },
  317. } = useContext()
  318. const t = i18n.t.bind(i18n)
  319. const nuxt = useNuxt()
  320. const bulkMode = ref(false)
  321. const bulkHeaders = ref("")
  322. watch(bulkHeaders, () => {
  323. try {
  324. const transformation = bulkHeaders.value.split("\n").map((item) => ({
  325. key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
  326. value: item.substring(item.indexOf(":") + 1).trim(),
  327. active: !item.trim().startsWith("//"),
  328. }))
  329. setGQLHeaders(transformation)
  330. } catch (e) {
  331. $toast.error(`${t("error.something_went_wrong")}`, {
  332. icon: "error_outline",
  333. })
  334. console.error(e)
  335. }
  336. })
  337. const url = useReadonlyStream(gqlURL$, "")
  338. const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
  339. const variableString = useStream(gqlVariables$, "", setGQLVariables)
  340. const headers = useStream(gqlHeaders$, [], setGQLHeaders)
  341. const bulkEditor = ref<any | null>(null)
  342. useCodemirror(bulkEditor, bulkHeaders, {
  343. extendedEditorConfig: {
  344. mode: "text/x-yaml",
  345. placeholder: `${t("state.bulk_mode_placeholder")}`,
  346. },
  347. linter: null,
  348. completer: null,
  349. })
  350. const variableEditor = ref<any | null>(null)
  351. useCodemirror(variableEditor, variableString, {
  352. extendedEditorConfig: {
  353. mode: "application/ld+json",
  354. placeholder: `${t("request.variables")}`,
  355. },
  356. linter: jsonLinter,
  357. completer: null,
  358. })
  359. const queryEditor = ref<any | null>(null)
  360. const schemaString = useReadonlyStream(props.conn.schema$, null)
  361. useCodemirror(queryEditor, gqlQueryString, {
  362. extendedEditorConfig: {
  363. mode: "graphql",
  364. placeholder: `${t("request.query")}`,
  365. },
  366. linter: createGQLQueryLinter(schemaString),
  367. completer: queryCompleter(schemaString),
  368. })
  369. const copyQueryIcon = ref("copy")
  370. const prettifyQueryIcon = ref("wand")
  371. const copyVariablesIcon = ref("copy")
  372. const showSaveRequestModal = ref(false)
  373. watch(
  374. headers,
  375. () => {
  376. if (
  377. (headers.value[headers.value.length - 1]?.key !== "" ||
  378. headers.value[headers.value.length - 1]?.value !== "") &&
  379. headers.value.length
  380. )
  381. addRequestHeader()
  382. },
  383. { deep: true }
  384. )
  385. const editBulkHeadersLine = (index: number, item?: GQLHeader | null) => {
  386. bulkHeaders.value = headers.value
  387. .reduce((all, header, pIndex) => {
  388. const current =
  389. index === pIndex && item != null
  390. ? `${item.active ? "" : "//"}${item.key}: ${item.value}`
  391. : `${header.active ? "" : "//"}${header.key}: ${header.value}`
  392. return [...all, current]
  393. }, [])
  394. .join("\n")
  395. }
  396. const clearBulkEditor = () => {
  397. bulkHeaders.value = ""
  398. }
  399. onMounted(() => {
  400. if (!headers.value?.length) {
  401. addRequestHeader()
  402. }
  403. })
  404. const copyQuery = () => {
  405. copyToClipboard(gqlQueryString.value)
  406. copyQueryIcon.value = "check"
  407. setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
  408. }
  409. const response = useStream(gqlResponse$, "", setGQLResponse)
  410. const runQuery = async () => {
  411. const startTime = Date.now()
  412. nuxt.value.$loading.start()
  413. response.value = "loading"
  414. try {
  415. const runURL = clone(url.value)
  416. const runHeaders = clone(headers.value)
  417. const runQuery = clone(gqlQueryString.value)
  418. const runVariables = clone(variableString.value)
  419. const responseText = await props.conn.runQuery(
  420. runURL,
  421. runHeaders,
  422. runQuery,
  423. runVariables
  424. )
  425. const duration = Date.now() - startTime
  426. nuxt.value.$loading.finish()
  427. response.value = JSON.stringify(JSON.parse(responseText), null, 2)
  428. addGraphqlHistoryEntry(
  429. makeGQLHistoryEntry({
  430. request: makeGQLRequest({
  431. name: "",
  432. url: runURL,
  433. query: runQuery,
  434. headers: runHeaders,
  435. variables: runVariables,
  436. }),
  437. response: response.value,
  438. star: false,
  439. })
  440. )
  441. $toast.success(`${t("state.finished_in", { duration })}`, {
  442. icon: "done",
  443. })
  444. } catch (e: any) {
  445. response.value = `${e}`
  446. nuxt.value.$loading.finish()
  447. $toast.error(
  448. `${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
  449. {
  450. icon: "error_outline",
  451. }
  452. )
  453. console.error(e)
  454. }
  455. logHoppRequestRunToAnalytics({
  456. platform: "graphql-query",
  457. strategy: getCurrentStrategyID(),
  458. })
  459. }
  460. const hideRequestModal = () => {
  461. showSaveRequestModal.value = false
  462. }
  463. const prettifyQuery = () => {
  464. try {
  465. gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
  466. } catch (e) {
  467. $toast.error(`${t("error.gql_prettify_invalid_query")}`, {
  468. icon: "error_outline",
  469. })
  470. }
  471. prettifyQueryIcon.value = "check"
  472. setTimeout(() => (prettifyQueryIcon.value = "wand"), 1000)
  473. }
  474. const saveRequest = () => {
  475. showSaveRequestModal.value = true
  476. }
  477. const copyVariables = () => {
  478. copyToClipboard(variableString.value)
  479. copyVariablesIcon.value = "check"
  480. setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
  481. }
  482. const addRequestHeader = () => {
  483. const empty = { key: "", value: "", active: true }
  484. const index = headers.value.length
  485. addGQLHeader(empty)
  486. editBulkHeadersLine(index, empty)
  487. }
  488. const updateRequestHeader = (
  489. index: number,
  490. item: { key: string; value: string; active: boolean }
  491. ) => {
  492. updateGQLHeader(index, item)
  493. editBulkHeadersLine(index, item)
  494. }
  495. const removeRequestHeader = (index: number) => {
  496. const headersBeforeDeletion = headers.value
  497. removeGQLHeader(index)
  498. editBulkHeadersLine(index, null)
  499. const deletedItem = headersBeforeDeletion[index]
  500. if (deletedItem.key || deletedItem.value) {
  501. $toast.success(t("state.deleted").toString(), {
  502. icon: "delete",
  503. action: [
  504. {
  505. text: t("action.undo").toString(),
  506. onClick: (_, toastObject) => {
  507. setGQLHeaders(headersBeforeDeletion as GQLHeader[])
  508. editBulkHeadersLine(index, deletedItem)
  509. toastObject.goAway(0)
  510. },
  511. },
  512. ],
  513. })
  514. }
  515. }
  516. const clearContent = () => {
  517. headers.value = []
  518. clearBulkEditor()
  519. }
  520. </script>