ImportExport.vue 12 KB

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