ImportExport.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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. <ButtonSecondary
  10. v-if="mode == 'import_from_my_collections'"
  11. v-tippy="{ theme: 'tooltip' }"
  12. :title="$t('action.go_back')"
  13. svg="arrow-left"
  14. @click.native="
  15. () => {
  16. mode = 'import_export'
  17. mySelectedCollectionID = undefined
  18. }
  19. "
  20. />
  21. <span>
  22. <tippy
  23. v-if="
  24. mode == 'import_export' && collectionsType.type == 'my-collections'
  25. "
  26. ref="options"
  27. interactive
  28. trigger="click"
  29. theme="popover"
  30. arrow
  31. >
  32. <template #trigger>
  33. <ButtonSecondary
  34. v-tippy="{ theme: 'tooltip' }"
  35. :title="$t('action.more')"
  36. svg="more-vertical"
  37. />
  38. </template>
  39. <SmartItem
  40. icon="assignment_returned"
  41. :label="$t('import.from_gist')"
  42. @click.native="
  43. () => {
  44. readCollectionGist
  45. $refs.options.tippy().hide()
  46. }
  47. "
  48. />
  49. <span
  50. v-tippy="{ theme: 'tooltip' }"
  51. :title="
  52. !currentUser
  53. ? $t('export.require_github')
  54. : currentUser.provider !== 'github.com'
  55. ? $t('export.require_github')
  56. : null
  57. "
  58. >
  59. <SmartItem
  60. :disabled="
  61. !currentUser
  62. ? true
  63. : currentUser.provider !== 'github.com'
  64. ? true
  65. : false
  66. "
  67. icon="assignment_turned_in"
  68. :label="$t('export.create_secret_gist')"
  69. @click.native="
  70. () => {
  71. createCollectionGist()
  72. $refs.options.tippy().hide()
  73. }
  74. "
  75. />
  76. </span>
  77. </tippy>
  78. </span>
  79. </template>
  80. <template #body>
  81. <div v-if="mode == 'import_export'" class="flex flex-col space-y-2">
  82. <SmartItem
  83. v-tippy="{ theme: 'tooltip' }"
  84. :title="$t('action.replace_current')"
  85. svg="file"
  86. :label="$t('action.replace_json')"
  87. @click.native="openDialogChooseFileToReplaceWith"
  88. />
  89. <input
  90. ref="inputChooseFileToReplaceWith"
  91. class="input"
  92. type="file"
  93. style="display: none"
  94. accept="application/json"
  95. @change="replaceWithJSON"
  96. />
  97. <SmartItem
  98. v-tippy="{ theme: 'tooltip' }"
  99. :title="$t('action.preserve_current')"
  100. svg="folder-plus"
  101. :label="$t('import.json')"
  102. @click.native="openDialogChooseFileToImportFrom"
  103. />
  104. <input
  105. ref="inputChooseFileToImportFrom"
  106. class="input"
  107. type="file"
  108. style="display: none"
  109. accept="application/json"
  110. @change="importFromJSON"
  111. />
  112. <SmartItem
  113. v-if="collectionsType.type == 'team-collections'"
  114. v-tippy="{ theme: 'tooltip' }"
  115. :title="$t('action.preserve_current')"
  116. svg="user"
  117. :label="$t('import.from_my_collections')"
  118. @click.native="mode = 'import_from_my_collections'"
  119. />
  120. <SmartItem
  121. v-tippy="{ theme: 'tooltip' }"
  122. :title="$t('action.download_file')"
  123. svg="download"
  124. :label="$t('export.as_json')"
  125. @click.native="exportJSON"
  126. />
  127. </div>
  128. <div
  129. v-if="mode == 'import_from_my_collections'"
  130. class="flex flex-col px-2"
  131. >
  132. <div class="select-wrapper">
  133. <select
  134. type="text"
  135. autocomplete="off"
  136. class="select"
  137. autofocus
  138. @change="
  139. ($event) => {
  140. mySelectedCollectionID = $event.target.value
  141. }
  142. "
  143. >
  144. <option
  145. :key="undefined"
  146. :value="undefined"
  147. hidden
  148. disabled
  149. selected
  150. >
  151. Select Collection
  152. </option>
  153. <option
  154. v-for="(collection, index) in myCollections"
  155. :key="`collection-${index}`"
  156. :value="index"
  157. >
  158. {{ collection.name }}
  159. </option>
  160. </select>
  161. </div>
  162. </div>
  163. </template>
  164. <template #footer>
  165. <div v-if="mode == 'import_from_my_collections'">
  166. <span>
  167. <ButtonPrimary
  168. :disabled="mySelectedCollectionID == undefined"
  169. svg="folder-plus"
  170. :label="$t('import.title')"
  171. @click.native="importFromMyCollections"
  172. />
  173. </span>
  174. </div>
  175. </template>
  176. </SmartModal>
  177. </template>
  178. <script>
  179. import { defineComponent } from "@nuxtjs/composition-api"
  180. import { currentUser$ } from "~/helpers/fb/auth"
  181. import * as teamUtils from "~/helpers/teams/utils"
  182. import { useReadonlyStream } from "~/helpers/utils/composables"
  183. import {
  184. restCollections$,
  185. setRESTCollections,
  186. appendRESTCollections,
  187. } from "~/newstore/collections"
  188. export default defineComponent({
  189. props: {
  190. show: Boolean,
  191. collectionsType: { type: Object, default: () => {} },
  192. },
  193. setup() {
  194. return {
  195. myCollections: useReadonlyStream(restCollections$, []),
  196. currentUser: useReadonlyStream(currentUser$, null),
  197. }
  198. },
  199. data() {
  200. return {
  201. showJsonCode: false,
  202. mode: "import_export",
  203. mySelectedCollectionID: undefined,
  204. collectionJson: "",
  205. }
  206. },
  207. methods: {
  208. async createCollectionGist() {
  209. this.getJSONCollection()
  210. await this.$axios
  211. .$post(
  212. "https://api.github.com/gists",
  213. {
  214. files: {
  215. "hoppscotch-collections.json": {
  216. content: this.collectionJson,
  217. },
  218. },
  219. },
  220. {
  221. headers: {
  222. Authorization: `token ${this.currentUser.accessToken}`,
  223. Accept: "application/vnd.github.v3+json",
  224. },
  225. }
  226. )
  227. .then((res) => {
  228. this.$toast.success(this.$t("export.gist_created"))
  229. window.open(res.html_url)
  230. })
  231. .catch((e) => {
  232. this.$toast.error(this.$t("error.something_went_wrong"))
  233. console.error(e)
  234. })
  235. },
  236. async readCollectionGist() {
  237. const gist = prompt(this.$t("import.gist_url"))
  238. if (!gist) return
  239. await this.$axios
  240. .$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
  241. headers: {
  242. Accept: "application/vnd.github.v3+json",
  243. },
  244. })
  245. .then(({ files }) => {
  246. const collections = JSON.parse(Object.values(files)[0].content)
  247. setRESTCollections(collections)
  248. this.fileImported()
  249. })
  250. .catch((e) => {
  251. this.failedImport()
  252. console.error(e)
  253. })
  254. },
  255. hideModal() {
  256. this.mode = "import_export"
  257. this.mySelectedCollectionID = undefined
  258. this.$emit("hide-modal")
  259. },
  260. openDialogChooseFileToReplaceWith() {
  261. this.$refs.inputChooseFileToReplaceWith.click()
  262. },
  263. openDialogChooseFileToImportFrom() {
  264. this.$refs.inputChooseFileToImportFrom.click()
  265. },
  266. replaceWithJSON() {
  267. const reader = new FileReader()
  268. reader.onload = ({ target }) => {
  269. const content = target.result
  270. let collections = JSON.parse(content)
  271. if (collections[0]) {
  272. const [name, folders, requests] = Object.keys(collections[0])
  273. if (
  274. name === "name" &&
  275. folders === "folders" &&
  276. requests === "requests"
  277. ) {
  278. // Do nothing
  279. }
  280. } else if (
  281. collections.info &&
  282. collections.info.schema.includes("v2.1.0")
  283. ) {
  284. collections = [this.parsePostmanCollection(collections)]
  285. } else {
  286. this.failedImport()
  287. }
  288. if (this.collectionsType.type === "team-collections") {
  289. teamUtils
  290. .replaceWithJSON(
  291. this.$apollo,
  292. collections,
  293. this.collectionsType.selectedTeam.id
  294. )
  295. .then((status) => {
  296. if (status) {
  297. this.fileImported()
  298. } else {
  299. this.failedImport()
  300. }
  301. })
  302. .catch((e) => {
  303. console.error(e)
  304. this.failedImport()
  305. })
  306. } else {
  307. setRESTCollections(collections)
  308. this.fileImported()
  309. }
  310. }
  311. reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
  312. this.$refs.inputChooseFileToReplaceWith.value = ""
  313. },
  314. importFromJSON() {
  315. const reader = new FileReader()
  316. reader.onload = ({ target }) => {
  317. const content = target.result
  318. let collections = JSON.parse(content)
  319. if (collections[0]) {
  320. const [name, folders, requests] = Object.keys(collections[0])
  321. if (
  322. name === "name" &&
  323. folders === "folders" &&
  324. requests === "requests"
  325. ) {
  326. // Do nothing
  327. }
  328. } else if (
  329. collections.info &&
  330. collections.info.schema.includes("v2.1.0")
  331. ) {
  332. // replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
  333. collections = JSON.parse(
  334. content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
  335. )
  336. collections = [this.parsePostmanCollection(collections)]
  337. } else {
  338. this.failedImport()
  339. return
  340. }
  341. if (this.collectionsType.type === "team-collections") {
  342. teamUtils
  343. .importFromJSON(
  344. this.$apollo,
  345. collections,
  346. this.collectionsType.selectedTeam.id
  347. )
  348. .then((status) => {
  349. if (status) {
  350. this.$emit("update-team-collections")
  351. this.fileImported()
  352. } else {
  353. this.failedImport()
  354. }
  355. })
  356. .catch((e) => {
  357. console.error(e)
  358. this.failedImport()
  359. })
  360. } else {
  361. appendRESTCollections(collections)
  362. this.fileImported()
  363. }
  364. }
  365. reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
  366. this.$refs.inputChooseFileToImportFrom.value = ""
  367. },
  368. importFromMyCollections() {
  369. teamUtils
  370. .importFromMyCollections(
  371. this.$apollo,
  372. this.mySelectedCollectionID,
  373. this.collectionsType.selectedTeam.id
  374. )
  375. .then((success) => {
  376. if (success) {
  377. this.fileImported()
  378. this.$emit("update-team-collections")
  379. } else {
  380. this.failedImport()
  381. }
  382. })
  383. .catch((e) => {
  384. console.error(e)
  385. this.failedImport()
  386. })
  387. },
  388. async getJSONCollection() {
  389. if (this.collectionsType.type === "my-collections") {
  390. this.collectionJson = JSON.stringify(this.myCollections, null, 2)
  391. } else {
  392. this.collectionJson = await teamUtils.exportAsJSON(
  393. this.$apollo,
  394. this.collectionsType.selectedTeam.id
  395. )
  396. }
  397. return this.collectionJson
  398. },
  399. exportJSON() {
  400. this.getJSONCollection()
  401. const dataToWrite = this.collectionJson
  402. const file = new Blob([dataToWrite], { type: "application/json" })
  403. const a = document.createElement("a")
  404. const url = URL.createObjectURL(file)
  405. a.href = url
  406. // TODO get uri from meta
  407. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
  408. document.body.appendChild(a)
  409. a.click()
  410. this.$toast.success(this.$t("state.download_started"))
  411. setTimeout(() => {
  412. document.body.removeChild(a)
  413. URL.revokeObjectURL(url)
  414. }, 1000)
  415. },
  416. fileImported() {
  417. this.$toast.success(this.$t("state.file_imported"))
  418. },
  419. failedImport() {
  420. this.$toast.error(this.$t("import.failed"))
  421. },
  422. parsePostmanCollection({ info, name, item }) {
  423. const hoppscotchCollection = {
  424. name: "",
  425. folders: [],
  426. requests: [],
  427. }
  428. hoppscotchCollection.name = info ? info.name : name
  429. if (item && item.length > 0) {
  430. for (const collectionItem of item) {
  431. if (collectionItem.request) {
  432. if (
  433. Object.prototype.hasOwnProperty.call(
  434. hoppscotchCollection,
  435. "folders"
  436. )
  437. ) {
  438. hoppscotchCollection.name = info ? info.name : name
  439. hoppscotchCollection.requests.push(
  440. this.parsePostmanRequest(collectionItem)
  441. )
  442. } else {
  443. hoppscotchCollection.name = name || ""
  444. hoppscotchCollection.requests.push(
  445. this.parsePostmanRequest(collectionItem)
  446. )
  447. }
  448. } else if (this.hasFolder(collectionItem)) {
  449. hoppscotchCollection.folders.push(
  450. this.parsePostmanCollection(collectionItem)
  451. )
  452. } else {
  453. hoppscotchCollection.requests.push(
  454. this.parsePostmanRequest(collectionItem)
  455. )
  456. }
  457. }
  458. }
  459. return hoppscotchCollection
  460. },
  461. parsePostmanRequest({ name, request }) {
  462. const pwRequest = {
  463. url: "",
  464. path: "",
  465. method: "",
  466. auth: "",
  467. httpUser: "",
  468. httpPassword: "",
  469. passwordFieldType: "password",
  470. bearerToken: "",
  471. headers: [],
  472. params: [],
  473. bodyParams: [],
  474. rawParams: "",
  475. rawInput: false,
  476. contentType: "",
  477. requestType: "",
  478. name: "",
  479. }
  480. pwRequest.name = name
  481. if (request.url) {
  482. const requestObjectUrl = request.url.raw.match(
  483. /^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
  484. )
  485. if (requestObjectUrl) {
  486. pwRequest.url = requestObjectUrl[1]
  487. pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
  488. }
  489. }
  490. pwRequest.method = request.method
  491. const itemAuth = request.auth ? request.auth : ""
  492. const authType = itemAuth ? itemAuth.type : ""
  493. if (authType === "basic") {
  494. pwRequest.auth = "Basic Auth"
  495. pwRequest.httpUser =
  496. itemAuth.basic[0].key === "username"
  497. ? itemAuth.basic[0].value
  498. : itemAuth.basic[1].value
  499. pwRequest.httpPassword =
  500. itemAuth.basic[0].key === "password"
  501. ? itemAuth.basic[0].value
  502. : itemAuth.basic[1].value
  503. } else if (authType === "oauth2") {
  504. pwRequest.auth = "OAuth 2.0"
  505. pwRequest.bearerToken =
  506. itemAuth.oauth2[0].key === "accessToken"
  507. ? itemAuth.oauth2[0].value
  508. : itemAuth.oauth2[1].value
  509. } else if (authType === "bearer") {
  510. pwRequest.auth = "Bearer Token"
  511. pwRequest.bearerToken = itemAuth.bearer[0].value
  512. }
  513. const requestObjectHeaders = request.header
  514. if (requestObjectHeaders) {
  515. pwRequest.headers = requestObjectHeaders
  516. for (const header of pwRequest.headers) {
  517. delete header.name
  518. delete header.type
  519. }
  520. }
  521. if (request.url) {
  522. const requestObjectParams = request.url.query
  523. if (requestObjectParams) {
  524. pwRequest.params = requestObjectParams
  525. for (const param of pwRequest.params) {
  526. delete param.disabled
  527. }
  528. }
  529. }
  530. if (request.body) {
  531. if (request.body.mode === "urlencoded") {
  532. const params = request.body.urlencoded
  533. pwRequest.bodyParams = params || []
  534. for (const param of pwRequest.bodyParams) {
  535. delete param.type
  536. }
  537. } else if (request.body.mode === "raw") {
  538. pwRequest.rawInput = true
  539. pwRequest.rawParams = request.body.raw
  540. }
  541. }
  542. return pwRequest
  543. },
  544. hasFolder(item) {
  545. return Object.prototype.hasOwnProperty.call(item, "item")
  546. },
  547. },
  548. })
  549. </script>