ImportExport.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <template>
  2. <SmartModal
  3. v-if="show"
  4. :title="`${$t('modal.import_export')} ${$t('modal.collections')}`"
  5. max-width="sm:max-w-md"
  6. @close="hideModal"
  7. >
  8. <template #actions>
  9. <span>
  10. <tippy ref="options" interactive trigger="click" theme="popover" arrow>
  11. <template #trigger>
  12. <ButtonSecondary
  13. v-tippy="{ theme: 'tooltip' }"
  14. :title="$t('action.more')"
  15. class="rounded"
  16. svg="more-vertical"
  17. />
  18. </template>
  19. <SmartItem
  20. icon="assignment_returned"
  21. :label="$t('import.from_gist')"
  22. @click.native="
  23. readCollectionGist
  24. $refs.options.tippy().hide()
  25. "
  26. />
  27. <span
  28. v-tippy="{ theme: 'tooltip' }"
  29. :title="
  30. !currentUser
  31. ? $t('export.require_github')
  32. : currentUser.provider !== 'github.com'
  33. ? $t('export.require_github')
  34. : null
  35. "
  36. >
  37. <SmartItem
  38. :disabled="
  39. !currentUser
  40. ? true
  41. : currentUser.provider !== 'github.com'
  42. ? true
  43. : false
  44. "
  45. icon="assignment_turned_in"
  46. :label="$t('export.create_secret_gist')"
  47. @click.native="
  48. createCollectionGist()
  49. $refs.options.tippy().hide()
  50. "
  51. />
  52. </span>
  53. </tippy>
  54. </span>
  55. </template>
  56. <template #body>
  57. <div class="flex flex-col space-y-2">
  58. <SmartItem
  59. v-tippy="{ theme: 'tooltip' }"
  60. :title="$t('action.replace_current')"
  61. svg="file"
  62. :label="$t('action.replace_json')"
  63. @click.native="openDialogChooseFileToReplaceWith"
  64. />
  65. <input
  66. ref="inputChooseFileToReplaceWith"
  67. class="input"
  68. type="file"
  69. accept="application/json"
  70. @change="replaceWithJSON"
  71. />
  72. <SmartItem
  73. v-tippy="{ theme: 'tooltip' }"
  74. :title="$t('action.preserve_current')"
  75. svg="folder-plus"
  76. :label="$t('import.json')"
  77. @click.native="openDialogChooseFileToImportFrom"
  78. />
  79. <input
  80. ref="inputChooseFileToImportFrom"
  81. class="input"
  82. type="file"
  83. accept="application/json"
  84. @change="importFromJSON"
  85. />
  86. <SmartItem
  87. v-tippy="{ theme: 'tooltip' }"
  88. :title="$t('action.download_file')"
  89. svg="download"
  90. :label="$t('export.as_json')"
  91. @click.native="exportJSON"
  92. />
  93. </div>
  94. </template>
  95. </SmartModal>
  96. </template>
  97. <script>
  98. import { defineComponent } from "@nuxtjs/composition-api"
  99. import { currentUser$ } from "~/helpers/fb/auth"
  100. import { useReadonlyStream } from "~/helpers/utils/composables"
  101. import {
  102. graphqlCollections$,
  103. setGraphqlCollections,
  104. appendGraphqlCollections,
  105. } from "~/newstore/collections"
  106. export default defineComponent({
  107. props: {
  108. show: Boolean,
  109. },
  110. setup() {
  111. return {
  112. collections: useReadonlyStream(graphqlCollections$, []),
  113. currentUser: useReadonlyStream(currentUser$, null),
  114. }
  115. },
  116. computed: {
  117. collectionJson() {
  118. return JSON.stringify(this.collections, null, 2)
  119. },
  120. },
  121. methods: {
  122. async createCollectionGist() {
  123. await this.$axios
  124. .$post(
  125. "https://api.github.com/gists",
  126. {
  127. files: {
  128. "hoppscotch-collections.json": {
  129. content: this.collectionJson,
  130. },
  131. },
  132. },
  133. {
  134. headers: {
  135. Authorization: `token ${this.currentUser.accessToken}`,
  136. Accept: "application/vnd.github.v3+json",
  137. },
  138. }
  139. )
  140. .then((res) => {
  141. this.$toast.success(this.$t("export.gist_created"))
  142. window.open(res.html_url)
  143. })
  144. .catch((e) => {
  145. this.$toast.error(this.$t("error.something_went_wrong"))
  146. console.error(e)
  147. })
  148. },
  149. async readCollectionGist() {
  150. const gist = prompt(this.$t("import.gist_url"))
  151. if (!gist) return
  152. await this.$axios
  153. .$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
  154. headers: {
  155. Accept: "application/vnd.github.v3+json",
  156. },
  157. })
  158. .then(({ files }) => {
  159. const collections = JSON.parse(Object.values(files)[0].content)
  160. setGraphqlCollections(collections)
  161. this.fileImported()
  162. })
  163. .catch((e) => {
  164. this.failedImport()
  165. console.error(e)
  166. })
  167. },
  168. hideModal() {
  169. this.$emit("hide-modal")
  170. },
  171. openDialogChooseFileToReplaceWith() {
  172. this.$refs.inputChooseFileToReplaceWith.click()
  173. },
  174. openDialogChooseFileToImportFrom() {
  175. this.$refs.inputChooseFileToImportFrom.click()
  176. },
  177. replaceWithJSON() {
  178. const reader = new FileReader()
  179. reader.onload = ({ target }) => {
  180. const content = target.result
  181. let collections = JSON.parse(content)
  182. if (collections[0]) {
  183. const [name, folders, requests] = Object.keys(collections[0])
  184. if (
  185. name === "name" &&
  186. folders === "folders" &&
  187. requests === "requests"
  188. ) {
  189. // Do nothing
  190. }
  191. } else if (
  192. collections.info &&
  193. collections.info.schema.includes("v2.1.0")
  194. ) {
  195. collections = [this.parsePostmanCollection(collections)]
  196. } else {
  197. this.failedImport()
  198. return
  199. }
  200. setGraphqlCollections(collections)
  201. this.fileImported()
  202. }
  203. reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
  204. this.$refs.inputChooseFileToReplaceWith.value = ""
  205. },
  206. importFromJSON() {
  207. const reader = new FileReader()
  208. reader.onload = ({ target }) => {
  209. const content = target.result
  210. let collections = JSON.parse(content)
  211. if (collections[0]) {
  212. const [name, folders, requests] = Object.keys(collections[0])
  213. if (
  214. name === "name" &&
  215. folders === "folders" &&
  216. requests === "requests"
  217. ) {
  218. // Do nothing
  219. }
  220. } else if (
  221. collections.info &&
  222. collections.info.schema.includes("v2.1.0")
  223. ) {
  224. // replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
  225. collections = JSON.parse(
  226. content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
  227. )
  228. collections = [this.parsePostmanCollection(collections)]
  229. } else {
  230. this.failedImport()
  231. return
  232. }
  233. appendGraphqlCollections(collections)
  234. this.fileImported()
  235. }
  236. reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
  237. this.$refs.inputChooseFileToImportFrom.value = ""
  238. },
  239. exportJSON() {
  240. const dataToWrite = this.collectionJson
  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. this.$toast.success(this.$t("state.download_started"))
  250. setTimeout(() => {
  251. document.body.removeChild(a)
  252. URL.revokeObjectURL(url)
  253. }, 1000)
  254. },
  255. fileImported() {
  256. this.$toast.success(this.$t("state.file_imported"))
  257. },
  258. failedImport() {
  259. this.$toast.error(this.$t("import.failed"))
  260. },
  261. parsePostmanCollection({ info, name, item }) {
  262. const hoppscotchCollection = {
  263. name: "",
  264. folders: [],
  265. requests: [],
  266. }
  267. hoppscotchCollection.name = info ? info.name : name
  268. if (item && item.length > 0) {
  269. for (const collectionItem of item) {
  270. if (collectionItem.request) {
  271. if (
  272. Object.prototype.hasOwnProperty.call(
  273. hoppscotchCollection,
  274. "folders"
  275. )
  276. ) {
  277. hoppscotchCollection.name = info ? info.name : name
  278. hoppscotchCollection.requests.push(
  279. this.parsePostmanRequest(collectionItem)
  280. )
  281. } else {
  282. hoppscotchCollection.name = name || ""
  283. hoppscotchCollection.requests.push(
  284. this.parsePostmanRequest(collectionItem)
  285. )
  286. }
  287. } else if (this.hasFolder(collectionItem)) {
  288. hoppscotchCollection.folders.push(
  289. this.parsePostmanCollection(collectionItem)
  290. )
  291. } else {
  292. hoppscotchCollection.requests.push(
  293. this.parsePostmanRequest(collectionItem)
  294. )
  295. }
  296. }
  297. }
  298. return hoppscotchCollection
  299. },
  300. parsePostmanRequest({ name, request }) {
  301. const pwRequest = {
  302. url: "",
  303. path: "",
  304. method: "",
  305. auth: "",
  306. httpUser: "",
  307. httpPassword: "",
  308. passwordFieldType: "password",
  309. bearerToken: "",
  310. headers: [],
  311. params: [],
  312. bodyParams: [],
  313. rawParams: "",
  314. rawInput: false,
  315. contentType: "",
  316. requestType: "",
  317. name: "",
  318. }
  319. pwRequest.name = name
  320. const requestObjectUrl = request.url.raw.match(
  321. /^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
  322. )
  323. if (requestObjectUrl) {
  324. pwRequest.url = requestObjectUrl[1]
  325. pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
  326. }
  327. pwRequest.method = request.method
  328. const itemAuth = request.auth ? request.auth : ""
  329. const authType = itemAuth ? itemAuth.type : ""
  330. if (authType === "basic") {
  331. pwRequest.auth = "Basic Auth"
  332. pwRequest.httpUser =
  333. itemAuth.basic[0].key === "username"
  334. ? itemAuth.basic[0].value
  335. : itemAuth.basic[1].value
  336. pwRequest.httpPassword =
  337. itemAuth.basic[0].key === "password"
  338. ? itemAuth.basic[0].value
  339. : itemAuth.basic[1].value
  340. } else if (authType === "oauth2") {
  341. pwRequest.auth = "OAuth 2.0"
  342. pwRequest.bearerToken =
  343. itemAuth.oauth2[0].key === "accessToken"
  344. ? itemAuth.oauth2[0].value
  345. : itemAuth.oauth2[1].value
  346. } else if (authType === "bearer") {
  347. pwRequest.auth = "Bearer Token"
  348. pwRequest.bearerToken = itemAuth.bearer[0].value
  349. }
  350. const requestObjectHeaders = request.header
  351. if (requestObjectHeaders) {
  352. pwRequest.headers = requestObjectHeaders
  353. for (const header of pwRequest.headers) {
  354. delete header.name
  355. delete header.type
  356. }
  357. }
  358. const requestObjectParams = request.url.query
  359. if (requestObjectParams) {
  360. pwRequest.params = requestObjectParams
  361. for (const param of pwRequest.params) {
  362. delete param.disabled
  363. }
  364. }
  365. if (request.body) {
  366. if (request.body.mode === "urlencoded") {
  367. const params = request.body.urlencoded
  368. pwRequest.bodyParams = params || []
  369. for (const param of pwRequest.bodyParams) {
  370. delete param.type
  371. }
  372. } else if (request.body.mode === "raw") {
  373. pwRequest.rawInput = true
  374. pwRequest.rawParams = request.body.raw
  375. }
  376. }
  377. return pwRequest
  378. },
  379. hasFolder(item) {
  380. return Object.prototype.hasOwnProperty.call(item, "item")
  381. },
  382. },
  383. })
  384. </script>