index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. <template>
  2. <AppSection
  3. label="collections"
  4. :class="{ 'rounded border border-divider': saveRequest }"
  5. >
  6. <div
  7. class="divide-dividerLight bg-primary border-dividerLight sticky top-0 z-10 flex flex-col border-b divide-y rounded-t"
  8. >
  9. <div v-if="!saveRequest" class="search-wrappe">
  10. <input
  11. v-model="filterText"
  12. type="search"
  13. autocomplete="off"
  14. :placeholder="$t('action.search')"
  15. class="flex w-full py-2 pl-4 pr-2 bg-transparent"
  16. />
  17. </div>
  18. <CollectionsChooseType
  19. :collections-type="collectionsType"
  20. :show="showTeamCollections"
  21. :doc="doc"
  22. @update-collection-type="updateCollectionType"
  23. @update-selected-team="updateSelectedTeam"
  24. />
  25. <div class="flex justify-between flex-1">
  26. <ButtonSecondary
  27. v-if="
  28. collectionsType.type == 'team-collections' &&
  29. (collectionsType.selectedTeam == undefined ||
  30. collectionsType.selectedTeam.myRole == 'VIEWER')
  31. "
  32. v-tippy="{ theme: 'tooltip' }"
  33. disabled
  34. class="!rounded-none"
  35. svg="plus"
  36. :title="$t('team.no_access')"
  37. :label="$t('action.new')"
  38. />
  39. <ButtonSecondary
  40. v-else
  41. svg="plus"
  42. :label="$t('action.new')"
  43. class="!rounded-none"
  44. @click.native="displayModalAdd(true)"
  45. />
  46. <span class="flex">
  47. <ButtonSecondary
  48. v-tippy="{ theme: 'tooltip' }"
  49. to="https://docs.hoppscotch.io/features/collections"
  50. blank
  51. :title="$t('app.wiki')"
  52. svg="help-circle"
  53. />
  54. <ButtonSecondary
  55. v-if="!saveRequest"
  56. v-tippy="{ theme: 'tooltip' }"
  57. :disabled="
  58. collectionsType.type == 'team-collections' &&
  59. collectionsType.selectedTeam == undefined
  60. "
  61. svg="archive"
  62. :title="$t('modal.import_export')"
  63. @click.native="displayModalImportExport(true)"
  64. />
  65. </span>
  66. </div>
  67. </div>
  68. <div class="flex flex-col">
  69. <component
  70. :is="
  71. collectionsType.type == 'my-collections'
  72. ? 'CollectionsMyCollection'
  73. : 'CollectionsTeamsCollection'
  74. "
  75. v-for="(collection, index) in filteredCollections"
  76. :key="`collection-${index}`"
  77. :collection-index="index"
  78. :collection="collection"
  79. :doc="doc"
  80. :is-filtered="filterText.length > 0"
  81. :selected="selected.some((coll) => coll == collection)"
  82. :save-request="saveRequest"
  83. :collections-type="collectionsType"
  84. :picked="picked"
  85. @edit-collection="editCollection(collection, index)"
  86. @add-folder="addFolder($event)"
  87. @edit-folder="editFolder($event)"
  88. @edit-request="editRequest($event)"
  89. @duplicate-request="duplicateRequest($event)"
  90. @update-team-collections="updateTeamCollections"
  91. @select-collection="$emit('use-collection', collection)"
  92. @unselect-collection="$emit('remove-collection', collection)"
  93. @select="$emit('select', $event)"
  94. @expand-collection="expandCollection"
  95. @remove-collection="removeCollection"
  96. @remove-request="removeRequest"
  97. />
  98. </div>
  99. <div
  100. v-if="filteredCollections.length === 0 && filterText.length === 0"
  101. class="text-secondaryLight flex flex-col items-center justify-center p-4"
  102. >
  103. <img
  104. :src="`/images/states/${$colorMode.value}/pack.svg`"
  105. loading="lazy"
  106. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  107. :alt="$t('empty.collections')"
  108. />
  109. <span class="pb-4 text-center">
  110. {{ $t("empty.collections") }}
  111. </span>
  112. <ButtonSecondary
  113. v-if="
  114. collectionsType.type == 'team-collections' &&
  115. (collectionsType.selectedTeam == undefined ||
  116. collectionsType.selectedTeam.myRole == 'VIEWER')
  117. "
  118. v-tippy="{ theme: 'tooltip' }"
  119. :title="$t('team.no_access')"
  120. :label="$t('add.new')"
  121. class="mb-4"
  122. filled
  123. />
  124. <ButtonSecondary
  125. v-else
  126. :label="$t('add.new')"
  127. filled
  128. class="mb-4"
  129. @click.native="displayModalAdd(true)"
  130. />
  131. </div>
  132. <div
  133. v-if="filterText.length !== 0 && filteredCollections.length === 0"
  134. class="text-secondaryLight flex flex-col items-center justify-center p-4"
  135. >
  136. <i class="material-icons pb-2 opacity-75">manage_search</i>
  137. <span class="text-center">
  138. {{ $t("state.nothing_found") }} "{{ filterText }}"
  139. </span>
  140. </div>
  141. <CollectionsAdd
  142. :show="showModalAdd"
  143. @submit="addNewRootCollection"
  144. @hide-modal="displayModalAdd(false)"
  145. />
  146. <CollectionsEdit
  147. :show="showModalEdit"
  148. :editing-coll-name="editingCollection ? editingCollection.name : ''"
  149. :placeholder-coll-name="editingCollection ? editingCollection.name : ''"
  150. @hide-modal="displayModalEdit(false)"
  151. @submit="updateEditingCollection"
  152. />
  153. <CollectionsAddFolder
  154. :show="showModalAddFolder"
  155. :folder="editingFolder"
  156. :folder-path="editingFolderPath"
  157. @add-folder="onAddFolder($event)"
  158. @hide-modal="displayModalAddFolder(false)"
  159. />
  160. <CollectionsEditFolder
  161. :show="showModalEditFolder"
  162. @submit="updateEditingFolder"
  163. @hide-modal="displayModalEditFolder(false)"
  164. />
  165. <CollectionsEditRequest
  166. :show="showModalEditRequest"
  167. :placeholder-req-name="editingRequest ? editingRequest.name : ''"
  168. @submit="updateEditingRequest"
  169. @hide-modal="displayModalEditRequest(false)"
  170. />
  171. <CollectionsImportExport
  172. :show="showModalImportExport"
  173. :collections-type="collectionsType"
  174. @hide-modal="displayModalImportExport(false)"
  175. @update-team-collections="updateTeamCollections"
  176. />
  177. </AppSection>
  178. </template>
  179. <script>
  180. import gql from "graphql-tag"
  181. import cloneDeep from "lodash/cloneDeep"
  182. import { defineComponent } from "@nuxtjs/composition-api"
  183. import CollectionsMyCollection from "./my/Collection.vue"
  184. import CollectionsTeamsCollection from "./teams/Collection.vue"
  185. import { currentUser$ } from "~/helpers/fb/auth"
  186. import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
  187. import * as teamUtils from "~/helpers/teams/utils"
  188. import {
  189. restCollections$,
  190. addRESTCollection,
  191. editRESTCollection,
  192. addRESTFolder,
  193. removeRESTCollection,
  194. editRESTFolder,
  195. removeRESTRequest,
  196. editRESTRequest,
  197. saveRESTRequestAs,
  198. } from "~/newstore/collections"
  199. import {
  200. useReadonlyStream,
  201. useStreamSubscriber,
  202. } from "~/helpers/utils/composables"
  203. export default defineComponent({
  204. components: {
  205. CollectionsMyCollection,
  206. CollectionsTeamsCollection,
  207. },
  208. props: {
  209. doc: Boolean,
  210. selected: { type: Array, default: () => [] },
  211. saveRequest: Boolean,
  212. picked: { type: Object, default: () => {} },
  213. },
  214. setup() {
  215. const { subscribeToStream } = useStreamSubscriber()
  216. return {
  217. subscribeTo: subscribeToStream,
  218. collections: useReadonlyStream(restCollections$, []),
  219. currentUser: useReadonlyStream(currentUser$, null),
  220. }
  221. },
  222. data() {
  223. return {
  224. showModalAdd: false,
  225. showModalEdit: false,
  226. showModalImportExport: false,
  227. showModalAddFolder: false,
  228. showModalEditFolder: false,
  229. showModalEditRequest: false,
  230. editingCollection: undefined,
  231. editingCollectionIndex: undefined,
  232. editingFolder: undefined,
  233. editingFolderName: undefined,
  234. editingFolderIndex: undefined,
  235. editingFolderPath: undefined,
  236. editingRequest: undefined,
  237. editingRequestIndex: undefined,
  238. filterText: "",
  239. collectionsType: {
  240. type: "my-collections",
  241. selectedTeam: undefined,
  242. },
  243. teamCollectionAdapter: new TeamCollectionAdapter(null),
  244. teamCollectionsNew: [],
  245. }
  246. },
  247. computed: {
  248. showTeamCollections() {
  249. if (this.currentUser == null) {
  250. return false
  251. }
  252. return true
  253. },
  254. filteredCollections() {
  255. const collections =
  256. this.collectionsType.type === "my-collections"
  257. ? this.collections
  258. : this.teamCollectionsNew
  259. if (!this.filterText) {
  260. return collections
  261. }
  262. if (this.collectionsType.type === "team-collections") {
  263. return []
  264. }
  265. const filterText = this.filterText.toLowerCase()
  266. const filteredCollections = []
  267. for (const collection of collections) {
  268. const filteredRequests = []
  269. const filteredFolders = []
  270. for (const request of collection.requests) {
  271. if (request.name.toLowerCase().includes(filterText))
  272. filteredRequests.push(request)
  273. }
  274. for (const folder of this.collectionsType.type === "team-collections"
  275. ? collection.children
  276. : collection.folders) {
  277. const filteredFolderRequests = []
  278. for (const request of folder.requests) {
  279. if (request.name.toLowerCase().includes(filterText))
  280. filteredFolderRequests.push(request)
  281. }
  282. if (filteredFolderRequests.length > 0) {
  283. const filteredFolder = Object.assign({}, folder)
  284. filteredFolder.requests = filteredFolderRequests
  285. filteredFolders.push(filteredFolder)
  286. }
  287. }
  288. if (
  289. filteredRequests.length + filteredFolders.length > 0 ||
  290. collection.name.toLowerCase().includes(filterText)
  291. ) {
  292. const filteredCollection = Object.assign({}, collection)
  293. filteredCollection.requests = filteredRequests
  294. filteredCollection.folders = filteredFolders
  295. filteredCollections.push(filteredCollection)
  296. }
  297. }
  298. return filteredCollections
  299. },
  300. },
  301. watch: {
  302. "collectionsType.type": function emitstuff() {
  303. this.$emit("update-collection", this.$data.collectionsType.type)
  304. },
  305. "collectionsType.selectedTeam"(value) {
  306. if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
  307. },
  308. },
  309. mounted() {
  310. this.subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
  311. this.teamCollectionsNew = cloneDeep(colls)
  312. })
  313. },
  314. methods: {
  315. updateTeamCollections() {
  316. // TODO: Remove this at some point
  317. },
  318. updateSelectedTeam(newSelectedTeam) {
  319. this.collectionsType.selectedTeam = newSelectedTeam
  320. this.$emit("update-coll-type", this.collectionsType)
  321. },
  322. updateCollectionType(newCollectionType) {
  323. this.collectionsType.type = newCollectionType
  324. this.$emit("update-coll-type", this.collectionsType)
  325. },
  326. // Intented to be called by the CollectionAdd modal submit event
  327. addNewRootCollection(name) {
  328. if (this.collectionsType.type === "my-collections") {
  329. addRESTCollection({
  330. name,
  331. folders: [],
  332. requests: [],
  333. })
  334. } else if (
  335. this.collectionsType.type === "team-collections" &&
  336. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  337. ) {
  338. teamUtils
  339. .createNewRootCollection(
  340. this.$apollo,
  341. name,
  342. this.collectionsType.selectedTeam.id
  343. )
  344. .then(() => {
  345. this.$toast.success(this.$t("collection.created"))
  346. })
  347. .catch((e) => {
  348. this.$toast.error(this.$t("error.something_went_wrong"))
  349. console.error(e)
  350. })
  351. }
  352. this.displayModalAdd(false)
  353. },
  354. // Intented to be called by CollectionEdit modal submit event
  355. updateEditingCollection(newName) {
  356. if (!newName) {
  357. this.$toast.error(this.$t("collection.invalid_name"))
  358. return
  359. }
  360. if (this.collectionsType.type === "my-collections") {
  361. const collectionUpdated = {
  362. ...this.editingCollection,
  363. name: newName,
  364. }
  365. editRESTCollection(this.editingCollectionIndex, collectionUpdated)
  366. } else if (
  367. this.collectionsType.type === "team-collections" &&
  368. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  369. ) {
  370. teamUtils
  371. .renameCollection(this.$apollo, newName, this.editingCollection.id)
  372. .then(() => {
  373. this.$toast.success(this.$t("collection.renamed"))
  374. })
  375. .catch((e) => {
  376. this.$toast.error(this.$t("error.something_went_wrong"))
  377. console.error(e)
  378. })
  379. }
  380. this.displayModalEdit(false)
  381. },
  382. // Intended to be called by CollectionEditFolder modal submit event
  383. updateEditingFolder(name) {
  384. if (this.collectionsType.type === "my-collections") {
  385. editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
  386. } else if (
  387. this.collectionsType.type === "team-collections" &&
  388. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  389. ) {
  390. teamUtils
  391. .renameCollection(this.$apollo, name, this.editingFolder.id)
  392. .then(() => {
  393. this.$toast.success(this.$t("folder.renamed"))
  394. })
  395. .catch((e) => {
  396. this.$toast.error(this.$t("error.something_went_wrong"))
  397. console.error(e)
  398. })
  399. }
  400. this.displayModalEditFolder(false)
  401. },
  402. // Intented to by called by CollectionsEditRequest modal submit event
  403. updateEditingRequest(requestUpdateData) {
  404. const requestUpdated = {
  405. ...this.editingRequest,
  406. name: requestUpdateData.name || this.editingRequest.name,
  407. }
  408. if (this.collectionsType.type === "my-collections") {
  409. editRESTRequest(
  410. this.editingFolderPath,
  411. this.editingRequestIndex,
  412. requestUpdated
  413. )
  414. } else if (
  415. this.collectionsType.type === "team-collections" &&
  416. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  417. ) {
  418. const requestName = requestUpdateData.name || this.editingRequest.name
  419. teamUtils
  420. .updateRequest(
  421. this.$apollo,
  422. requestUpdated,
  423. requestName,
  424. this.editingRequestIndex
  425. )
  426. .then(() => {
  427. this.$toast.success(this.$t("request.renamed"))
  428. this.$emit("update-team-collections")
  429. })
  430. .catch((e) => {
  431. this.$toast.error(this.$t("error.something_went_wrong"))
  432. console.error(e)
  433. })
  434. }
  435. this.displayModalEditRequest(false)
  436. },
  437. displayModalAdd(shouldDisplay) {
  438. this.showModalAdd = shouldDisplay
  439. },
  440. displayModalEdit(shouldDisplay) {
  441. this.showModalEdit = shouldDisplay
  442. if (!shouldDisplay) this.resetSelectedData()
  443. },
  444. displayModalImportExport(shouldDisplay) {
  445. this.showModalImportExport = shouldDisplay
  446. },
  447. displayModalAddFolder(shouldDisplay) {
  448. this.showModalAddFolder = shouldDisplay
  449. if (!shouldDisplay) this.resetSelectedData()
  450. },
  451. displayModalEditFolder(shouldDisplay) {
  452. this.showModalEditFolder = shouldDisplay
  453. if (!shouldDisplay) this.resetSelectedData()
  454. },
  455. displayModalEditRequest(shouldDisplay) {
  456. this.showModalEditRequest = shouldDisplay
  457. if (!shouldDisplay) this.resetSelectedData()
  458. },
  459. editCollection(collection, collectionIndex) {
  460. this.$data.editingCollection = collection
  461. this.$data.editingCollectionIndex = collectionIndex
  462. this.displayModalEdit(true)
  463. },
  464. onAddFolder({ name, folder, path }) {
  465. if (this.collectionsType.type === "my-collections") {
  466. addRESTFolder(name, path)
  467. } else if (this.collectionsType.type === "team-collections") {
  468. if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
  469. this.$apollo
  470. .mutate({
  471. mutation: gql`
  472. mutation CreateChildCollection(
  473. $childTitle: String!
  474. $collectionID: ID!
  475. ) {
  476. createChildCollection(
  477. childTitle: $childTitle
  478. collectionID: $collectionID
  479. ) {
  480. id
  481. }
  482. }
  483. `,
  484. // Parameters
  485. variables: {
  486. childTitle: name,
  487. collectionID: folder.id,
  488. },
  489. })
  490. .then(() => {
  491. this.$toast.success(this.$t("folder.created"))
  492. this.$emit("update-team-collections")
  493. })
  494. .catch((e) => {
  495. this.$toast.error(this.$t("error.something_went_wrong"))
  496. console.error(e)
  497. })
  498. }
  499. }
  500. this.displayModalAddFolder(false)
  501. },
  502. addFolder(payload) {
  503. const { folder, path } = payload
  504. this.$data.editingFolder = folder
  505. this.$data.editingFolderPath = path
  506. this.displayModalAddFolder(true)
  507. },
  508. editFolder(payload) {
  509. const { collectionIndex, folder, folderIndex, folderPath } = payload
  510. this.$data.editingCollectionIndex = collectionIndex
  511. this.$data.editingFolder = folder
  512. this.$data.editingFolderIndex = folderIndex
  513. this.$data.editingFolderPath = folderPath
  514. this.$data.collectionsType = this.collectionsType
  515. this.displayModalEditFolder(true)
  516. },
  517. editRequest(payload) {
  518. const {
  519. collectionIndex,
  520. folderIndex,
  521. folderName,
  522. request,
  523. requestIndex,
  524. folderPath,
  525. } = payload
  526. this.$data.editingCollectionIndex = collectionIndex
  527. this.$data.editingFolderIndex = folderIndex
  528. this.$data.editingFolderName = folderName
  529. this.$data.editingRequest = request
  530. this.$data.editingRequestIndex = requestIndex
  531. this.editingFolderPath = folderPath
  532. this.$emit("select-request", requestIndex)
  533. this.displayModalEditRequest(true)
  534. },
  535. resetSelectedData() {
  536. this.$data.editingCollection = undefined
  537. this.$data.editingCollectionIndex = undefined
  538. this.$data.editingFolder = undefined
  539. this.$data.editingFolderIndex = undefined
  540. this.$data.editingRequest = undefined
  541. this.$data.editingRequestIndex = undefined
  542. },
  543. expandCollection(collectionID) {
  544. this.teamCollectionAdapter.expandCollection(collectionID)
  545. },
  546. removeCollection({ collectionsType, collectionIndex, collectionID }) {
  547. if (collectionsType.type === "my-collections") {
  548. // Cancel pick if picked collection is deleted
  549. if (
  550. this.picked &&
  551. this.picked.pickedType === "my-collection" &&
  552. this.picked.collectionIndex === collectionIndex
  553. ) {
  554. this.$emit("select", { picked: null })
  555. }
  556. removeRESTCollection(collectionIndex)
  557. this.$toast.success(this.$t("state.deleted"))
  558. } else if (collectionsType.type === "team-collections") {
  559. // Cancel pick if picked collection is deleted
  560. if (
  561. this.picked &&
  562. this.picked.pickedType === "teams-collection" &&
  563. this.picked.collectionID === collectionID
  564. ) {
  565. this.$emit("select", { picked: null })
  566. }
  567. if (collectionsType.selectedTeam.myRole !== "VIEWER") {
  568. this.$apollo
  569. .mutate({
  570. // Query
  571. mutation: gql`
  572. mutation ($collectionID: ID!) {
  573. deleteCollection(collectionID: $collectionID)
  574. }
  575. `,
  576. // Parameters
  577. variables: {
  578. collectionID,
  579. },
  580. })
  581. .then(() => {
  582. this.$toast.success(this.$t("state.deleted"))
  583. })
  584. .catch((e) => {
  585. this.$toast.error(this.$t("error.something_went_wrong"))
  586. console.error(e)
  587. })
  588. }
  589. }
  590. },
  591. removeRequest({ requestIndex, folderPath }) {
  592. if (this.collectionsType.type === "my-collections") {
  593. // Cancel pick if the picked item is being deleted
  594. if (
  595. this.picked &&
  596. this.picked.pickedType === "my-request" &&
  597. this.picked.folderPath === folderPath &&
  598. this.picked.requestIndex === requestIndex
  599. ) {
  600. this.$emit("select", { picked: null })
  601. }
  602. removeRESTRequest(folderPath, requestIndex)
  603. this.$toast.success(this.$t("state.deleted"))
  604. } else if (this.collectionsType.type === "team-collections") {
  605. // Cancel pick if the picked item is being deleted
  606. if (
  607. this.picked &&
  608. this.picked.pickedType === "teams-request" &&
  609. this.picked.requestID === requestIndex
  610. ) {
  611. this.$emit("select", { picked: null })
  612. }
  613. teamUtils
  614. .deleteRequest(this.$apollo, requestIndex)
  615. .then(() => {
  616. this.$toast.success(this.$t("state.deleted"))
  617. })
  618. .catch((e) => {
  619. this.$toast.error(this.$t("error.something_went_wrong"))
  620. console.error(e)
  621. })
  622. }
  623. },
  624. duplicateRequest({ folderPath, request }) {
  625. saveRESTRequestAs(folderPath, {
  626. ...cloneDeep(request),
  627. name: `${request.name} - ${this.$t("action.duplicate")}`,
  628. })
  629. },
  630. },
  631. })
  632. </script>