index.vue 21 KB

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