RequestOptions.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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.copy')"
  48. :svg="`${copyQueryIcon}`"
  49. @click.native="copyQuery"
  50. />
  51. <ButtonSecondary
  52. v-tippy="{ theme: 'tooltip' }"
  53. :title="$t('action.prettify')"
  54. :svg="`${prettifyQueryIcon}`"
  55. @click.native="prettifyQuery"
  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. :disabled="Boolean(bulkMode)"
  130. @click.native="headers = []"
  131. />
  132. <ButtonSecondary
  133. v-tippy="{ theme: 'tooltip' }"
  134. :title="$t('state.bulk_mode')"
  135. svg="edit"
  136. :class="{ '!text-accent': bulkMode }"
  137. @click.native="bulkMode = !bulkMode"
  138. />
  139. <ButtonSecondary
  140. v-tippy="{ theme: 'tooltip' }"
  141. :title="$t('add.new')"
  142. svg="plus"
  143. :disabled="Boolean(bulkMode)"
  144. @click.native="addRequestHeader"
  145. />
  146. </div>
  147. </div>
  148. <div v-if="bulkMode" ref="bulkEditor"></div>
  149. <div v-else>
  150. <div
  151. v-for="(header, index) in headers"
  152. :key="`header-${String(index)}`"
  153. class="
  154. divide-x divide-dividerLight
  155. border-b border-dividerLight
  156. flex
  157. "
  158. >
  159. <SmartAutoComplete
  160. :placeholder="`${$t('count.header', { count: index + 1 })}`"
  161. :source="commonHeaders"
  162. :spellcheck="false"
  163. :value="header.key"
  164. autofocus
  165. styles="
  166. bg-transparent
  167. flex
  168. flex-1
  169. py-1
  170. px-4
  171. truncate
  172. focus:outline-none
  173. "
  174. @input="
  175. updateGQLHeader(index, {
  176. key: $event,
  177. value: header.value,
  178. active: header.active,
  179. })
  180. "
  181. />
  182. <input
  183. class="bg-transparent flex flex-1 py-2 px-4"
  184. :placeholder="`${$t('count.value', { count: index + 1 })}`"
  185. :name="`value ${String(index)}`"
  186. :value="header.value"
  187. autofocus
  188. @change="
  189. updateGQLHeader(index, {
  190. key: header.key,
  191. value: $event.target.value,
  192. active: header.active,
  193. })
  194. "
  195. />
  196. <span>
  197. <ButtonSecondary
  198. v-tippy="{ theme: 'tooltip' }"
  199. :title="
  200. header.hasOwnProperty('active')
  201. ? header.active
  202. ? $t('action.turn_off')
  203. : $t('action.turn_on')
  204. : $t('action.turn_off')
  205. "
  206. :svg="
  207. header.hasOwnProperty('active')
  208. ? header.active
  209. ? 'check-circle'
  210. : 'circle'
  211. : 'check-circle'
  212. "
  213. color="green"
  214. @click.native="
  215. updateGQLHeader(index, {
  216. key: header.key,
  217. value: header.value,
  218. active: !header.active,
  219. })
  220. "
  221. />
  222. </span>
  223. <span>
  224. <ButtonSecondary
  225. v-tippy="{ theme: 'tooltip' }"
  226. :title="$t('action.remove')"
  227. svg="trash"
  228. color="red"
  229. @click.native="removeRequestHeader(index)"
  230. />
  231. </span>
  232. </div>
  233. <div
  234. v-if="headers.length === 0"
  235. class="
  236. flex flex-col
  237. text-secondaryLight
  238. p-4
  239. items-center
  240. justify-center
  241. "
  242. >
  243. <span class="text-center pb-4">
  244. {{ $t("empty.headers") }}
  245. </span>
  246. <ButtonSecondary
  247. :label="`${$t('add.new')}`"
  248. filled
  249. svg="plus"
  250. @click.native="addRequestHeader"
  251. />
  252. </div>
  253. </div>
  254. </AppSection>
  255. </SmartTab>
  256. </SmartTabs>
  257. <CollectionsSaveRequest
  258. mode="graphql"
  259. :show="Boolean(showSaveRequestModal)"
  260. @hide-modal="hideRequestModal"
  261. />
  262. </div>
  263. </template>
  264. <script setup lang="ts">
  265. import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
  266. import clone from "lodash/clone"
  267. import * as gql from "graphql"
  268. import { copyToClipboard } from "~/helpers/utils/clipboard"
  269. import {
  270. useNuxt,
  271. useReadonlyStream,
  272. useStream,
  273. } from "~/helpers/utils/composables"
  274. import {
  275. addGQLHeader,
  276. gqlHeaders$,
  277. gqlQuery$,
  278. gqlResponse$,
  279. gqlURL$,
  280. gqlVariables$,
  281. removeGQLHeader,
  282. setGQLHeaders,
  283. setGQLQuery,
  284. setGQLResponse,
  285. setGQLVariables,
  286. updateGQLHeader,
  287. } from "~/newstore/GQLSession"
  288. import { commonHeaders } from "~/helpers/headers"
  289. import { GQLConnection } from "~/helpers/GQLConnection"
  290. import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
  291. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  292. import { getCurrentStrategyID } from "~/helpers/network"
  293. import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
  294. import { useCodemirror } from "~/helpers/editor/codemirror"
  295. import "codemirror/mode/javascript/javascript"
  296. import "~/helpers/editor/modes/graphql"
  297. import jsonLinter from "~/helpers/editor/linting/json"
  298. import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
  299. import queryCompleter from "~/helpers/editor/completion/gqlQuery"
  300. const props = defineProps<{
  301. conn: GQLConnection
  302. }>()
  303. const {
  304. $toast,
  305. app: { i18n },
  306. } = useContext()
  307. const t = i18n.t.bind(i18n)
  308. const nuxt = useNuxt()
  309. const bulkMode = ref(false)
  310. const bulkHeaders = ref("")
  311. watch(bulkHeaders, () => {
  312. try {
  313. const transformation = bulkHeaders.value.split("\n").map((item) => ({
  314. key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
  315. value: item.substring(item.indexOf(":") + 1).trim(),
  316. active: !item.trim().startsWith("//"),
  317. }))
  318. setGQLHeaders(transformation)
  319. } catch (e) {
  320. $toast.error(`${t("error.something_went_wrong")}`, {
  321. icon: "error_outline",
  322. })
  323. console.error(e)
  324. }
  325. })
  326. const url = useReadonlyStream(gqlURL$, "")
  327. const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
  328. const variableString = useStream(gqlVariables$, "", setGQLVariables)
  329. const headers = useStream(gqlHeaders$, [], setGQLHeaders)
  330. const bulkEditor = ref<any | null>(null)
  331. useCodemirror(bulkEditor, bulkHeaders, {
  332. extendedEditorConfig: {
  333. mode: "text/x-yaml",
  334. placeholder: `${t("state.bulk_mode_placeholder")}`,
  335. },
  336. linter: null,
  337. completer: null,
  338. })
  339. const variableEditor = ref<any | null>(null)
  340. useCodemirror(variableEditor, variableString, {
  341. extendedEditorConfig: {
  342. mode: "application/ld+json",
  343. placeholder: `${t("request.variables")}`,
  344. },
  345. linter: jsonLinter,
  346. completer: null,
  347. })
  348. const queryEditor = ref<any | null>(null)
  349. const schemaString = useReadonlyStream(props.conn.schema$, null)
  350. useCodemirror(queryEditor, gqlQueryString, {
  351. extendedEditorConfig: {
  352. mode: "graphql",
  353. placeholder: `${t("request.query")}`,
  354. },
  355. linter: createGQLQueryLinter(schemaString),
  356. completer: queryCompleter(schemaString),
  357. })
  358. const copyQueryIcon = ref("copy")
  359. const prettifyQueryIcon = ref("align-left")
  360. const copyVariablesIcon = ref("copy")
  361. const showSaveRequestModal = ref(false)
  362. watch(
  363. headers,
  364. () => {
  365. if (
  366. (headers.value[headers.value.length - 1]?.key !== "" ||
  367. headers.value[headers.value.length - 1]?.value !== "") &&
  368. headers.value.length
  369. )
  370. addRequestHeader()
  371. },
  372. { deep: true }
  373. )
  374. onMounted(() => {
  375. if (!headers.value?.length) {
  376. addRequestHeader()
  377. }
  378. })
  379. const copyQuery = () => {
  380. copyToClipboard(gqlQueryString.value)
  381. copyQueryIcon.value = "check"
  382. setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
  383. }
  384. const response = useStream(gqlResponse$, "", setGQLResponse)
  385. const runQuery = async () => {
  386. const startTime = Date.now()
  387. nuxt.value.$loading.start()
  388. response.value = "loading"
  389. try {
  390. const runURL = clone(url.value)
  391. const runHeaders = clone(headers.value)
  392. const runQuery = clone(gqlQueryString.value)
  393. const runVariables = clone(variableString.value)
  394. const responseText = await props.conn.runQuery(
  395. runURL,
  396. runHeaders,
  397. runQuery,
  398. runVariables
  399. )
  400. const duration = Date.now() - startTime
  401. nuxt.value.$loading.finish()
  402. response.value = JSON.stringify(JSON.parse(responseText), null, 2)
  403. addGraphqlHistoryEntry(
  404. makeGQLHistoryEntry({
  405. request: makeGQLRequest({
  406. name: "",
  407. url: runURL,
  408. query: runQuery,
  409. headers: runHeaders,
  410. variables: runVariables,
  411. }),
  412. response: response.value,
  413. star: false,
  414. })
  415. )
  416. $toast.success(`${t("state.finished_in", { duration })}`, {
  417. icon: "done",
  418. })
  419. } catch (e: any) {
  420. response.value = `${e}. ${t("error.check_console_details")}`
  421. nuxt.value.$loading.finish()
  422. $toast.error(`${e} ${t("error.f12_details")}`, {
  423. icon: "error_outline",
  424. })
  425. console.error(e)
  426. }
  427. logHoppRequestRunToAnalytics({
  428. platform: "graphql-query",
  429. strategy: getCurrentStrategyID(),
  430. })
  431. }
  432. const hideRequestModal = () => {
  433. showSaveRequestModal.value = false
  434. }
  435. const prettifyQuery = () => {
  436. try {
  437. gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
  438. } catch (e) {
  439. $toast.error(`${t("error.gql_prettify_invalid_query")}`, {
  440. icon: "error_outline",
  441. })
  442. }
  443. prettifyQueryIcon.value = "check"
  444. setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
  445. }
  446. const saveRequest = () => {
  447. showSaveRequestModal.value = true
  448. }
  449. const copyVariables = () => {
  450. copyToClipboard(variableString.value)
  451. copyVariablesIcon.value = "check"
  452. setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
  453. }
  454. const addRequestHeader = () => {
  455. addGQLHeader({
  456. key: "",
  457. value: "",
  458. active: true,
  459. })
  460. }
  461. const removeRequestHeader = (index: number) => {
  462. removeGQLHeader(index)
  463. }
  464. </script>