index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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-coll-name="editingCollection ? editingCollection.name : ''"
  150. :placeholder-coll-name="editingCollection ? editingCollection.name : ''"
  151. @hide-modal="displayModalEdit(false)"
  152. @submit="updateEditingCollection"
  153. />
  154. <CollectionsAddFolder
  155. :show="showModalAddFolder"
  156. :folder="editingFolder"
  157. :folder-path="editingFolderPath"
  158. @add-folder="onAddFolder($event)"
  159. @hide-modal="displayModalAddFolder(false)"
  160. />
  161. <CollectionsEditFolder
  162. :show="showModalEditFolder"
  163. @submit="updateEditingFolder"
  164. @hide-modal="displayModalEditFolder(false)"
  165. />
  166. <CollectionsEditRequest
  167. :show="showModalEditRequest"
  168. :placeholder-req-name="editingRequest ? editingRequest.name : ''"
  169. @submit="updateEditingRequest"
  170. @hide-modal="displayModalEditRequest(false)"
  171. />
  172. <CollectionsImportExport
  173. :show="showModalImportExport"
  174. :collections-type="collectionsType"
  175. @hide-modal="displayModalImportExport(false)"
  176. @update-team-collections="updateTeamCollections"
  177. />
  178. </AppSection>
  179. </template>
  180. <script>
  181. import gql from "graphql-tag"
  182. import cloneDeep from "lodash/cloneDeep"
  183. import { defineComponent } from "@nuxtjs/composition-api"
  184. import CollectionsMyCollection from "./my/Collection.vue"
  185. import CollectionsTeamsCollection from "./teams/Collection.vue"
  186. import { currentUser$ } from "~/helpers/fb/auth"
  187. import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
  188. import * as teamUtils from "~/helpers/teams/utils"
  189. import {
  190. restCollections$,
  191. addRESTCollection,
  192. editRESTCollection,
  193. addRESTFolder,
  194. removeRESTCollection,
  195. editRESTFolder,
  196. removeRESTRequest,
  197. editRESTRequest,
  198. saveRESTRequestAs,
  199. } from "~/newstore/collections"
  200. import {
  201. useReadonlyStream,
  202. useStreamSubscriber,
  203. } from "~/helpers/utils/composables"
  204. export default defineComponent({
  205. components: {
  206. CollectionsMyCollection,
  207. CollectionsTeamsCollection,
  208. },
  209. props: {
  210. doc: Boolean,
  211. selected: { type: Array, default: () => [] },
  212. saveRequest: Boolean,
  213. picked: { type: Object, default: () => {} },
  214. },
  215. setup() {
  216. const { subscribeToStream } = useStreamSubscriber()
  217. return {
  218. subscribeTo: subscribeToStream,
  219. collections: useReadonlyStream(restCollections$, []),
  220. currentUser: useReadonlyStream(currentUser$, null),
  221. }
  222. },
  223. data() {
  224. return {
  225. showModalAdd: false,
  226. showModalEdit: false,
  227. showModalImportExport: false,
  228. showModalAddFolder: false,
  229. showModalEditFolder: false,
  230. showModalEditRequest: false,
  231. editingCollection: undefined,
  232. editingCollectionIndex: undefined,
  233. editingFolder: undefined,
  234. editingFolderName: undefined,
  235. editingFolderIndex: undefined,
  236. editingFolderPath: undefined,
  237. editingRequest: undefined,
  238. editingRequestIndex: undefined,
  239. filterText: "",
  240. collectionsType: {
  241. type: "my-collections",
  242. selectedTeam: undefined,
  243. },
  244. teamCollectionAdapter: new TeamCollectionAdapter(null),
  245. teamCollectionsNew: [],
  246. }
  247. },
  248. computed: {
  249. showTeamCollections() {
  250. if (this.currentUser == null) {
  251. return false
  252. }
  253. return true
  254. },
  255. filteredCollections() {
  256. const collections =
  257. this.collectionsType.type === "my-collections"
  258. ? this.collections
  259. : this.teamCollectionsNew
  260. if (!this.filterText) {
  261. return collections
  262. }
  263. if (this.collectionsType.type === "team-collections") {
  264. return []
  265. }
  266. const filterText = this.filterText.toLowerCase()
  267. const filteredCollections = []
  268. for (const collection of collections) {
  269. const filteredRequests = []
  270. const filteredFolders = []
  271. for (const request of collection.requests) {
  272. if (request.name.toLowerCase().includes(filterText))
  273. filteredRequests.push(request)
  274. }
  275. for (const folder of this.collectionsType.type === "team-collections"
  276. ? collection.children
  277. : collection.folders) {
  278. const filteredFolderRequests = []
  279. for (const request of folder.requests) {
  280. if (request.name.toLowerCase().includes(filterText))
  281. filteredFolderRequests.push(request)
  282. }
  283. if (filteredFolderRequests.length > 0) {
  284. const filteredFolder = Object.assign({}, folder)
  285. filteredFolder.requests = filteredFolderRequests
  286. filteredFolders.push(filteredFolder)
  287. }
  288. }
  289. if (
  290. filteredRequests.length + filteredFolders.length > 0 ||
  291. collection.name.toLowerCase().includes(filterText)
  292. ) {
  293. const filteredCollection = Object.assign({}, collection)
  294. filteredCollection.requests = filteredRequests
  295. filteredCollection.folders = filteredFolders
  296. filteredCollections.push(filteredCollection)
  297. }
  298. }
  299. return filteredCollections
  300. },
  301. },
  302. watch: {
  303. "collectionsType.type": function emitstuff() {
  304. this.$emit("update-collection", this.$data.collectionsType.type)
  305. },
  306. "collectionsType.selectedTeam"(value) {
  307. if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
  308. },
  309. },
  310. mounted() {
  311. this.subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
  312. this.teamCollectionsNew = cloneDeep(colls)
  313. })
  314. },
  315. methods: {
  316. updateTeamCollections() {
  317. // TODO: Remove this at some point
  318. },
  319. updateSelectedTeam(newSelectedTeam) {
  320. this.collectionsType.selectedTeam = newSelectedTeam
  321. this.$emit("update-coll-type", this.collectionsType)
  322. },
  323. updateCollectionType(newCollectionType) {
  324. this.collectionsType.type = newCollectionType
  325. this.$emit("update-coll-type", this.collectionsType)
  326. },
  327. // Intented to be called by the CollectionAdd modal submit event
  328. addNewRootCollection(name) {
  329. if (this.collectionsType.type === "my-collections") {
  330. addRESTCollection({
  331. name,
  332. folders: [],
  333. requests: [],
  334. })
  335. } else if (
  336. this.collectionsType.type === "team-collections" &&
  337. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  338. ) {
  339. teamUtils
  340. .createNewRootCollection(
  341. this.$apollo,
  342. name,
  343. this.collectionsType.selectedTeam.id
  344. )
  345. .then(() => {
  346. this.$toast.success(this.$t("collection.created"))
  347. })
  348. .catch((e) => {
  349. this.$toast.error(this.$t("error.something_went_wrong"))
  350. console.error(e)
  351. })
  352. }
  353. this.displayModalAdd(false)
  354. },
  355. // Intented to be called by CollectionEdit modal submit event
  356. updateEditingCollection(newName) {
  357. if (!newName) {
  358. this.$toast.error(this.$t("collection.invalid_name"))
  359. return
  360. }
  361. if (this.collectionsType.type === "my-collections") {
  362. const collectionUpdated = {
  363. ...this.editingCollection,
  364. name: newName,
  365. }
  366. editRESTCollection(this.editingCollectionIndex, collectionUpdated)
  367. } else if (
  368. this.collectionsType.type === "team-collections" &&
  369. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  370. ) {
  371. teamUtils
  372. .renameCollection(this.$apollo, newName, this.editingCollection.id)
  373. .then(() => {
  374. this.$toast.success(this.$t("collection.renamed"))
  375. })
  376. .catch((e) => {
  377. this.$toast.error(this.$t("error.something_went_wrong"))
  378. console.error(e)
  379. })
  380. }
  381. this.displayModalEdit(false)
  382. },
  383. // Intended to be called by CollectionEditFolder modal submit event
  384. updateEditingFolder(name) {
  385. if (this.collectionsType.type === "my-collections") {
  386. editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
  387. } else if (
  388. this.collectionsType.type === "team-collections" &&
  389. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  390. ) {
  391. teamUtils
  392. .renameCollection(this.$apollo, name, this.editingFolder.id)
  393. .then(() => {
  394. this.$toast.success(this.$t("folder.renamed"))
  395. })
  396. .catch((e) => {
  397. this.$toast.error(this.$t("error.something_went_wrong"))
  398. console.error(e)
  399. })
  400. }
  401. this.displayModalEditFolder(false)
  402. },
  403. // Intented to by called by CollectionsEditRequest modal submit event
  404. updateEditingRequest(requestUpdateData) {
  405. const requestUpdated = {
  406. ...this.editingRequest,
  407. name: requestUpdateData.name || this.editingRequest.name,
  408. }
  409. if (this.collectionsType.type === "my-collections") {
  410. editRESTRequest(
  411. this.editingFolderPath,
  412. this.editingRequestIndex,
  413. requestUpdated
  414. )
  415. } else if (
  416. this.collectionsType.type === "team-collections" &&
  417. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  418. ) {
  419. const requestName = requestUpdateData.name || this.editingRequest.name
  420. teamUtils
  421. .updateRequest(
  422. this.$apollo,
  423. requestUpdated,
  424. requestName,
  425. this.editingRequestIndex
  426. )
  427. .then(() => {
  428. this.$toast.success(this.$t("request.renamed"))
  429. this.$emit("update-team-collections")
  430. })
  431. .catch((e) => {
  432. this.$toast.error(this.$t("error.something_went_wrong"))
  433. console.error(e)
  434. })
  435. }
  436. this.displayModalEditRequest(false)
  437. },
  438. displayModalAdd(shouldDisplay) {
  439. this.showModalAdd = shouldDisplay
  440. },
  441. displayModalEdit(shouldDisplay) {
  442. this.showModalEdit = shouldDisplay
  443. if (!shouldDisplay) this.resetSelectedData()
  444. },
  445. displayModalImportExport(shouldDisplay) {
  446. this.showModalImportExport = shouldDisplay
  447. },
  448. displayModalAddFolder(shouldDisplay) {
  449. this.showModalAddFolder = shouldDisplay
  450. if (!shouldDisplay) this.resetSelectedData()
  451. },
  452. displayModalEditFolder(shouldDisplay) {
  453. this.showModalEditFolder = shouldDisplay
  454. if (!shouldDisplay) this.resetSelectedData()
  455. },
  456. displayModalEditRequest(shouldDisplay) {
  457. this.showModalEditRequest = shouldDisplay
  458. if (!shouldDisplay) this.resetSelectedData()
  459. },
  460. editCollection(collection, collectionIndex) {
  461. this.$data.editingCollection = collection
  462. this.$data.editingCollectionIndex = collectionIndex
  463. this.displayModalEdit(true)
  464. },
  465. onAddFolder({ name, folder, path }) {
  466. if (this.collectionsType.type === "my-collections") {
  467. addRESTFolder(name, path)
  468. } else if (this.collectionsType.type === "team-collections") {
  469. if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
  470. this.$apollo
  471. .mutate({
  472. mutation: gql`
  473. mutation CreateChildCollection(
  474. $childTitle: String!
  475. $collectionID: ID!
  476. ) {
  477. createChildCollection(
  478. childTitle: $childTitle
  479. collectionID: $collectionID
  480. ) {
  481. id
  482. }
  483. }
  484. `,
  485. // Parameters
  486. variables: {
  487. childTitle: name,
  488. collectionID: folder.id,
  489. },
  490. })
  491. .then(() => {
  492. this.$toast.success(this.$t("folder.created"))
  493. this.$emit("update-team-collections")
  494. })
  495. .catch((e) => {
  496. this.$toast.error(this.$t("error.something_went_wrong"))
  497. console.error(e)
  498. })
  499. }
  500. }
  501. this.displayModalAddFolder(false)
  502. },
  503. addFolder(payload) {
  504. const { folder, path } = payload
  505. this.$data.editingFolder = folder
  506. this.$data.editingFolderPath = path
  507. this.displayModalAddFolder(true)
  508. },
  509. editFolder(payload) {
  510. const { collectionIndex, folder, folderIndex, folderPath } = payload
  511. this.$data.editingCollectionIndex = collectionIndex
  512. this.$data.editingFolder = folder
  513. this.$data.editingFolderIndex = folderIndex
  514. this.$data.editingFolderPath = folderPath
  515. this.$data.collectionsType = this.collectionsType
  516. this.displayModalEditFolder(true)
  517. },
  518. editRequest(payload) {
  519. const {
  520. collectionIndex,
  521. folderIndex,
  522. folderName,
  523. request,
  524. requestIndex,
  525. folderPath,
  526. } = payload
  527. this.$data.editingCollectionIndex = collectionIndex
  528. this.$data.editingFolderIndex = folderIndex
  529. this.$data.editingFolderName = folderName
  530. this.$data.editingRequest = request
  531. this.$data.editingRequestIndex = requestIndex
  532. this.editingFolderPath = folderPath
  533. this.$emit("select-request", requestIndex)
  534. this.displayModalEditRequest(true)
  535. },
  536. resetSelectedData() {
  537. this.$data.editingCollection = undefined
  538. this.$data.editingCollectionIndex = undefined
  539. this.$data.editingFolder = undefined
  540. this.$data.editingFolderIndex = undefined
  541. this.$data.editingRequest = undefined
  542. this.$data.editingRequestIndex = undefined
  543. },
  544. expandCollection(collectionID) {
  545. this.teamCollectionAdapter.expandCollection(collectionID)
  546. },
  547. removeCollection({ collectionsType, collectionIndex, collectionID }) {
  548. if (collectionsType.type === "my-collections") {
  549. // Cancel pick if picked collection is deleted
  550. if (
  551. this.picked &&
  552. this.picked.pickedType === "my-collection" &&
  553. this.picked.collectionIndex === collectionIndex
  554. ) {
  555. this.$emit("select", { picked: null })
  556. }
  557. removeRESTCollection(collectionIndex)
  558. this.$toast.success(this.$t("state.deleted"))
  559. } else if (collectionsType.type === "team-collections") {
  560. // Cancel pick if picked collection is deleted
  561. if (
  562. this.picked &&
  563. this.picked.pickedType === "teams-collection" &&
  564. this.picked.collectionID === collectionID
  565. ) {
  566. this.$emit("select", { picked: null })
  567. }
  568. if (collectionsType.selectedTeam.myRole !== "VIEWER") {
  569. this.$apollo
  570. .mutate({
  571. // Query
  572. mutation: gql`
  573. mutation ($collectionID: ID!) {
  574. deleteCollection(collectionID: $collectionID)
  575. }
  576. `,
  577. // Parameters
  578. variables: {
  579. collectionID,
  580. },
  581. })
  582. .then(() => {
  583. this.$toast.success(this.$t("state.deleted"))
  584. })
  585. .catch((e) => {
  586. this.$toast.error(this.$t("error.something_went_wrong"))
  587. console.error(e)
  588. })
  589. }
  590. }
  591. },
  592. removeRequest({ requestIndex, folderPath }) {
  593. if (this.collectionsType.type === "my-collections") {
  594. // Cancel pick if the picked item is being deleted
  595. if (
  596. this.picked &&
  597. this.picked.pickedType === "my-request" &&
  598. this.picked.folderPath === folderPath &&
  599. this.picked.requestIndex === requestIndex
  600. ) {
  601. this.$emit("select", { picked: null })
  602. }
  603. removeRESTRequest(folderPath, requestIndex)
  604. this.$toast.success(this.$t("state.deleted"))
  605. } else if (this.collectionsType.type === "team-collections") {
  606. // Cancel pick if the picked item is being deleted
  607. if (
  608. this.picked &&
  609. this.picked.pickedType === "teams-request" &&
  610. this.picked.requestID === requestIndex
  611. ) {
  612. this.$emit("select", { picked: null })
  613. }
  614. teamUtils
  615. .deleteRequest(this.$apollo, requestIndex)
  616. .then(() => {
  617. this.$toast.success(this.$t("state.deleted"))
  618. })
  619. .catch((e) => {
  620. this.$toast.error(this.$t("error.something_went_wrong"))
  621. console.error(e)
  622. })
  623. }
  624. },
  625. duplicateRequest({ folderPath, request }) {
  626. saveRESTRequestAs(folderPath, {
  627. ...cloneDeep(request),
  628. name: `${request.name} - ${this.$t("action.duplicate")}`,
  629. })
  630. },
  631. },
  632. })
  633. </script>