ImportExport.vue 16 KB

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