index.vue 21 KB

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