index.vue 21 KB

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