Headers.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <template>
  2. <AppSection label="headers">
  3. <div
  4. class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
  5. >
  6. <label class="font-semibold text-secondaryLight">
  7. {{ t("request.header_list") }}
  8. </label>
  9. <div class="flex">
  10. <ButtonSecondary
  11. v-tippy="{ theme: 'tooltip' }"
  12. to="https://docs.hoppscotch.io/features/headers"
  13. blank
  14. :title="t('app.wiki')"
  15. svg="help-circle"
  16. />
  17. <ButtonSecondary
  18. v-tippy="{ theme: 'tooltip' }"
  19. :title="t('action.clear_all')"
  20. svg="trash-2"
  21. @click.native="clearContent()"
  22. />
  23. <ButtonSecondary
  24. v-tippy="{ theme: 'tooltip' }"
  25. :title="t('state.bulk_mode')"
  26. svg="edit"
  27. :class="{ '!text-accent': bulkMode }"
  28. @click.native="bulkMode = !bulkMode"
  29. />
  30. <ButtonSecondary
  31. v-tippy="{ theme: 'tooltip' }"
  32. :title="t('add.new')"
  33. svg="plus"
  34. :disabled="bulkMode"
  35. @click.native="addHeader"
  36. />
  37. </div>
  38. </div>
  39. <div v-if="bulkMode" ref="bulkEditor"></div>
  40. <div v-else>
  41. <div
  42. v-for="(header, index) in headers$"
  43. :key="`header-${index}`"
  44. class="divide-dividerLight divide-x border-b border-dividerLight flex"
  45. >
  46. <SmartAutoComplete
  47. :placeholder="`${t('count.header', { count: index + 1 })}`"
  48. :source="commonHeaders"
  49. :spellcheck="false"
  50. :value="header.key"
  51. autofocus
  52. styles="
  53. bg-transparent
  54. flex
  55. flex-1
  56. py-1
  57. px-4
  58. truncate
  59. "
  60. class="flex-1 !flex"
  61. @input="
  62. updateHeader(index, {
  63. key: $event,
  64. value: header.value,
  65. active: header.active,
  66. })
  67. "
  68. />
  69. <SmartEnvInput
  70. v-model="header.value"
  71. :placeholder="`${t('count.value', { count: index + 1 })}`"
  72. styles="
  73. bg-transparent
  74. flex
  75. flex-1
  76. py-1
  77. px-4
  78. "
  79. @change="
  80. updateHeader(index, {
  81. key: header.key,
  82. value: $event,
  83. active: header.active,
  84. })
  85. "
  86. />
  87. <span>
  88. <ButtonSecondary
  89. v-tippy="{ theme: 'tooltip' }"
  90. :title="
  91. header.hasOwnProperty('active')
  92. ? header.active
  93. ? t('action.turn_off')
  94. : t('action.turn_on')
  95. : t('action.turn_off')
  96. "
  97. :svg="
  98. header.hasOwnProperty('active')
  99. ? header.active
  100. ? 'check-circle'
  101. : 'circle'
  102. : 'check-circle'
  103. "
  104. color="green"
  105. @click.native="
  106. updateHeader(index, {
  107. key: header.key,
  108. value: header.value,
  109. active: header.hasOwnProperty('active')
  110. ? !header.active
  111. : false,
  112. })
  113. "
  114. />
  115. </span>
  116. <span>
  117. <ButtonSecondary
  118. v-tippy="{ theme: 'tooltip' }"
  119. :title="t('action.remove')"
  120. svg="trash"
  121. color="red"
  122. @click.native="deleteHeader(index)"
  123. />
  124. </span>
  125. </div>
  126. <div
  127. v-if="headers$.length === 0"
  128. class="flex flex-col text-secondaryLight p-4 items-center justify-center"
  129. >
  130. <img
  131. :src="`/images/states/${$colorMode.value}/add_category.svg`"
  132. loading="lazy"
  133. class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
  134. :alt="`${t('empty.headers')}`"
  135. />
  136. <span class="text-center pb-4">
  137. {{ t("empty.headers") }}
  138. </span>
  139. <ButtonSecondary
  140. filled
  141. :label="`${t('add.new')}`"
  142. svg="plus"
  143. class="mb-4"
  144. @click.native="addHeader"
  145. />
  146. </div>
  147. </div>
  148. </AppSection>
  149. </template>
  150. <script setup lang="ts">
  151. import { onBeforeUpdate, ref, watch } from "@nuxtjs/composition-api"
  152. import { HoppRESTHeader } from "@hoppscotch/data"
  153. import { useCodemirror } from "~/helpers/editor/codemirror"
  154. import {
  155. addRESTHeader,
  156. deleteAllRESTHeaders,
  157. deleteRESTHeader,
  158. restHeaders$,
  159. setRESTHeaders,
  160. updateRESTHeader,
  161. } from "~/newstore/RESTSession"
  162. import { commonHeaders } from "~/helpers/headers"
  163. import {
  164. useReadonlyStream,
  165. useI18n,
  166. useToast,
  167. } from "~/helpers/utils/composables"
  168. const t = useI18n()
  169. const toast = useToast()
  170. const bulkMode = ref(false)
  171. const bulkHeaders = ref("")
  172. const bulkEditor = ref<any | null>(null)
  173. useCodemirror(bulkEditor, bulkHeaders, {
  174. extendedEditorConfig: {
  175. mode: "text/x-yaml",
  176. placeholder: `${t("state.bulk_mode_placeholder")}`,
  177. },
  178. linter: null,
  179. completer: null,
  180. })
  181. watch(bulkHeaders, () => {
  182. try {
  183. const transformation = bulkHeaders.value.split("\n").map((item) => ({
  184. key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
  185. value: item.substring(item.indexOf(":") + 1).trim(),
  186. active: !item.trim().startsWith("//"),
  187. }))
  188. setRESTHeaders(transformation as HoppRESTHeader[])
  189. } catch (e) {
  190. toast.error(`${t("error.something_went_wrong")}`)
  191. console.error(e)
  192. }
  193. })
  194. const headers$ = useReadonlyStream(restHeaders$, [])
  195. watch(
  196. headers$,
  197. (newValue) => {
  198. if (!bulkMode.value)
  199. if (
  200. (newValue[newValue.length - 1]?.key !== "" ||
  201. newValue[newValue.length - 1]?.value !== "") &&
  202. newValue.length
  203. )
  204. addHeader()
  205. },
  206. { deep: true }
  207. )
  208. onBeforeUpdate(() => editBulkHeadersLine(-1, null))
  209. const editBulkHeadersLine = (index: number, item?: HoppRESTHeader | null) => {
  210. const headers = headers$.value
  211. bulkHeaders.value = headers
  212. .reduce((all, header, pIndex) => {
  213. const current =
  214. index === pIndex && item != null
  215. ? `${item.active ? "" : "//"}${item.key}: ${item.value}`
  216. : `${header.active ? "" : "//"}${header.key}: ${header.value}`
  217. return [...all, current]
  218. }, [])
  219. .join("\n")
  220. }
  221. const clearBulkEditor = () => {
  222. bulkHeaders.value = ""
  223. }
  224. const addHeader = () => {
  225. const empty = { key: "", value: "", active: true }
  226. const index = headers$.value.length
  227. addRESTHeader(empty)
  228. editBulkHeadersLine(index, empty)
  229. }
  230. const updateHeader = (index: number, item: HoppRESTHeader) => {
  231. updateRESTHeader(index, item)
  232. editBulkHeadersLine(index, item)
  233. }
  234. const deleteHeader = (index: number) => {
  235. const headersBeforeDeletion = headers$.value
  236. deleteRESTHeader(index)
  237. editBulkHeadersLine(index, null)
  238. const deletedItem = headersBeforeDeletion[index]
  239. if (deletedItem.key || deletedItem.value) {
  240. toast.success(`${t("state.deleted")}`, {
  241. action: [
  242. {
  243. text: `${t("action.undo")}`,
  244. onClick: (_, toastObject) => {
  245. setRESTHeaders(headersBeforeDeletion as HoppRESTHeader[])
  246. editBulkHeadersLine(index, deletedItem)
  247. toastObject.goAway(0)
  248. },
  249. },
  250. ],
  251. })
  252. }
  253. }
  254. const clearContent = () => {
  255. deleteAllRESTHeaders()
  256. clearBulkEditor()
  257. }
  258. </script>