ImportExport.vue 16 KB

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