Communication.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <template>
  2. <div class="flex flex-col flex-1">
  3. <div v-if="showEventField" class="flex items-center justify-between p-4">
  4. <input
  5. id="event_name"
  6. v-model="eventName"
  7. class="input"
  8. name="event_name"
  9. :placeholder="`${t('socketio.event_name')}`"
  10. type="text"
  11. autocomplete="off"
  12. />
  13. </div>
  14. <div
  15. class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
  16. >
  17. <span class="flex items-center">
  18. <label class="font-semibold text-secondaryLight">
  19. {{ $t("websocket.message") }}
  20. </label>
  21. <tippy
  22. ref="contentTypeOptions"
  23. interactive
  24. trigger="click"
  25. theme="popover"
  26. arrow
  27. >
  28. <template #trigger>
  29. <span class="select-wrapper">
  30. <ButtonSecondary
  31. :label="contentType || $t('state.none').toLowerCase()"
  32. class="pr-8 ml-2 rounded-none"
  33. />
  34. </span>
  35. </template>
  36. <div class="flex flex-col" role="menu">
  37. <SmartItem
  38. v-for="(contentTypeItem, index) in validContentTypes"
  39. :key="`contentTypeItem-${index}`"
  40. :label="contentTypeItem"
  41. :info-icon="contentTypeItem === contentType ? 'done' : ''"
  42. :active-info-icon="contentTypeItem === contentType"
  43. @click.native="
  44. () => {
  45. contentType = contentTypeItem
  46. $refs.contentTypeOptions.tippy().hide()
  47. }
  48. "
  49. />
  50. </div>
  51. </tippy>
  52. </span>
  53. <div class="flex">
  54. <ButtonSecondary
  55. v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
  56. :title="`${t('action.send')}`"
  57. :label="`${t('action.send')}`"
  58. :disabled="!communicationBody || !isConnected"
  59. svg="send"
  60. class="rounded-none !text-accent !hover:text-accentDark"
  61. @click.native="sendMessage()"
  62. />
  63. <ButtonSecondary
  64. v-tippy="{ theme: 'tooltip' }"
  65. to="https://docs.hoppscotch.io/features/body"
  66. blank
  67. :title="t('app.wiki')"
  68. svg="help-circle"
  69. />
  70. <ButtonSecondary
  71. v-tippy="{ theme: 'tooltip' }"
  72. :title="t('state.linewrap')"
  73. :class="{ '!text-accent': linewrapEnabled }"
  74. svg="wrap-text"
  75. @click.native.prevent="linewrapEnabled = !linewrapEnabled"
  76. />
  77. <ButtonSecondary
  78. v-tippy="{ theme: 'tooltip' }"
  79. :title="t('action.clear')"
  80. svg="trash-2"
  81. @click.native="clearContent"
  82. />
  83. <ButtonSecondary
  84. v-if="contentType && contentType == 'JSON'"
  85. ref="prettifyRequest"
  86. v-tippy="{ theme: 'tooltip' }"
  87. :title="t('action.prettify')"
  88. :svg="prettifyIcon"
  89. @click.native="prettifyRequestBody"
  90. />
  91. <label for="payload">
  92. <ButtonSecondary
  93. v-tippy="{ theme: 'tooltip' }"
  94. :title="t('import.title')"
  95. svg="file-plus"
  96. @click.native="$refs.payload.click()"
  97. />
  98. </label>
  99. <input
  100. ref="payload"
  101. class="input"
  102. name="payload"
  103. type="file"
  104. @change="uploadPayload"
  105. />
  106. </div>
  107. </div>
  108. <div ref="wsCommunicationBody" class="flex flex-col flex-1"></div>
  109. </div>
  110. </template>
  111. <script setup lang="ts">
  112. import { computed, reactive, ref } from "@nuxtjs/composition-api"
  113. import { pipe } from "fp-ts/function"
  114. import * as TO from "fp-ts/TaskOption"
  115. import * as O from "fp-ts/Option"
  116. import { refAutoReset } from "@vueuse/core"
  117. import { useCodemirror } from "~/helpers/editor/codemirror"
  118. import jsonLinter from "~/helpers/editor/linting/json"
  119. import { readFileAsText } from "~/helpers/functional/files"
  120. import { useI18n, useToast } from "~/helpers/utils/composables"
  121. import { isJSONContentType } from "~/helpers/utils/contenttypes"
  122. defineProps({
  123. showEventField: {
  124. type: Boolean,
  125. default: false,
  126. },
  127. isConnected: {
  128. type: Boolean,
  129. default: false,
  130. },
  131. })
  132. const emit = defineEmits<{
  133. (
  134. e: "send-message",
  135. body: {
  136. eventName: string
  137. message: string
  138. }
  139. ): void
  140. }>()
  141. const t = useI18n()
  142. const toast = useToast()
  143. const linewrapEnabled = ref(true)
  144. const wsCommunicationBody = ref<HTMLElement>()
  145. const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
  146. const knownContentTypes = {
  147. JSON: "application/ld+json",
  148. Raw: "text/plain",
  149. } as const
  150. const validContentTypes = Object.keys(knownContentTypes)
  151. const contentType = ref<keyof typeof knownContentTypes>("JSON")
  152. const eventName = ref("")
  153. const communicationBody = ref("")
  154. const rawInputEditorLang = computed(() => knownContentTypes[contentType.value])
  155. const langLinter = computed(() =>
  156. isJSONContentType(contentType.value) ? jsonLinter : null
  157. )
  158. useCodemirror(
  159. wsCommunicationBody,
  160. communicationBody,
  161. reactive({
  162. extendedEditorConfig: {
  163. lineWrapping: linewrapEnabled,
  164. mode: rawInputEditorLang,
  165. placeholder: t("websocket.message").toString(),
  166. },
  167. linter: langLinter,
  168. completer: null,
  169. environmentHighlights: true,
  170. })
  171. )
  172. const clearContent = () => {
  173. communicationBody.value = ""
  174. }
  175. const sendMessage = () => {
  176. if (!communicationBody.value) return
  177. emit("send-message", {
  178. eventName: eventName.value,
  179. message: communicationBody.value,
  180. })
  181. communicationBody.value = ""
  182. }
  183. const uploadPayload = async (e: InputEvent) => {
  184. const result = await pipe(
  185. (e.target as HTMLInputElement).files?.[0],
  186. TO.fromNullable,
  187. TO.chain(readFileAsText)
  188. )()
  189. if (O.isSome(result)) {
  190. communicationBody.value = result.value
  191. toast.success(`${t("state.file_imported")}`)
  192. } else {
  193. toast.error(`${t("action.choose_file")}`)
  194. }
  195. }
  196. const prettifyRequestBody = () => {
  197. try {
  198. const jsonObj = JSON.parse(communicationBody.value)
  199. communicationBody.value = JSON.stringify(jsonObj, null, 2)
  200. prettifyIcon.value = "check"
  201. } catch (e) {
  202. console.error(e)
  203. prettifyIcon.value = "info"
  204. toast.error(`${t("error.json_prettify_invalid_body")}`)
  205. }
  206. }
  207. </script>