Request.vue 11 KB

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