ImportExport.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <SmartModal
  3. v-if="show"
  4. dialog
  5. :title="`${t('modal.collections')}`"
  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. readCollectionGist()
  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. createCollectionGist()
  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 { currentUser$ } from "~/helpers/fb/auth"
  91. import {
  92. useAxios,
  93. useI18n,
  94. useReadonlyStream,
  95. useToast,
  96. } from "~/helpers/utils/composables"
  97. import {
  98. graphqlCollections$,
  99. setGraphqlCollections,
  100. appendGraphqlCollections,
  101. } from "~/newstore/collections"
  102. defineProps<{
  103. show: boolean
  104. }>()
  105. const emit = defineEmits<{
  106. (e: "hide-modal"): void
  107. }>()
  108. const axios = useAxios()
  109. const toast = useToast()
  110. const t = useI18n()
  111. const collections = useReadonlyStream(graphqlCollections$, [])
  112. const currentUser = useReadonlyStream(currentUser$, null)
  113. // Template refs
  114. const options = ref<any>()
  115. const inputChooseFileToImportFrom = ref<HTMLInputElement>()
  116. const collectionJson = computed(() => {
  117. return JSON.stringify(collections.value, null, 2)
  118. })
  119. const createCollectionGist = async () => {
  120. if (!currentUser.value) {
  121. toast.error(t("profile.no_permission").toString())
  122. return
  123. }
  124. try {
  125. const res = await axios.$post(
  126. "https://api.github.com/gists",
  127. {
  128. files: {
  129. "hoppscotch-collections.json": {
  130. content: collectionJson.value,
  131. },
  132. },
  133. },
  134. {
  135. headers: {
  136. Authorization: `token ${currentUser.value.accessToken}`,
  137. Accept: "application/vnd.github.v3+json",
  138. },
  139. }
  140. )
  141. toast.success(t("export.gist_created").toString())
  142. window.open(res.html_url)
  143. } catch (e) {
  144. toast.error(t("error.something_went_wrong").toString())
  145. console.error(e)
  146. }
  147. }
  148. const fileImported = () => {
  149. toast.success(t("state.file_imported").toString())
  150. }
  151. const failedImport = () => {
  152. toast.error(t("import.failed").toString())
  153. }
  154. const readCollectionGist = async () => {
  155. const gist = prompt(t("import.gist_url").toString())
  156. if (!gist) return
  157. try {
  158. const { files } = (await axios.$get(
  159. `https://api.github.com/gists/${gist.split("/").pop()}`,
  160. {
  161. headers: {
  162. Accept: "application/vnd.github.v3+json",
  163. },
  164. }
  165. )) as {
  166. files: {
  167. [fileName: string]: {
  168. content: any
  169. }
  170. }
  171. }
  172. const collections = JSON.parse(Object.values(files)[0].content)
  173. setGraphqlCollections(collections)
  174. fileImported()
  175. } catch (e) {
  176. failedImport()
  177. console.error(e)
  178. }
  179. }
  180. const hideModal = () => {
  181. emit("hide-modal")
  182. }
  183. const openDialogChooseFileToImportFrom = () => {
  184. if (inputChooseFileToImportFrom.value)
  185. inputChooseFileToImportFrom.value.click()
  186. }
  187. const importFromJSON = () => {
  188. if (!inputChooseFileToImportFrom.value) return
  189. if (
  190. !inputChooseFileToImportFrom.value.files ||
  191. inputChooseFileToImportFrom.value.files.length === 0
  192. ) {
  193. toast.show(t("action.choose_file").toString())
  194. return
  195. }
  196. const reader = new FileReader()
  197. reader.onload = ({ target }) => {
  198. const content = target!.result as string | null
  199. if (!content) {
  200. toast.show(t("action.choose_file").toString())
  201. return
  202. }
  203. const collections = JSON.parse(content)
  204. if (collections[0]) {
  205. const [name, folders, requests] = Object.keys(collections[0])
  206. if (name === "name" && folders === "folders" && requests === "requests") {
  207. // Do nothing
  208. }
  209. } else {
  210. failedImport()
  211. return
  212. }
  213. appendGraphqlCollections(collections)
  214. fileImported()
  215. }
  216. reader.readAsText(inputChooseFileToImportFrom.value.files[0])
  217. inputChooseFileToImportFrom.value.value = ""
  218. }
  219. const exportJSON = () => {
  220. const dataToWrite = collectionJson.value
  221. const file = new Blob([dataToWrite], { type: "application/json" })
  222. const a = document.createElement("a")
  223. const url = URL.createObjectURL(file)
  224. a.href = url
  225. // TODO: get uri from meta
  226. a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
  227. document.body.appendChild(a)
  228. a.click()
  229. toast.success(t("state.download_started").toString())
  230. setTimeout(() => {
  231. document.body.removeChild(a)
  232. URL.revokeObjectURL(url)
  233. }, 1000)
  234. }
  235. </script>