ImportExport.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. icon: "done",
  232. })
  233. window.open(res.html_url)
  234. })
  235. .catch((e) => {
  236. this.$toast.error(this.$t("error.something_went_wrong"), {
  237. icon: "error_outline",
  238. })
  239. console.error(e)
  240. })
  241. },
  242. async readCollectionGist() {
  243. const gist = prompt(this.$t("import.gist_url"))
  244. if (!gist) return
  245. await this.$axios
  246. .$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
  247. headers: {
  248. Accept: "application/vnd.github.v3+json",
  249. },
  250. })
  251. .then(({ files }) => {
  252. const collections = JSON.parse(Object.values(files)[0].content)
  253. setRESTCollections(collections)
  254. this.fileImported()
  255. })
  256. .catch((e) => {
  257. this.failedImport()
  258. console.error(e)
  259. })
  260. },
  261. hideModal() {
  262. this.mode = "import_export"
  263. this.mySelectedCollectionID = undefined
  264. this.$emit("hide-modal")
  265. },
  266. openDialogChooseFileToReplaceWith() {
  267. this.$refs.inputChooseFileToReplaceWith.click()
  268. },
  269. openDialogChooseFileToImportFrom() {
  270. this.$refs.inputChooseFileToImportFrom.click()
  271. },
  272. replaceWithJSON() {
  273. const reader = new FileReader()
  274. reader.onload = ({ target }) => {
  275. const content = target.result
  276. let collections = JSON.parse(content)
  277. if (collections[0]) {
  278. const [name, folders, requests] = Object.keys(collections[0])
  279. if (
  280. name === "name" &&
  281. folders === "folders" &&
  282. requests === "requests"
  283. ) {
  284. // Do nothing
  285. }
  286. } else if (
  287. collections.info &&
  288. collections.info.schema.includes("v2.1.0")
  289. ) {
  290. collections = [this.parsePostmanCollection(collections)]
  291. } else {
  292. this.failedImport()
  293. }
  294. if (this.collectionsType.type === "team-collections") {
  295. teamUtils
  296. .replaceWithJSON(
  297. this.$apollo,
  298. collections,
  299. this.collectionsType.selectedTeam.id
  300. )
  301. .then((status) => {
  302. if (status) {
  303. this.fileImported()
  304. } else {
  305. this.failedImport()
  306. }
  307. })
  308. .catch((e) => {
  309. console.error(e)
  310. this.failedImport()
  311. })
  312. } else {
  313. setRESTCollections(collections)
  314. this.fileImported()
  315. }
  316. }
  317. reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
  318. this.$refs.inputChooseFileToReplaceWith.value = ""
  319. },
  320. importFromJSON() {
  321. const reader = new FileReader()
  322. reader.onload = ({ target }) => {
  323. const content = target.result
  324. let collections = JSON.parse(content)
  325. if (collections[0]) {
  326. const [name, folders, requests] = Object.keys(collections[0])
  327. if (
  328. name === "name" &&
  329. folders === "folders" &&
  330. requests === "requests"
  331. ) {
  332. // Do nothing
  333. }
  334. } else if (
  335. collections.info &&
  336. collections.info.schema.includes("v2.1.0")
  337. ) {
  338. // replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
  339. collections = JSON.parse(
  340. content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
  341. )
  342. collections = [this.parsePostmanCollection(collections)]
  343. } else {
  344. this.failedImport()
  345. return
  346. }
  347. if (this.collectionsType.type === "team-collections") {
  348. teamUtils
  349. .importFromJSON(
  350. this.$apollo,
  351. collections,
  352. this.collectionsType.selectedTeam.id
  353. )
  354. .then((status) => {
  355. if (status) {
  356. this.$emit("update-team-collections")
  357. this.fileImported()
  358. } else {
  359. this.failedImport()
  360. }
  361. })
  362. .catch((e) => {
  363. console.error(e)
  364. this.failedImport()
  365. })
  366. } else {
  367. appendRESTCollections(collections)
  368. this.fileImported()
  369. }
  370. }
  371. reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
  372. this.$refs.inputChooseFileToImportFrom.value = ""
  373. },
  374. importFromMyCollections() {
  375. teamUtils
  376. .importFromMyCollections(
  377. this.$apollo,
  378. this.mySelectedCollectionID,
  379. this.collectionsType.selectedTeam.id
  380. )
  381. .then((success) => {
  382. if (success) {
  383. this.fileImported()
  384. this.$emit("update-team-collections")
  385. } else {
  386. this.failedImport()
  387. }
  388. })
  389. .catch((e) => {
  390. console.error(e)
  391. this.failedImport()
  392. })
  393. },
  394. async getJSONCollection() {
  395. if (this.collectionsType.type === "my-collections") {
  396. this.collectionJson = JSON.stringify(this.myCollections, null, 2)
  397. } else {
  398. this.collectionJson = await teamUtils.exportAsJSON(
  399. this.$apollo,
  400. this.collectionsType.selectedTeam.id
  401. )
  402. }
  403. return this.collectionJson
  404. },
  405. exportJSON() {
  406. this.getJSONCollection()
  407. const dataToWrite = this.collectionJson
  408. const file = new Blob([dataToWrite], { type: "application/json" })
  409. const a = document.createElement("a")
  410. const url = URL.createObjectURL(file)
  411. a.href = url
  412. // TODO get uri from meta
  413. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
  414. document.body.appendChild(a)
  415. a.click()
  416. this.$toast.success(this.$t("state.download_started"), {
  417. icon: "downloading",
  418. })
  419. setTimeout(() => {
  420. document.body.removeChild(a)
  421. URL.revokeObjectURL(url)
  422. }, 1000)
  423. },
  424. fileImported() {
  425. this.$toast.success(this.$t("state.file_imported"), {
  426. icon: "folder_shared",
  427. })
  428. },
  429. failedImport() {
  430. this.$toast.error(this.$t("import.failed"), {
  431. icon: "error_outline",
  432. })
  433. },
  434. parsePostmanCollection({ info, name, item }) {
  435. const hoppscotchCollection = {
  436. name: "",
  437. folders: [],
  438. requests: [],
  439. }
  440. hoppscotchCollection.name = info ? info.name : name
  441. if (item && item.length > 0) {
  442. for (const collectionItem of item) {
  443. if (collectionItem.request) {
  444. if (
  445. Object.prototype.hasOwnProperty.call(
  446. hoppscotchCollection,
  447. "folders"
  448. )
  449. ) {
  450. hoppscotchCollection.name = info ? info.name : name
  451. hoppscotchCollection.requests.push(
  452. this.parsePostmanRequest(collectionItem)
  453. )
  454. } else {
  455. hoppscotchCollection.name = name || ""
  456. hoppscotchCollection.requests.push(
  457. this.parsePostmanRequest(collectionItem)
  458. )
  459. }
  460. } else if (this.hasFolder(collectionItem)) {
  461. hoppscotchCollection.folders.push(
  462. this.parsePostmanCollection(collectionItem)
  463. )
  464. } else {
  465. hoppscotchCollection.requests.push(
  466. this.parsePostmanRequest(collectionItem)
  467. )
  468. }
  469. }
  470. }
  471. return hoppscotchCollection
  472. },
  473. parsePostmanRequest({ name, request }) {
  474. const pwRequest = {
  475. url: "",
  476. path: "",
  477. method: "",
  478. auth: "",
  479. httpUser: "",
  480. httpPassword: "",
  481. passwordFieldType: "password",
  482. bearerToken: "",
  483. headers: [],
  484. params: [],
  485. bodyParams: [],
  486. rawParams: "",
  487. rawInput: false,
  488. contentType: "",
  489. requestType: "",
  490. name: "",
  491. }
  492. pwRequest.name = name
  493. if (request.url) {
  494. const requestObjectUrl = request.url.raw.match(
  495. /^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
  496. )
  497. if (requestObjectUrl) {
  498. pwRequest.url = requestObjectUrl[1]
  499. pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
  500. }
  501. }
  502. pwRequest.method = request.method
  503. const itemAuth = request.auth ? request.auth : ""
  504. const authType = itemAuth ? itemAuth.type : ""
  505. if (authType === "basic") {
  506. pwRequest.auth = "Basic Auth"
  507. pwRequest.httpUser =
  508. itemAuth.basic[0].key === "username"
  509. ? itemAuth.basic[0].value
  510. : itemAuth.basic[1].value
  511. pwRequest.httpPassword =
  512. itemAuth.basic[0].key === "password"
  513. ? itemAuth.basic[0].value
  514. : itemAuth.basic[1].value
  515. } else if (authType === "oauth2") {
  516. pwRequest.auth = "OAuth 2.0"
  517. pwRequest.bearerToken =
  518. itemAuth.oauth2[0].key === "accessToken"
  519. ? itemAuth.oauth2[0].value
  520. : itemAuth.oauth2[1].value
  521. } else if (authType === "bearer") {
  522. pwRequest.auth = "Bearer Token"
  523. pwRequest.bearerToken = itemAuth.bearer[0].value
  524. }
  525. const requestObjectHeaders = request.header
  526. if (requestObjectHeaders) {
  527. pwRequest.headers = requestObjectHeaders
  528. for (const header of pwRequest.headers) {
  529. delete header.name
  530. delete header.type
  531. }
  532. }
  533. if (request.url) {
  534. const requestObjectParams = request.url.query
  535. if (requestObjectParams) {
  536. pwRequest.params = requestObjectParams
  537. for (const param of pwRequest.params) {
  538. delete param.disabled
  539. }
  540. }
  541. }
  542. if (request.body) {
  543. if (request.body.mode === "urlencoded") {
  544. const params = request.body.urlencoded
  545. pwRequest.bodyParams = params || []
  546. for (const param of pwRequest.bodyParams) {
  547. delete param.type
  548. }
  549. } else if (request.body.mode === "raw") {
  550. pwRequest.rawInput = true
  551. pwRequest.rawParams = request.body.raw
  552. }
  553. }
  554. return pwRequest
  555. },
  556. hasFolder(item) {
  557. return Object.prototype.hasOwnProperty.call(item, "item")
  558. },
  559. },
  560. })
  561. </script>