ImportExport.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <SmartModal
  3. v-if="show"
  4. dialog
  5. :title="`${t('environment.title')}`"
  6. max-width="sm:max-w-md"
  7. @close="hideModal"
  8. >
  9. <template #actions>
  10. <span>
  11. <tippy ref="options" interactive trigger="click" theme="popover" arrow>
  12. <template #trigger>
  13. <ButtonSecondary
  14. v-tippy="{ theme: 'tooltip' }"
  15. :title="t('action.more')"
  16. svg="more-vertical"
  17. />
  18. </template>
  19. <div class="flex flex-col" role="menu">
  20. <SmartItem
  21. icon="assignment_returned"
  22. :label="t('import.from_gist')"
  23. @click.native="
  24. () => {
  25. readEnvironmentGist()
  26. options.tippy().hide()
  27. }
  28. "
  29. />
  30. <span
  31. v-tippy="{ theme: 'tooltip' }"
  32. :title="
  33. !currentUser
  34. ? `${t('export.require_github')}`
  35. : currentUser.provider !== 'github.com'
  36. ? `${t('export.require_github')}`
  37. : undefined
  38. "
  39. >
  40. <SmartItem
  41. :disabled="
  42. !currentUser
  43. ? true
  44. : currentUser.provider !== 'github.com'
  45. ? true
  46. : false
  47. "
  48. icon="assignment_turned_in"
  49. :label="t('export.create_secret_gist')"
  50. @click.native="
  51. () => {
  52. createEnvironmentGist()
  53. options.tippy().hide()
  54. }
  55. "
  56. />
  57. </span>
  58. </div>
  59. </tippy>
  60. </span>
  61. </template>
  62. <template #body>
  63. <div class="flex flex-col px-2 space-y-2">
  64. <SmartItem
  65. svg="folder-plus"
  66. :label="t('import.from_json')"
  67. @click.native="openDialogChooseFileToImportFrom"
  68. />
  69. <input
  70. ref="inputChooseFileToImportFrom"
  71. class="input"
  72. type="file"
  73. accept="application/json"
  74. @change="importFromJSON"
  75. />
  76. <hr />
  77. <SmartItem
  78. v-tippy="{ theme: 'tooltip' }"
  79. :title="t('action.download_file')"
  80. svg="download"
  81. :label="t('export.as_json')"
  82. @click.native="exportJSON"
  83. />
  84. </div>
  85. </template>
  86. </SmartModal>
  87. </template>
  88. <script setup lang="ts">
  89. import { computed, ref } from "@nuxtjs/composition-api"
  90. import { Environment } from "@hoppscotch/data"
  91. import { currentUser$ } from "~/helpers/fb/auth"
  92. import {
  93. useAxios,
  94. useI18n,
  95. useReadonlyStream,
  96. useToast,
  97. } from "~/helpers/utils/composables"
  98. import {
  99. environments$,
  100. replaceEnvironments,
  101. appendEnvironments,
  102. } from "~/newstore/environments"
  103. defineProps<{
  104. show: boolean
  105. }>()
  106. const emit = defineEmits<{
  107. (e: "hide-modal"): void
  108. }>()
  109. const axios = useAxios()
  110. const toast = useToast()
  111. const t = useI18n()
  112. const environments = useReadonlyStream(environments$, [])
  113. const currentUser = useReadonlyStream(currentUser$, null)
  114. // Template refs
  115. const options = ref<any>()
  116. const inputChooseFileToImportFrom = ref<HTMLInputElement>()
  117. const environmentJson = computed(() => {
  118. return JSON.stringify(environments.value, null, 2)
  119. })
  120. const createEnvironmentGist = async () => {
  121. if (!currentUser.value) {
  122. toast.error(t("profile.no_permission").toString())
  123. return
  124. }
  125. try {
  126. const res = await axios.$post(
  127. "https://api.github.com/gists",
  128. {
  129. files: {
  130. "hoppscotch-environments.json": {
  131. content: environmentJson.value,
  132. },
  133. },
  134. },
  135. {
  136. headers: {
  137. Authorization: `token ${currentUser.value.accessToken}`,
  138. Accept: "application/vnd.github.v3+json",
  139. },
  140. }
  141. )
  142. toast.success(t("export.gist_created").toString())
  143. window.open(res.html_url)
  144. } catch (e) {
  145. toast.error(t("error.something_went_wrong").toString())
  146. console.error(e)
  147. }
  148. }
  149. const fileImported = () => {
  150. toast.success(t("state.file_imported").toString())
  151. }
  152. const failedImport = () => {
  153. toast.error(t("import.failed").toString())
  154. }
  155. const readEnvironmentGist = async () => {
  156. const gist = prompt(t("import.gist_url").toString())
  157. if (!gist) return
  158. try {
  159. const { files } = (await axios.$get(
  160. `https://api.github.com/gists/${gist.split("/").pop()}`,
  161. {
  162. headers: {
  163. Accept: "application/vnd.github.v3+json",
  164. },
  165. }
  166. )) as {
  167. files: {
  168. [fileName: string]: {
  169. content: any
  170. }
  171. }
  172. }
  173. const environments = JSON.parse(Object.values(files)[0].content)
  174. replaceEnvironments(environments)
  175. fileImported()
  176. } catch (e) {
  177. failedImport()
  178. console.error(e)
  179. }
  180. }
  181. const hideModal = () => {
  182. emit("hide-modal")
  183. }
  184. const openDialogChooseFileToImportFrom = () => {
  185. if (inputChooseFileToImportFrom.value)
  186. inputChooseFileToImportFrom.value.click()
  187. }
  188. const importFromJSON = () => {
  189. if (!inputChooseFileToImportFrom.value) return
  190. if (
  191. !inputChooseFileToImportFrom.value.files ||
  192. inputChooseFileToImportFrom.value.files.length === 0
  193. ) {
  194. toast.show(t("action.choose_file").toString())
  195. return
  196. }
  197. const reader = new FileReader()
  198. reader.onload = ({ target }) => {
  199. const content = target!.result as string | null
  200. if (!content) {
  201. toast.show(t("action.choose_file").toString())
  202. return
  203. }
  204. const environments = JSON.parse(content)
  205. if (
  206. environments._postman_variable_scope === "environment" ||
  207. environments._postman_variable_scope === "globals"
  208. ) {
  209. importFromPostman(environments)
  210. } else if (environments[0]) {
  211. const [name, variables] = Object.keys(environments[0])
  212. if (name === "name" && variables === "variables") {
  213. // Do nothing
  214. }
  215. importFromHoppscotch(environments)
  216. } else {
  217. failedImport()
  218. }
  219. }
  220. reader.readAsText(inputChooseFileToImportFrom.value.files[0])
  221. inputChooseFileToImportFrom.value.value = ""
  222. }
  223. const importFromHoppscotch = (environments: Environment[]) => {
  224. appendEnvironments(environments)
  225. fileImported()
  226. }
  227. const importFromPostman = ({
  228. name,
  229. values,
  230. }: {
  231. name: string
  232. values: { key: string; value: string }[]
  233. }) => {
  234. const environment: Environment = { name, variables: [] }
  235. values.forEach(({ key, value }) => environment.variables.push({ key, value }))
  236. const environments = [environment]
  237. importFromHoppscotch(environments)
  238. }
  239. const exportJSON = () => {
  240. const dataToWrite = environmentJson.value
  241. const file = new Blob([dataToWrite], { type: "application/json" })
  242. const a = document.createElement("a")
  243. const url = URL.createObjectURL(file)
  244. a.href = url
  245. // TODO: get uri from meta
  246. a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
  247. document.body.appendChild(a)
  248. a.click()
  249. toast.success(t("state.download_started").toString())
  250. setTimeout(() => {
  251. document.body.removeChild(a)
  252. URL.revokeObjectURL(url)
  253. }, 1000)
  254. }
  255. </script>