ImportExport.vue 12 KB

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