Request.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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="file-code"
  103. @click.native="
  104. () => {
  105. showCurlImportModal = !showCurlImportModal
  106. sendOptions.tippy().hide()
  107. }
  108. "
  109. />
  110. <SmartItem
  111. :label="`${$t('show.code')}`"
  112. svg="code-2"
  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 rounded-r-none 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
  154. svg="chevron-down"
  155. filled
  156. class="rounded rounded-l-none"
  157. />
  158. </template>
  159. <input
  160. id="request-name"
  161. v-model="requestName"
  162. :placeholder="`${$t('request.name')}`"
  163. name="request-name"
  164. type="text"
  165. autocomplete="off"
  166. class="mb-2 input"
  167. @keyup.enter="saveOptions.tippy().hide()"
  168. />
  169. <SmartItem
  170. ref="copyRequest"
  171. :label="`${$t('request.copy_link')}`"
  172. :svg="hasNavigatorShare ? 'share-2' : 'copy'"
  173. @click.native="
  174. () => {
  175. copyRequest()
  176. saveOptions.tippy().hide()
  177. }
  178. "
  179. />
  180. <SmartItem
  181. ref="saveRequest"
  182. :label="`${$t('request.save_as')}`"
  183. svg="folder-plus"
  184. @click.native="
  185. () => {
  186. showSaveRequestModal = true
  187. saveOptions.tippy().hide()
  188. }
  189. "
  190. />
  191. </tippy>
  192. </span>
  193. </div>
  194. <HttpImportCurl
  195. :show="showCurlImportModal"
  196. @hide-modal="showCurlImportModal = false"
  197. />
  198. <HttpCodegenModal
  199. :show="showCodegenModal"
  200. @hide-modal="showCodegenModal = false"
  201. />
  202. <CollectionsSaveRequest
  203. mode="rest"
  204. :show="showSaveRequestModal"
  205. @hide-modal="showSaveRequestModal = false"
  206. />
  207. </div>
  208. </template>
  209. <script setup lang="ts">
  210. import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
  211. import { isRight } from "fp-ts/lib/Either"
  212. import {
  213. updateRESTResponse,
  214. restEndpoint$,
  215. setRESTEndpoint,
  216. restMethod$,
  217. updateRESTMethod,
  218. resetRESTRequest,
  219. useRESTRequestName,
  220. getRESTSaveContext,
  221. getRESTRequest,
  222. } from "~/newstore/RESTSession"
  223. import { editRESTRequest } from "~/newstore/collections"
  224. import { runRESTRequest$ } from "~/helpers/RequestRunner"
  225. import {
  226. useStreamSubscriber,
  227. useStream,
  228. useNuxt,
  229. } from "~/helpers/utils/composables"
  230. import { defineActionHandler } from "~/helpers/actions"
  231. import { copyToClipboard } from "~/helpers/utils/clipboard"
  232. import { useSetting } from "~/newstore/settings"
  233. import { overwriteRequestTeams } from "~/helpers/teams/utils"
  234. import { apolloClient } from "~/helpers/apollo"
  235. import useWindowSize from "~/helpers/utils/useWindowSize"
  236. const methods = [
  237. "GET",
  238. "POST",
  239. "PUT",
  240. "PATCH",
  241. "DELETE",
  242. "HEAD",
  243. "CONNECT",
  244. "OPTIONS",
  245. "TRACE",
  246. "CUSTOM",
  247. ]
  248. const {
  249. $toast,
  250. app: { i18n },
  251. } = useContext()
  252. const nuxt = useNuxt()
  253. const t = i18n.t.bind(i18n)
  254. const { subscribeToStream } = useStreamSubscriber()
  255. const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
  256. const newMethod = useStream(restMethod$, "", updateRESTMethod)
  257. const loading = ref(false)
  258. const showCurlImportModal = ref(false)
  259. const showCodegenModal = ref(false)
  260. const showSaveRequestModal = ref(false)
  261. const hasNavigatorShare = !!navigator.share
  262. // Template refs
  263. const methodOptions = ref<any | null>(null)
  264. const saveOptions = ref<any | null>(null)
  265. const sendOptions = ref<any | null>(null)
  266. // Update Nuxt Loading bar
  267. watch(loading, () => {
  268. if (loading.value) {
  269. nuxt.value.$loading.start()
  270. } else {
  271. nuxt.value.$loading.finish()
  272. }
  273. })
  274. const newSendRequest = async () => {
  275. loading.value = true
  276. // Double calling is because the function returns a TaskEither than should be executed
  277. const streamResult = await runRESTRequest$()()
  278. // TODO: What if stream fetching failed (script execution errors ?) (isLeft)
  279. if (isRight(streamResult)) {
  280. subscribeToStream(
  281. streamResult.right,
  282. (responseState) => {
  283. if (loading.value) {
  284. // Check exists because, loading can be set to false
  285. // when cancelled
  286. updateRESTResponse(responseState)
  287. }
  288. },
  289. () => {
  290. loading.value = false
  291. },
  292. () => {
  293. loading.value = false
  294. }
  295. )
  296. }
  297. }
  298. const cancelRequest = () => {
  299. loading.value = false
  300. updateRESTResponse(null)
  301. }
  302. const updateMethod = (method: string) => {
  303. updateRESTMethod(method)
  304. }
  305. const onSelectMethod = (method: string) => {
  306. updateMethod(method)
  307. // Vue-tippy has no typescript support yet
  308. methodOptions.value.tippy().hide()
  309. }
  310. const clearContent = () => {
  311. resetRESTRequest()
  312. }
  313. const copyRequest = () => {
  314. if (navigator.share) {
  315. const time = new Date().toLocaleTimeString()
  316. const date = new Date().toLocaleDateString()
  317. navigator
  318. .share({
  319. title: "Hoppscotch",
  320. text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
  321. url: window.location.href,
  322. })
  323. .then(() => {})
  324. .catch(() => {})
  325. } else {
  326. copyToClipboard(window.location.href)
  327. $toast.success(`${t("state.copied_to_clipboard")}`, {
  328. icon: "content_paste",
  329. })
  330. }
  331. }
  332. const cycleUpMethod = () => {
  333. const currentIndex = methods.indexOf(newMethod.value)
  334. if (currentIndex === -1) {
  335. // Most probs we are in CUSTOM mode
  336. // Cycle up from CUSTOM is PATCH
  337. updateMethod("PATCH")
  338. } else if (currentIndex === 0) {
  339. updateMethod("CUSTOM")
  340. } else {
  341. updateMethod(methods[currentIndex - 1])
  342. }
  343. }
  344. const cycleDownMethod = () => {
  345. const currentIndex = methods.indexOf(newMethod.value)
  346. if (currentIndex === -1) {
  347. // Most probs we are in CUSTOM mode
  348. // Cycle down from CUSTOM is GET
  349. updateMethod("GET")
  350. } else if (currentIndex === methods.length - 1) {
  351. updateMethod("GET")
  352. } else {
  353. updateMethod(methods[currentIndex + 1])
  354. }
  355. }
  356. const saveRequest = () => {
  357. const saveCtx = getRESTSaveContext()
  358. if (!saveCtx) {
  359. showSaveRequestModal.value = true
  360. return
  361. }
  362. if (saveCtx.originLocation === "user-collection") {
  363. editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, getRESTRequest())
  364. $toast.success(`${t("request.saved")}`, {
  365. icon: "playlist_add_check",
  366. })
  367. } else if (saveCtx.originLocation === "team-collection") {
  368. const req = getRESTRequest()
  369. // TODO: handle error case (NOTE: overwriteRequestTeams is async)
  370. try {
  371. overwriteRequestTeams(
  372. apolloClient,
  373. JSON.stringify(req),
  374. req.name,
  375. saveCtx.requestID
  376. )
  377. .then(() => {
  378. $toast.success(`${t("request.saved")}`, {
  379. icon: "playlist_add_check",
  380. })
  381. })
  382. .catch(() => {
  383. $toast.error(t("profile.no_permission").toString(), {
  384. icon: "error_outline",
  385. })
  386. })
  387. } catch (error) {
  388. showSaveRequestModal.value = true
  389. $toast.error(t("error.something_went_wrong").toString(), {
  390. icon: "error_outline",
  391. })
  392. console.error(error)
  393. }
  394. }
  395. }
  396. defineActionHandler("request.send-cancel", () => {
  397. if (!loading.value) newSendRequest()
  398. else cancelRequest()
  399. })
  400. defineActionHandler("request.reset", clearContent)
  401. defineActionHandler("request.copy-link", copyRequest)
  402. defineActionHandler("request.method.next", cycleDownMethod)
  403. defineActionHandler("request.method.prev", cycleUpMethod)
  404. defineActionHandler("request.save", saveRequest)
  405. defineActionHandler(
  406. "request.save-as",
  407. () => (showSaveRequestModal.value = true)
  408. )
  409. defineActionHandler("request.method.get", () => updateMethod("GET"))
  410. defineActionHandler("request.method.post", () => updateMethod("POST"))
  411. defineActionHandler("request.method.put", () => updateMethod("PUT"))
  412. defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
  413. defineActionHandler("request.method.head", () => updateMethod("HEAD"))
  414. const isCustomMethod = computed(() => {
  415. return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
  416. })
  417. const requestName = useRESTRequestName()
  418. const windowInnerWidth = useWindowSize()
  419. const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
  420. </script>