ImportExport.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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. icon: "done",
  143. })
  144. window.open(res.html_url)
  145. })
  146. .catch((e) => {
  147. this.$toast.error(this.$t("error.something_went_wrong"), {
  148. icon: "error_outline",
  149. })
  150. console.error(e)
  151. })
  152. },
  153. async readCollectionGist() {
  154. const gist = prompt(this.$t("import.gist_url"))
  155. if (!gist) return
  156. await this.$axios
  157. .$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
  158. headers: {
  159. Accept: "application/vnd.github.v3+json",
  160. },
  161. })
  162. .then(({ files }) => {
  163. const collections = JSON.parse(Object.values(files)[0].content)
  164. setGraphqlCollections(collections)
  165. this.fileImported()
  166. })
  167. .catch((e) => {
  168. this.failedImport()
  169. console.error(e)
  170. })
  171. },
  172. hideModal() {
  173. this.$emit("hide-modal")
  174. },
  175. openDialogChooseFileToReplaceWith() {
  176. this.$refs.inputChooseFileToReplaceWith.click()
  177. },
  178. openDialogChooseFileToImportFrom() {
  179. this.$refs.inputChooseFileToImportFrom.click()
  180. },
  181. replaceWithJSON() {
  182. const reader = new FileReader()
  183. reader.onload = ({ target }) => {
  184. const content = target.result
  185. let collections = JSON.parse(content)
  186. if (collections[0]) {
  187. const [name, folders, requests] = Object.keys(collections[0])
  188. if (
  189. name === "name" &&
  190. folders === "folders" &&
  191. requests === "requests"
  192. ) {
  193. // Do nothing
  194. }
  195. } else if (
  196. collections.info &&
  197. collections.info.schema.includes("v2.1.0")
  198. ) {
  199. collections = [this.parsePostmanCollection(collections)]
  200. } else {
  201. this.failedImport()
  202. return
  203. }
  204. setGraphqlCollections(collections)
  205. this.fileImported()
  206. }
  207. reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
  208. this.$refs.inputChooseFileToReplaceWith.value = ""
  209. },
  210. importFromJSON() {
  211. const reader = new FileReader()
  212. reader.onload = ({ target }) => {
  213. const content = target.result
  214. let collections = JSON.parse(content)
  215. if (collections[0]) {
  216. const [name, folders, requests] = Object.keys(collections[0])
  217. if (
  218. name === "name" &&
  219. folders === "folders" &&
  220. requests === "requests"
  221. ) {
  222. // Do nothing
  223. }
  224. } else if (
  225. collections.info &&
  226. collections.info.schema.includes("v2.1.0")
  227. ) {
  228. // replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
  229. collections = JSON.parse(
  230. content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
  231. )
  232. collections = [this.parsePostmanCollection(collections)]
  233. } else {
  234. this.failedImport()
  235. return
  236. }
  237. appendGraphqlCollections(collections)
  238. this.fileImported()
  239. }
  240. reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
  241. this.$refs.inputChooseFileToImportFrom.value = ""
  242. },
  243. exportJSON() {
  244. const dataToWrite = this.collectionJson
  245. const file = new Blob([dataToWrite], { type: "application/json" })
  246. const a = document.createElement("a")
  247. const url = URL.createObjectURL(file)
  248. a.href = url
  249. // TODO get uri from meta
  250. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
  251. document.body.appendChild(a)
  252. a.click()
  253. this.$toast.success(this.$t("state.download_started"), {
  254. icon: "downloading",
  255. })
  256. setTimeout(() => {
  257. document.body.removeChild(a)
  258. URL.revokeObjectURL(url)
  259. }, 1000)
  260. },
  261. fileImported() {
  262. this.$toast.success(this.$t("state.file_imported"), {
  263. icon: "folder_shared",
  264. })
  265. },
  266. failedImport() {
  267. this.$toast.error(this.$t("import.failed"), {
  268. icon: "error_outline",
  269. })
  270. },
  271. parsePostmanCollection({ info, name, item }) {
  272. const hoppscotchCollection = {
  273. name: "",
  274. folders: [],
  275. requests: [],
  276. }
  277. hoppscotchCollection.name = info ? info.name : name
  278. if (item && item.length > 0) {
  279. for (const collectionItem of item) {
  280. if (collectionItem.request) {
  281. if (
  282. Object.prototype.hasOwnProperty.call(
  283. hoppscotchCollection,
  284. "folders"
  285. )
  286. ) {
  287. hoppscotchCollection.name = info ? info.name : name
  288. hoppscotchCollection.requests.push(
  289. this.parsePostmanRequest(collectionItem)
  290. )
  291. } else {
  292. hoppscotchCollection.name = name || ""
  293. hoppscotchCollection.requests.push(
  294. this.parsePostmanRequest(collectionItem)
  295. )
  296. }
  297. } else if (this.hasFolder(collectionItem)) {
  298. hoppscotchCollection.folders.push(
  299. this.parsePostmanCollection(collectionItem)
  300. )
  301. } else {
  302. hoppscotchCollection.requests.push(
  303. this.parsePostmanRequest(collectionItem)
  304. )
  305. }
  306. }
  307. }
  308. return hoppscotchCollection
  309. },
  310. parsePostmanRequest({ name, request }) {
  311. const pwRequest = {
  312. url: "",
  313. path: "",
  314. method: "",
  315. auth: "",
  316. httpUser: "",
  317. httpPassword: "",
  318. passwordFieldType: "password",
  319. bearerToken: "",
  320. headers: [],
  321. params: [],
  322. bodyParams: [],
  323. rawParams: "",
  324. rawInput: false,
  325. contentType: "",
  326. requestType: "",
  327. name: "",
  328. }
  329. pwRequest.name = name
  330. const requestObjectUrl = request.url.raw.match(
  331. /^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
  332. )
  333. if (requestObjectUrl) {
  334. pwRequest.url = requestObjectUrl[1]
  335. pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
  336. }
  337. pwRequest.method = request.method
  338. const itemAuth = request.auth ? request.auth : ""
  339. const authType = itemAuth ? itemAuth.type : ""
  340. if (authType === "basic") {
  341. pwRequest.auth = "Basic Auth"
  342. pwRequest.httpUser =
  343. itemAuth.basic[0].key === "username"
  344. ? itemAuth.basic[0].value
  345. : itemAuth.basic[1].value
  346. pwRequest.httpPassword =
  347. itemAuth.basic[0].key === "password"
  348. ? itemAuth.basic[0].value
  349. : itemAuth.basic[1].value
  350. } else if (authType === "oauth2") {
  351. pwRequest.auth = "OAuth 2.0"
  352. pwRequest.bearerToken =
  353. itemAuth.oauth2[0].key === "accessToken"
  354. ? itemAuth.oauth2[0].value
  355. : itemAuth.oauth2[1].value
  356. } else if (authType === "bearer") {
  357. pwRequest.auth = "Bearer Token"
  358. pwRequest.bearerToken = itemAuth.bearer[0].value
  359. }
  360. const requestObjectHeaders = request.header
  361. if (requestObjectHeaders) {
  362. pwRequest.headers = requestObjectHeaders
  363. for (const header of pwRequest.headers) {
  364. delete header.name
  365. delete header.type
  366. }
  367. }
  368. const requestObjectParams = request.url.query
  369. if (requestObjectParams) {
  370. pwRequest.params = requestObjectParams
  371. for (const param of pwRequest.params) {
  372. delete param.disabled
  373. }
  374. }
  375. if (request.body) {
  376. if (request.body.mode === "urlencoded") {
  377. const params = request.body.urlencoded
  378. pwRequest.bodyParams = params || []
  379. for (const param of pwRequest.bodyParams) {
  380. delete param.type
  381. }
  382. } else if (request.body.mode === "raw") {
  383. pwRequest.rawInput = true
  384. pwRequest.rawParams = request.body.raw
  385. }
  386. }
  387. return pwRequest
  388. },
  389. hasFolder(item) {
  390. return Object.prototype.hasOwnProperty.call(item, "item")
  391. },
  392. },
  393. })
  394. </script>