RequestOptions.vue 15 KB

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