index.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <template>
  2. <div class="flex flex-col flex-1">
  3. <header
  4. class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
  5. >
  6. <div class="flex items-center justify-between flex-1 space-x-2">
  7. <HoppButtonSecondary
  8. class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
  9. :label="t('app.name')"
  10. to="https://hoppscotch.io"
  11. blank
  12. />
  13. <div class="flex">
  14. <HoppButtonSecondary
  15. :label="t('app.open_in_hoppscotch')"
  16. :to="sharedRequestURL"
  17. blank
  18. />
  19. </div>
  20. </div>
  21. </header>
  22. <div class="sticky top-0 z-10 flex-1">
  23. <div
  24. class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
  25. >
  26. <div
  27. class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
  28. >
  29. <span
  30. class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
  31. >
  32. {{ tab.document.request.method }}
  33. </span>
  34. <div
  35. class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
  36. >
  37. {{ tab.document.request.endpoint }}
  38. </div>
  39. </div>
  40. <div class="flex mt-2 space-x-2 sm:mt-0">
  41. <HoppButtonPrimary
  42. id="send"
  43. :title="`${t(
  44. 'action.send'
  45. )} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
  46. :label="`${!loading ? t('action.send') : t('action.cancel')}`"
  47. class="flex-1 min-w-20"
  48. outline
  49. @click="!loading ? newSendRequest() : cancelRequest()"
  50. />
  51. <div class="flex">
  52. <HoppButtonSecondary
  53. :title="`${t(
  54. 'request.save'
  55. )} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
  56. :label="t('request.save')"
  57. filled
  58. :icon="IconSave"
  59. class="flex-1 rounded"
  60. blank
  61. outline
  62. :to="sharedRequestURL"
  63. />
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. <HttpRequestOptions
  69. v-model="tab.document.request"
  70. v-model:option-tab="selectedOptionTab"
  71. :properties="properties"
  72. />
  73. <HttpResponse :document="tab.document" :is-embed="true" />
  74. </div>
  75. </template>
  76. <script lang="ts" setup>
  77. import { Ref } from "vue"
  78. import { computed, useModel } from "vue"
  79. import { ref } from "vue"
  80. import { useI18n } from "~/composables/i18n"
  81. import { useToast } from "~/composables/toast"
  82. import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
  83. import * as E from "fp-ts/Either"
  84. import { useStreamSubscriber } from "~/composables/stream"
  85. import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
  86. import { runRESTRequest$ } from "~/helpers/RequestRunner"
  87. import { HoppTab } from "~/services/tab"
  88. import { HoppRESTDocument } from "~/helpers/rest/document"
  89. import IconSave from "~icons/lucide/save"
  90. const t = useI18n()
  91. const toast = useToast()
  92. const props = defineProps<{
  93. modelTab: HoppTab<HoppRESTDocument>
  94. properties: string[]
  95. sharedRequestID: string
  96. }>()
  97. const tab = useModel(props, "modelTab")
  98. const selectedOptionTab = ref(props.properties[0])
  99. const requestCancelFunc: Ref<(() => void) | null> = ref(null)
  100. const loading = ref(false)
  101. const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
  102. const sharedRequestURL = computed(() => {
  103. return `${baseURL}/r/${props.sharedRequestID}`
  104. })
  105. const { subscribeToStream } = useStreamSubscriber()
  106. const newSendRequest = async () => {
  107. if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
  108. toast.error(`${t("empty.endpoint")}`)
  109. return
  110. }
  111. ensureMethodInEndpoint()
  112. loading.value = true
  113. const [cancel, streamPromise] = runRESTRequest$(tab)
  114. const streamResult = await streamPromise
  115. requestCancelFunc.value = cancel
  116. if (E.isRight(streamResult)) {
  117. subscribeToStream(
  118. streamResult.right,
  119. (responseState) => {
  120. if (loading.value) {
  121. // Check exists because, loading can be set to false
  122. // when cancelled
  123. updateRESTResponse(responseState)
  124. }
  125. },
  126. () => {
  127. loading.value = false
  128. },
  129. () => {
  130. // TODO: Change this any to a proper type
  131. const result = (streamResult.right as any).value
  132. if (
  133. result.type === "network_fail" &&
  134. result.error?.error === "NO_PW_EXT_HOOK"
  135. ) {
  136. const errorResponse: HoppRESTResponse = {
  137. type: "extension_error",
  138. error: result.error.humanMessage.heading,
  139. component: result.error.component,
  140. req: result.req,
  141. }
  142. updateRESTResponse(errorResponse)
  143. }
  144. loading.value = false
  145. }
  146. )
  147. } else {
  148. loading.value = false
  149. toast.error(`${t("error.script_fail")}`)
  150. let error: Error
  151. if (typeof streamResult.left === "string") {
  152. error = { name: "RequestFailure", message: streamResult.left }
  153. } else {
  154. error = streamResult.left
  155. }
  156. updateRESTResponse({
  157. type: "script_fail",
  158. error,
  159. })
  160. }
  161. }
  162. const updateRESTResponse = (response: HoppRESTResponse | null) => {
  163. tab.value.document.response = response
  164. }
  165. const newEndpoint = computed(() => {
  166. return tab.value.document.request.endpoint
  167. })
  168. const ensureMethodInEndpoint = () => {
  169. if (
  170. !/^http[s]?:\/\//.test(newEndpoint.value) &&
  171. !newEndpoint.value.startsWith("<<")
  172. ) {
  173. const domain = newEndpoint.value.split(/[/:#?]+/)[0]
  174. if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
  175. tab.value.document.request.endpoint =
  176. "http://" + tab.value.document.request.endpoint
  177. } else {
  178. tab.value.document.request.endpoint =
  179. "https://" + tab.value.document.request.endpoint
  180. }
  181. }
  182. }
  183. const cancelRequest = () => {
  184. loading.value = false
  185. requestCancelFunc.value?.()
  186. updateRESTResponse(null)
  187. }
  188. </script>