ImportExport.vue 16 KB

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