index.vue 21 KB

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