Request.vue 12 KB

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