Request.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div
  3. class="
  4. bg-primary
  5. flex
  6. space-x-2
  7. p-4
  8. top-0
  9. z-10
  10. sticky
  11. overflow-x-auto
  12. hide-scrollbar
  13. "
  14. >
  15. <div class="flex flex-1">
  16. <div class="flex relative">
  17. <label for="method">
  18. <tippy
  19. ref="methodOptions"
  20. interactive
  21. trigger="click"
  22. theme="popover"
  23. arrow
  24. >
  25. <template #trigger>
  26. <span class="select-wrapper">
  27. <input
  28. id="method"
  29. class="
  30. bg-primaryLight
  31. border border-divider
  32. rounded-l
  33. cursor-pointer
  34. flex
  35. font-semibold
  36. text-secondaryDark
  37. py-2
  38. px-4
  39. w-26
  40. hover:border-dividerDark
  41. focus-visible:bg-transparent
  42. focus-visible:border-dividerDark
  43. "
  44. :value="newMethod"
  45. :readonly="!isCustomMethod"
  46. :placeholder="`${$t('request.method')}`"
  47. @input="onSelectMethod($event.target.value)"
  48. />
  49. </span>
  50. </template>
  51. <SmartItem
  52. v-for="(method, index) in methods"
  53. :key="`method-${index}`"
  54. :label="method"
  55. @click.native="onSelectMethod(method)"
  56. />
  57. </tippy>
  58. </label>
  59. </div>
  60. <div class="flex flex-1">
  61. <SmartEnvInput
  62. v-model="newEndpoint"
  63. :placeholder="`${$t('request.url')}`"
  64. styles="
  65. bg-primaryLight
  66. border border-divider
  67. flex
  68. flex-1
  69. rounded-r
  70. text-secondaryDark
  71. min-w-32
  72. py-1
  73. px-4
  74. hover:border-dividerDark
  75. focus-visible:border-dividerDark
  76. focus-visible:bg-transparent
  77. "
  78. @enter="newSendRequest()"
  79. />
  80. </div>
  81. </div>
  82. <div class="flex">
  83. <ButtonPrimary
  84. id="send"
  85. class="rounded-r-none flex-1 min-w-20"
  86. :label="`${!loading ? $t('action.send') : $t('action.cancel')}`"
  87. @click.native="!loading ? newSendRequest() : cancelRequest()"
  88. />
  89. <span class="flex">
  90. <tippy
  91. ref="sendOptions"
  92. interactive
  93. trigger="click"
  94. theme="popover"
  95. arrow
  96. >
  97. <template #trigger>
  98. <ButtonPrimary class="rounded-l-none" filled svg="chevron-down" />
  99. </template>
  100. <SmartItem
  101. :label="`${$t('import.curl')}`"
  102. svg="terminal"
  103. @click.native="
  104. () => {
  105. showCurlImportModal = !showCurlImportModal
  106. sendOptions.tippy().hide()
  107. }
  108. "
  109. />
  110. <SmartItem
  111. :label="`${$t('show.code')}`"
  112. svg="code"
  113. @click.native="
  114. () => {
  115. showCodegenModal = !showCodegenModal
  116. sendOptions.tippy().hide()
  117. }
  118. "
  119. />
  120. <SmartItem
  121. ref="clearAll"
  122. :label="`${$t('action.clear_all')}`"
  123. svg="rotate-ccw"
  124. @click.native="
  125. () => {
  126. clearContent()
  127. sendOptions.tippy().hide()
  128. }
  129. "
  130. />
  131. </tippy>
  132. </span>
  133. <ButtonSecondary
  134. class="rounded-r-none rounded-l ml-2"
  135. :label="
  136. windowInnerWidth.x.value >= 768 && COLUMN_LAYOUT
  137. ? `${$t('request.save')}`
  138. : ''
  139. "
  140. filled
  141. svg="save"
  142. @click.native="saveRequest()"
  143. />
  144. <span class="flex">
  145. <tippy
  146. ref="saveOptions"
  147. interactive
  148. trigger="click"
  149. theme="popover"
  150. arrow
  151. >
  152. <template #trigger>
  153. <ButtonSecondary svg="chevron-down" filled class="rounded-r" />
  154. </template>
  155. <input
  156. id="request-name"
  157. v-model="requestName"
  158. :placeholder="`${$t('request.name')}`"
  159. name="request-name"
  160. type="text"
  161. autocomplete="off"
  162. class="mb-2 input"
  163. @keyup.enter="saveOptions.tippy().hide()"
  164. />
  165. <SmartItem
  166. ref="copyRequest"
  167. :label="`${$t('request.copy_link')}`"
  168. :svg="hasNavigatorShare ? 'share-2' : 'copy'"
  169. @click.native="
  170. () => {
  171. copyRequest()
  172. saveOptions.tippy().hide()
  173. }
  174. "
  175. />
  176. <SmartItem
  177. ref="saveRequest"
  178. :label="`${$t('request.save_as')}`"
  179. svg="folder-plus"
  180. @click.native="
  181. () => {
  182. showSaveRequestModal = true
  183. saveOptions.tippy().hide()
  184. }
  185. "
  186. />
  187. </tippy>
  188. </span>
  189. </div>
  190. <HttpImportCurl
  191. :show="showCurlImportModal"
  192. @hide-modal="showCurlImportModal = false"
  193. />
  194. <HttpCodegenModal
  195. :show="showCodegenModal"
  196. @hide-modal="showCodegenModal = false"
  197. />
  198. <CollectionsSaveRequest
  199. mode="rest"
  200. :show="showSaveRequestModal"
  201. @hide-modal="showSaveRequestModal = false"
  202. />
  203. </div>
  204. </template>
  205. <script setup lang="ts">
  206. import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
  207. import { isRight } from "fp-ts/lib/Either"
  208. import {
  209. updateRESTResponse,
  210. restEndpoint$,
  211. setRESTEndpoint,
  212. restMethod$,
  213. updateRESTMethod,
  214. resetRESTRequest,
  215. useRESTRequestName,
  216. getRESTSaveContext,
  217. getRESTRequest,
  218. } from "~/newstore/RESTSession"
  219. import { editRESTRequest } from "~/newstore/collections"
  220. import { runRESTRequest$ } from "~/helpers/RequestRunner"
  221. import {
  222. useStreamSubscriber,
  223. useStream,
  224. useNuxt,
  225. } from "~/helpers/utils/composables"
  226. import { defineActionHandler } from "~/helpers/actions"
  227. import { copyToClipboard } from "~/helpers/utils/clipboard"
  228. import { useSetting } from "~/newstore/settings"
  229. import { overwriteRequestTeams } from "~/helpers/teams/utils"
  230. import { apolloClient } from "~/helpers/apollo"
  231. import useWindowSize from "~/helpers/utils/useWindowSize"
  232. const methods = [
  233. "GET",
  234. "POST",
  235. "PUT",
  236. "PATCH",
  237. "DELETE",
  238. "HEAD",
  239. "CONNECT",
  240. "OPTIONS",
  241. "TRACE",
  242. "CUSTOM",
  243. ]
  244. const {
  245. $toast,
  246. app: { i18n },
  247. } = useContext()
  248. const nuxt = useNuxt()
  249. const t = i18n.t.bind(i18n)
  250. const { subscribeToStream } = useStreamSubscriber()
  251. const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
  252. const newMethod = useStream(restMethod$, "", updateRESTMethod)
  253. const loading = ref(false)
  254. const showCurlImportModal = ref(false)
  255. const showCodegenModal = ref(false)
  256. const showSaveRequestModal = ref(false)
  257. const hasNavigatorShare = !!navigator.share
  258. // Template refs
  259. const methodOptions = ref<any | null>(null)
  260. const saveOptions = ref<any | null>(null)
  261. const sendOptions = ref<any | null>(null)
  262. // Update Nuxt Loading bar
  263. watch(loading, () => {
  264. if (loading.value) {
  265. nuxt.value.$loading.start()
  266. } else {
  267. nuxt.value.$loading.finish()
  268. }
  269. })
  270. const newSendRequest = async () => {
  271. loading.value = true
  272. // Double calling is because the function returns a TaskEither than should be executed
  273. const streamResult = await runRESTRequest$()()
  274. // TODO: What if stream fetching failed (script execution errors ?) (isLeft)
  275. if (isRight(streamResult)) {
  276. subscribeToStream(
  277. streamResult.right,
  278. (responseState) => {
  279. if (loading.value) {
  280. // Check exists because, loading can be set to false
  281. // when cancelled
  282. updateRESTResponse(responseState)
  283. }
  284. },
  285. () => {
  286. loading.value = false
  287. },
  288. () => {
  289. loading.value = false
  290. }
  291. )
  292. }
  293. }
  294. const cancelRequest = () => {
  295. loading.value = false
  296. updateRESTResponse(null)
  297. }
  298. const updateMethod = (method: string) => {
  299. updateRESTMethod(method)
  300. }
  301. const onSelectMethod = (method: string) => {
  302. updateMethod(method)
  303. // Vue-tippy has no typescript support yet
  304. methodOptions.value.tippy().hide()
  305. }
  306. const clearContent = () => {
  307. resetRESTRequest()
  308. }
  309. const copyRequest = () => {
  310. if (navigator.share) {
  311. const time = new Date().toLocaleTimeString()
  312. const date = new Date().toLocaleDateString()
  313. navigator
  314. .share({
  315. title: "Hoppscotch",
  316. text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
  317. url: window.location.href,
  318. })
  319. .then(() => {})
  320. .catch(() => {})
  321. } else {
  322. copyToClipboard(window.location.href)
  323. $toast.success(`${t("state.copied_to_clipboard")}`, {
  324. icon: "content_paste",
  325. })
  326. }
  327. }
  328. const cycleUpMethod = () => {
  329. const currentIndex = methods.indexOf(newMethod.value)
  330. if (currentIndex === -1) {
  331. // Most probs we are in CUSTOM mode
  332. // Cycle up from CUSTOM is PATCH
  333. updateMethod("PATCH")
  334. } else if (currentIndex === 0) {
  335. updateMethod("CUSTOM")
  336. } else {
  337. updateMethod(methods[currentIndex - 1])
  338. }
  339. }
  340. const cycleDownMethod = () => {
  341. const currentIndex = methods.indexOf(newMethod.value)
  342. if (currentIndex === -1) {
  343. // Most probs we are in CUSTOM mode
  344. // Cycle down from CUSTOM is GET
  345. updateMethod("GET")
  346. } else if (currentIndex === methods.length - 1) {
  347. updateMethod("GET")
  348. } else {
  349. updateMethod(methods[currentIndex + 1])
  350. }
  351. }
  352. const saveRequest = () => {
  353. const saveCtx = getRESTSaveContext()
  354. if (!saveCtx) {
  355. showSaveRequestModal.value = true
  356. return
  357. }
  358. if (saveCtx.originLocation === "user-collection") {
  359. editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, getRESTRequest())
  360. } else if (saveCtx.originLocation === "team-collection") {
  361. const req = getRESTRequest()
  362. // TODO: handle error case (NOTE: overwriteRequestTeams is async)
  363. try {
  364. overwriteRequestTeams(
  365. apolloClient,
  366. JSON.stringify(req),
  367. req.name,
  368. saveCtx.requestID
  369. )
  370. } catch (error) {
  371. showSaveRequestModal.value = true
  372. return
  373. }
  374. }
  375. $toast.success(`${t("request.saved")}`, {
  376. icon: "playlist_add_check",
  377. })
  378. }
  379. defineActionHandler("request.send-cancel", () => {
  380. if (!loading.value) newSendRequest()
  381. else cancelRequest()
  382. })
  383. defineActionHandler("request.reset", clearContent)
  384. defineActionHandler("request.copy-link", copyRequest)
  385. defineActionHandler("request.method.next", cycleDownMethod)
  386. defineActionHandler("request.method.prev", cycleUpMethod)
  387. defineActionHandler("request.save", saveRequest)
  388. defineActionHandler(
  389. "request.save-as",
  390. () => (showSaveRequestModal.value = true)
  391. )
  392. defineActionHandler("request.method.get", () => updateMethod("GET"))
  393. defineActionHandler("request.method.post", () => updateMethod("POST"))
  394. defineActionHandler("request.method.put", () => updateMethod("PUT"))
  395. defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
  396. defineActionHandler("request.method.head", () => updateMethod("HEAD"))
  397. const isCustomMethod = computed(() => {
  398. return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
  399. })
  400. const requestName = useRESTRequestName()
  401. const windowInnerWidth = useWindowSize()
  402. const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
  403. </script>