index.vue 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085
  1. <template>
  2. <div
  3. :class="{
  4. 'rounded border border-divider': saveRequest,
  5. 'bg-primaryDark':
  6. draggingToRoot && currentReorderingStatus.type !== 'request',
  7. }"
  8. class="flex-1"
  9. @drop.prevent="dropToRoot"
  10. @dragover.prevent="draggingToRoot = true"
  11. @dragend="draggingToRoot = false"
  12. >
  13. <div
  14. class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary border-b border-dividerLight"
  15. :class="{ 'rounded-t': saveRequest }"
  16. :style="
  17. saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
  18. "
  19. >
  20. <WorkspaceCurrent :section="t('tab.collections')" />
  21. <input
  22. v-model="filterTexts"
  23. type="search"
  24. autocomplete="off"
  25. class="flex w-full bg-transparent px-4 py-2 h-8"
  26. :placeholder="t('action.search')"
  27. :disabled="collectionsType.type === 'team-collections'"
  28. />
  29. </div>
  30. <CollectionsMyCollections
  31. v-if="collectionsType.type === 'my-collections'"
  32. :collections-type="collectionsType"
  33. :filtered-collections="filteredCollections"
  34. :filter-text="filterTexts"
  35. :save-request="saveRequest"
  36. :picked="picked"
  37. @add-folder="addFolder"
  38. @add-request="addRequest"
  39. @edit-collection="editCollection"
  40. @edit-folder="editFolder"
  41. @edit-properties="editProperties"
  42. @export-data="exportData"
  43. @remove-collection="removeCollection"
  44. @remove-folder="removeFolder"
  45. @share-request="shareRequest"
  46. @drop-collection="dropCollection"
  47. @update-request-order="updateRequestOrder"
  48. @update-collection-order="updateCollectionOrder"
  49. @edit-request="editRequest"
  50. @duplicate-request="duplicateRequest"
  51. @remove-request="removeRequest"
  52. @select-request="selectRequest"
  53. @select="selectPicked"
  54. @drop-request="dropRequest"
  55. @display-modal-add="displayModalAdd(true)"
  56. @display-modal-import-export="displayModalImportExport(true)"
  57. />
  58. <CollectionsTeamCollections
  59. v-else
  60. :collections-type="collectionsType"
  61. :team-collection-list="teamCollectionList"
  62. :team-loading-collections="teamLoadingCollections"
  63. :export-loading="exportLoading"
  64. :duplicate-loading="duplicateLoading"
  65. :save-request="saveRequest"
  66. :picked="picked"
  67. :collection-move-loading="collectionMoveLoading"
  68. :request-move-loading="requestMoveLoading"
  69. @add-request="addRequest"
  70. @add-folder="addFolder"
  71. @edit-collection="editCollection"
  72. @edit-folder="editFolder"
  73. @edit-properties="editProperties"
  74. @export-data="exportData"
  75. @remove-collection="removeCollection"
  76. @remove-folder="removeFolder"
  77. @share-request="shareRequest"
  78. @edit-request="editRequest"
  79. @duplicate-request="duplicateRequest"
  80. @remove-request="removeRequest"
  81. @select-request="selectRequest"
  82. @select="selectPicked"
  83. @drop-request="dropRequest"
  84. @drop-collection="dropCollection"
  85. @update-request-order="updateRequestOrder"
  86. @update-collection-order="updateCollectionOrder"
  87. @expand-team-collection="expandTeamCollection"
  88. @display-modal-add="displayModalAdd(true)"
  89. @display-modal-import-export="displayModalImportExport(true)"
  90. />
  91. <div
  92. class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
  93. :class="{
  94. '!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
  95. }"
  96. >
  97. <icon-lucide-list-end class="svg-icons !h-8 !w-8" />
  98. </div>
  99. <CollectionsAdd
  100. :show="showModalAdd"
  101. :loading-state="modalLoadingState"
  102. @submit="addNewRootCollection"
  103. @hide-modal="displayModalAdd(false)"
  104. />
  105. <CollectionsAddRequest
  106. :show="showModalAddRequest"
  107. :loading-state="modalLoadingState"
  108. @add-request="onAddRequest"
  109. @hide-modal="displayModalAddRequest(false)"
  110. />
  111. <CollectionsAddFolder
  112. :show="showModalAddFolder"
  113. :loading-state="modalLoadingState"
  114. @add-folder="onAddFolder"
  115. @hide-modal="displayModalAddFolder(false)"
  116. />
  117. <CollectionsEdit
  118. :show="showModalEditCollection"
  119. :editing-collection-name="editingCollectionName ?? ''"
  120. :loading-state="modalLoadingState"
  121. @hide-modal="displayModalEditCollection(false)"
  122. @submit="updateEditingCollection"
  123. />
  124. <CollectionsEditFolder
  125. :show="showModalEditFolder"
  126. :editing-folder-name="editingFolderName ?? ''"
  127. :loading-state="modalLoadingState"
  128. @submit="updateEditingFolder"
  129. @hide-modal="displayModalEditFolder(false)"
  130. />
  131. <CollectionsEditRequest
  132. v-model="editingRequestName"
  133. :show="showModalEditRequest"
  134. :loading-state="modalLoadingState"
  135. @submit="updateEditingRequest"
  136. @hide-modal="displayModalEditRequest(false)"
  137. />
  138. <HoppSmartConfirmModal
  139. :show="showConfirmModal"
  140. :title="confirmModalTitle"
  141. :loading-state="modalLoadingState"
  142. @hide-modal="showConfirmModal = false"
  143. @resolve="resolveConfirmModal"
  144. />
  145. <CollectionsImportExport
  146. v-if="showModalImportExport"
  147. :collections-type="collectionsType"
  148. @hide-modal="displayModalImportExport(false)"
  149. />
  150. <TeamsAdd
  151. :show="showTeamModalAdd"
  152. @hide-modal="displayTeamModalAdd(false)"
  153. />
  154. <CollectionsProperties
  155. :show="showModalEditProperties"
  156. :editing-properties="editingProperties"
  157. @hide-modal="displayModalEditProperties(false)"
  158. @set-collection-properties="setCollectionProperties"
  159. />
  160. </div>
  161. </template>
  162. <script setup lang="ts">
  163. import { computed, nextTick, PropType, ref, watch } from "vue"
  164. import { useToast } from "@composables/toast"
  165. import { useI18n } from "@composables/i18n"
  166. import { Picked } from "~/helpers/types/HoppPicked"
  167. import { useReadonlyStream } from "~/composables/stream"
  168. import { useLocalState } from "~/newstore/localstate"
  169. import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
  170. import { pipe } from "fp-ts/function"
  171. import * as TE from "fp-ts/TaskEither"
  172. import {
  173. addRESTCollection,
  174. addRESTFolder,
  175. editRESTCollection,
  176. editRESTFolder,
  177. editRESTRequest,
  178. moveRESTRequest,
  179. removeRESTCollection,
  180. removeRESTFolder,
  181. removeRESTRequest,
  182. restCollections$,
  183. saveRESTRequestAs,
  184. updateRESTRequestOrder,
  185. updateRESTCollectionOrder,
  186. moveRESTFolder,
  187. navigateToFolderWithIndexPath,
  188. restCollectionStore,
  189. cascaseParentCollectionForHeaderAuth,
  190. } from "~/newstore/collections"
  191. import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
  192. import {
  193. HoppCollection,
  194. HoppRESTRequest,
  195. makeCollection,
  196. } from "@hoppscotch/data"
  197. import { cloneDeep, isEqual } from "lodash-es"
  198. import { GQLError } from "~/helpers/backend/GQLClient"
  199. import {
  200. createNewRootCollection,
  201. createChildCollection,
  202. renameCollection,
  203. deleteCollection,
  204. moveRESTTeamCollection,
  205. updateOrderRESTTeamCollection,
  206. } from "~/helpers/backend/mutations/TeamCollection"
  207. import {
  208. updateTeamRequest,
  209. createRequestInCollection,
  210. deleteTeamRequest,
  211. moveRESTTeamRequest,
  212. updateOrderRESTTeamRequest,
  213. } from "~/helpers/backend/mutations/TeamRequest"
  214. import { TeamCollection } from "~/helpers/teams/TeamCollection"
  215. import { Collection as NodeCollection } from "./MyCollections.vue"
  216. import {
  217. getCompleteCollectionTree,
  218. teamCollToHoppRESTColl,
  219. } from "~/helpers/backend/helpers"
  220. import { platform } from "~/platform"
  221. import {
  222. getRequestsByPath,
  223. resolveSaveContextOnRequestReorder,
  224. } from "~/helpers/collection/request"
  225. import {
  226. getFoldersByPath,
  227. resolveSaveContextOnCollectionReorder,
  228. updateSaveContextForAffectedRequests,
  229. updateInheritedPropertiesForAffectedRequests,
  230. resetTeamRequestsContext,
  231. } from "~/helpers/collection/collection"
  232. import { currentReorderingStatus$ } from "~/newstore/reordering"
  233. import { defineActionHandler, invokeAction } from "~/helpers/actions"
  234. import { WorkspaceService } from "~/services/workspace.service"
  235. import { useService } from "dioc/vue"
  236. import { RESTTabService } from "~/services/tab/rest"
  237. import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
  238. const t = useI18n()
  239. const toast = useToast()
  240. const tabs = useService(RESTTabService)
  241. const props = defineProps({
  242. saveRequest: {
  243. type: Boolean,
  244. default: false,
  245. required: false,
  246. },
  247. picked: {
  248. type: Object as PropType<Picked | null>,
  249. default: null,
  250. required: false,
  251. },
  252. })
  253. const emit = defineEmits<{
  254. (event: "select", payload: Picked | null): void
  255. (event: "update-team", team: SelectedTeam): void
  256. (event: "update-collection-type", type: CollectionType["type"]): void
  257. }>()
  258. type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
  259. type CollectionType =
  260. | {
  261. type: "team-collections"
  262. selectedTeam: SelectedTeam
  263. }
  264. | { type: "my-collections"; selectedTeam: undefined }
  265. const collectionsType = ref<CollectionType>({
  266. type: "my-collections",
  267. selectedTeam: undefined,
  268. })
  269. // Collection Data
  270. const editingCollection = ref<
  271. HoppCollection<HoppRESTRequest> | TeamCollection | null
  272. >(null)
  273. const editingCollectionName = ref<string | null>(null)
  274. const editingCollectionIndex = ref<number | null>(null)
  275. const editingCollectionID = ref<string | null>(null)
  276. const editingFolder = ref<
  277. HoppCollection<HoppRESTRequest> | TeamCollection | null
  278. >(null)
  279. const editingFolderName = ref<string | null>(null)
  280. const editingFolderPath = ref<string | null>(null)
  281. const editingRequest = ref<HoppRESTRequest | null>(null)
  282. const editingRequestName = ref("")
  283. const editingRequestIndex = ref<number | null>(null)
  284. const editingRequestID = ref<string | null>(null)
  285. const editingProperties = ref<{
  286. collection: HoppCollection<HoppRESTRequest> | TeamCollection | null
  287. isRootCollection: boolean
  288. path: string
  289. inheritedProperties?: HoppInheritedProperty
  290. }>({
  291. collection: null,
  292. isRootCollection: false,
  293. path: "",
  294. inheritedProperties: undefined,
  295. })
  296. const confirmModalTitle = ref<string | null>(null)
  297. const filterTexts = ref("")
  298. const currentUser = useReadonlyStream(
  299. platform.auth.getCurrentUserStream(),
  300. platform.auth.getCurrentUser()
  301. )
  302. const myCollections = useReadonlyStream(restCollections$, [], "deep")
  303. // Draging
  304. const draggingToRoot = ref(false)
  305. const collectionMoveLoading = ref<string[]>([])
  306. const requestMoveLoading = ref<string[]>([])
  307. // TeamList-Adapter
  308. const workspaceService = useService(WorkspaceService)
  309. const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
  310. const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
  311. const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
  312. const teamListFetched = ref(false)
  313. // Team Collection Adapter
  314. const teamCollectionAdapter = new TeamCollectionAdapter(null)
  315. const teamCollectionList = useReadonlyStream(
  316. teamCollectionAdapter.collections$,
  317. []
  318. )
  319. const teamLoadingCollections = useReadonlyStream(
  320. teamCollectionAdapter.loadingCollections$,
  321. []
  322. )
  323. watch(
  324. () => myTeams.value,
  325. (newTeams) => {
  326. if (newTeams && !teamListFetched.value) {
  327. teamListFetched.value = true
  328. if (REMEMBERED_TEAM_ID.value && currentUser.value) {
  329. const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
  330. if (team) updateSelectedTeam(team)
  331. }
  332. }
  333. }
  334. )
  335. watch(
  336. () => collectionsType.value.selectedTeam,
  337. (newTeam) => {
  338. if (newTeam) {
  339. teamCollectionAdapter.changeTeamID(newTeam.id)
  340. }
  341. }
  342. )
  343. const switchToMyCollections = () => {
  344. collectionsType.value.type = "my-collections"
  345. collectionsType.value.selectedTeam = undefined
  346. teamCollectionAdapter.changeTeamID(null)
  347. }
  348. const expandTeamCollection = (collectionID: string) => {
  349. teamCollectionAdapter.expandCollection(collectionID)
  350. }
  351. const updateSelectedTeam = (team: SelectedTeam) => {
  352. if (team) {
  353. collectionsType.value.type = "team-collections"
  354. collectionsType.value.selectedTeam = team
  355. REMEMBERED_TEAM_ID.value = team.id
  356. emit("update-team", team)
  357. emit("update-collection-type", "team-collections")
  358. }
  359. }
  360. const workspace = workspaceService.currentWorkspace
  361. // Used to switch collection type and team when user switch workspace in the global workspace switcher
  362. // Check if there is a teamID in the workspace, if yes, switch to team collections and select the team
  363. // If there is no teamID, switch to my collections
  364. watch(
  365. () => {
  366. const space = workspace.value
  367. return space.type === "personal" ? undefined : space.teamID
  368. },
  369. (teamID) => {
  370. if (teamID) {
  371. const team = myTeams.value?.find((t) => t.id === teamID)
  372. if (team) {
  373. updateSelectedTeam(team)
  374. }
  375. return
  376. }
  377. return switchToMyCollections()
  378. },
  379. {
  380. immediate: true,
  381. }
  382. )
  383. // Switch to my-collections and reset the team collection when user logout
  384. watch(
  385. () => currentUser.value,
  386. (user) => {
  387. if (!user) {
  388. switchToMyCollections()
  389. }
  390. }
  391. )
  392. const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
  393. type: "collection",
  394. id: "",
  395. parentID: "",
  396. })
  397. const hasTeamWriteAccess = computed(() => {
  398. if (collectionsType.value.type !== "team-collections") {
  399. return false
  400. }
  401. const role = collectionsType.value.selectedTeam?.myRole
  402. return role === "OWNER" || role === "EDITOR"
  403. })
  404. const filteredCollections = computed(() => {
  405. const collections =
  406. collectionsType.value.type === "my-collections" ? myCollections.value : []
  407. if (filterTexts.value === "") return collections
  408. if (collectionsType.value.type === "team-collections") return []
  409. const filterText = filterTexts.value.toLowerCase()
  410. const filteredCollections = []
  411. const isMatch = (text: string) => text.toLowerCase().includes(filterText)
  412. for (const collection of collections) {
  413. const filteredRequests = []
  414. const filteredFolders = []
  415. for (const request of collection.requests) {
  416. if (isMatch(request.name)) filteredRequests.push(request)
  417. }
  418. for (const folder of collection.folders) {
  419. if (isMatch(folder.name)) filteredFolders.push(folder)
  420. const filteredFolderRequests = []
  421. for (const request of folder.requests) {
  422. if (isMatch(request.name)) filteredFolderRequests.push(request)
  423. }
  424. if (filteredFolderRequests.length > 0) {
  425. const filteredFolder = Object.assign({}, folder)
  426. filteredFolder.requests = filteredFolderRequests
  427. filteredFolders.push(filteredFolder)
  428. }
  429. }
  430. if (
  431. filteredRequests.length + filteredFolders.length > 0 ||
  432. isMatch(collection.name)
  433. ) {
  434. const filteredCollection = Object.assign({}, collection)
  435. filteredCollection.requests = filteredRequests
  436. filteredCollection.folders = filteredFolders
  437. filteredCollections.push(filteredCollection)
  438. }
  439. }
  440. return filteredCollections
  441. })
  442. const isSelected = ({
  443. collectionIndex,
  444. folderPath,
  445. requestIndex,
  446. collectionID,
  447. folderID,
  448. requestID,
  449. }: {
  450. collectionIndex?: number | undefined
  451. folderPath?: string | undefined
  452. requestIndex?: number | undefined
  453. collectionID?: string | undefined
  454. folderID?: string | undefined
  455. requestID?: string | undefined
  456. }) => {
  457. if (collectionIndex !== undefined) {
  458. return (
  459. props.picked &&
  460. props.picked.pickedType === "my-collection" &&
  461. props.picked.collectionIndex === collectionIndex
  462. )
  463. } else if (requestIndex !== undefined && folderPath !== undefined) {
  464. return (
  465. props.picked &&
  466. props.picked.pickedType === "my-request" &&
  467. props.picked.folderPath === folderPath &&
  468. props.picked.requestIndex === requestIndex
  469. )
  470. } else if (folderPath !== undefined) {
  471. return (
  472. props.picked &&
  473. props.picked.pickedType === "my-folder" &&
  474. props.picked.folderPath === folderPath
  475. )
  476. } else if (collectionID !== undefined) {
  477. return (
  478. props.picked &&
  479. props.picked.pickedType === "teams-collection" &&
  480. props.picked.collectionID === collectionID
  481. )
  482. } else if (requestID !== undefined) {
  483. return (
  484. props.picked &&
  485. props.picked.pickedType === "teams-request" &&
  486. props.picked.requestID === requestID
  487. )
  488. } else if (folderID !== undefined) {
  489. return (
  490. props.picked &&
  491. props.picked.pickedType === "teams-folder" &&
  492. props.picked.folderID === folderID
  493. )
  494. }
  495. }
  496. const modalLoadingState = ref(false)
  497. const exportLoading = ref(false)
  498. const duplicateLoading = ref(false)
  499. const showModalAdd = ref(false)
  500. const showModalAddRequest = ref(false)
  501. const showModalAddFolder = ref(false)
  502. const showModalEditCollection = ref(false)
  503. const showModalEditFolder = ref(false)
  504. const showModalEditRequest = ref(false)
  505. const showModalImportExport = ref(false)
  506. const showModalEditProperties = ref(false)
  507. const showConfirmModal = ref(false)
  508. const showTeamModalAdd = ref(false)
  509. const displayModalAdd = (show: boolean) => {
  510. showModalAdd.value = show
  511. if (!show) resetSelectedData()
  512. }
  513. const displayModalAddRequest = (show: boolean) => {
  514. showModalAddRequest.value = show
  515. if (!show) resetSelectedData()
  516. }
  517. const displayModalAddFolder = (show: boolean) => {
  518. showModalAddFolder.value = show
  519. if (!show) resetSelectedData()
  520. }
  521. const displayModalEditCollection = (show: boolean) => {
  522. showModalEditCollection.value = show
  523. if (!show) resetSelectedData()
  524. }
  525. const displayModalEditFolder = (show: boolean) => {
  526. showModalEditFolder.value = show
  527. if (!show) resetSelectedData()
  528. }
  529. const displayModalEditRequest = (show: boolean) => {
  530. showModalEditRequest.value = show
  531. if (!show) resetSelectedData()
  532. }
  533. const displayModalImportExport = (show: boolean) => {
  534. showModalImportExport.value = show
  535. if (!show) resetSelectedData()
  536. }
  537. const displayModalEditProperties = (show: boolean) => {
  538. showModalEditProperties.value = show
  539. if (!show) resetSelectedData()
  540. }
  541. const displayConfirmModal = (show: boolean) => {
  542. showConfirmModal.value = show
  543. if (!show) resetSelectedData()
  544. }
  545. const displayTeamModalAdd = (show: boolean) => {
  546. showTeamModalAdd.value = show
  547. teamListAdapter.fetchList()
  548. }
  549. const addNewRootCollection = (name: string) => {
  550. if (collectionsType.value.type === "my-collections") {
  551. addRESTCollection(
  552. makeCollection({
  553. name,
  554. folders: [],
  555. requests: [],
  556. headers: [],
  557. auth: {
  558. authType: "inherit",
  559. authActive: false,
  560. },
  561. })
  562. )
  563. platform.analytics?.logEvent({
  564. type: "HOPP_CREATE_COLLECTION",
  565. platform: "rest",
  566. workspaceType: "personal",
  567. isRootCollection: true,
  568. })
  569. displayModalAdd(false)
  570. } else if (hasTeamWriteAccess.value) {
  571. if (!collectionsType.value.selectedTeam) return
  572. modalLoadingState.value = true
  573. platform.analytics?.logEvent({
  574. type: "HOPP_CREATE_COLLECTION",
  575. platform: "rest",
  576. workspaceType: "team",
  577. isRootCollection: true,
  578. })
  579. pipe(
  580. createNewRootCollection(name, collectionsType.value.selectedTeam.id),
  581. TE.match(
  582. (err: GQLError<string>) => {
  583. toast.error(`${getErrorMessage(err)}`)
  584. modalLoadingState.value = false
  585. },
  586. () => {
  587. modalLoadingState.value = false
  588. toast.success(t("collection.created"))
  589. displayModalAdd(false)
  590. }
  591. )
  592. )()
  593. }
  594. }
  595. const addRequest = (payload: {
  596. path: string
  597. folder: HoppCollection<HoppRESTRequest> | TeamCollection
  598. }) => {
  599. const { path, folder } = payload
  600. editingFolder.value = folder
  601. editingFolderPath.value = path
  602. displayModalAddRequest(true)
  603. }
  604. const onAddRequest = (requestName: string) => {
  605. const newRequest = {
  606. ...cloneDeep(tabs.currentActiveTab.value.document.request),
  607. name: requestName,
  608. }
  609. if (collectionsType.value.type === "my-collections") {
  610. const path = editingFolderPath.value
  611. if (!path) return
  612. const insertionIndex = saveRESTRequestAs(path, newRequest)
  613. const { auth, headers } = cascaseParentCollectionForHeaderAuth(path)
  614. tabs.createNewTab({
  615. request: newRequest,
  616. isDirty: false,
  617. saveContext: {
  618. originLocation: "user-collection",
  619. folderPath: path,
  620. requestIndex: insertionIndex,
  621. },
  622. inheritedProperties: {
  623. auth,
  624. headers,
  625. },
  626. })
  627. platform.analytics?.logEvent({
  628. type: "HOPP_SAVE_REQUEST",
  629. workspaceType: "personal",
  630. createdNow: true,
  631. platform: "rest",
  632. })
  633. displayModalAddRequest(false)
  634. } else if (hasTeamWriteAccess.value) {
  635. const folder = editingFolder.value
  636. if (!folder || !collectionsType.value.selectedTeam) return
  637. if (!folder.id) return
  638. modalLoadingState.value = true
  639. const data = {
  640. request: JSON.stringify(newRequest),
  641. teamID: collectionsType.value.selectedTeam.id,
  642. title: requestName,
  643. }
  644. platform.analytics?.logEvent({
  645. type: "HOPP_SAVE_REQUEST",
  646. workspaceType: "team",
  647. platform: "rest",
  648. createdNow: true,
  649. })
  650. pipe(
  651. createRequestInCollection(folder.id, data),
  652. TE.match(
  653. (err: GQLError<string>) => {
  654. toast.error(`${getErrorMessage(err)}`)
  655. modalLoadingState.value = false
  656. },
  657. (result) => {
  658. const { createRequestInCollection } = result
  659. tabs.createNewTab({
  660. request: newRequest,
  661. isDirty: false,
  662. saveContext: {
  663. originLocation: "team-collection",
  664. requestID: createRequestInCollection.id,
  665. collectionID: createRequestInCollection.collection.id,
  666. teamID: createRequestInCollection.collection.team.id,
  667. },
  668. })
  669. modalLoadingState.value = false
  670. displayModalAddRequest(false)
  671. }
  672. )
  673. )()
  674. }
  675. }
  676. const addFolder = (payload: {
  677. path: string
  678. folder: HoppCollection<HoppRESTRequest> | TeamCollection
  679. }) => {
  680. const { path, folder } = payload
  681. editingFolder.value = folder
  682. editingFolderPath.value = path
  683. displayModalAddFolder(true)
  684. }
  685. const onAddFolder = (folderName: string) => {
  686. const path = editingFolderPath.value
  687. if (collectionsType.value.type === "my-collections") {
  688. if (!path) return
  689. addRESTFolder(folderName, path)
  690. platform.analytics?.logEvent({
  691. type: "HOPP_CREATE_COLLECTION",
  692. workspaceType: "personal",
  693. isRootCollection: false,
  694. platform: "rest",
  695. })
  696. displayModalAddFolder(false)
  697. } else if (hasTeamWriteAccess.value) {
  698. const folder = editingFolder.value
  699. if (!folder || !folder.id) return
  700. modalLoadingState.value = true
  701. platform.analytics?.logEvent({
  702. type: "HOPP_CREATE_COLLECTION",
  703. workspaceType: "personal",
  704. isRootCollection: false,
  705. platform: "rest",
  706. })
  707. pipe(
  708. createChildCollection(folderName, folder.id),
  709. TE.match(
  710. (err: GQLError<string>) => {
  711. if (err.error === "team_coll/short_title") {
  712. toast.error(t("folder.name_length_insufficient"))
  713. } else {
  714. toast.error(`${getErrorMessage(err)}`)
  715. }
  716. modalLoadingState.value = false
  717. },
  718. () => {
  719. toast.success(t("folder.created"))
  720. modalLoadingState.value = false
  721. displayModalAddFolder(false)
  722. }
  723. )
  724. )()
  725. }
  726. }
  727. const editCollection = (payload: {
  728. collectionIndex: string
  729. collection: HoppCollection<HoppRESTRequest> | TeamCollection
  730. }) => {
  731. const { collectionIndex, collection } = payload
  732. editingCollection.value = collection
  733. if (collectionsType.value.type === "my-collections") {
  734. editingCollectionIndex.value = parseInt(collectionIndex)
  735. editingCollectionName.value = (
  736. collection as HoppCollection<HoppRESTRequest>
  737. ).name
  738. } else {
  739. editingCollectionName.value = (collection as TeamCollection).title
  740. }
  741. displayModalEditCollection(true)
  742. }
  743. const updateEditingCollection = (newName: string) => {
  744. if (!editingCollection.value) return
  745. if (!newName) {
  746. toast.error(t("collection.invalid_name"))
  747. return
  748. }
  749. if (collectionsType.value.type === "my-collections") {
  750. const collectionIndex = editingCollectionIndex.value
  751. if (collectionIndex === null) return
  752. const collectionUpdated = {
  753. ...editingCollection.value,
  754. name: newName,
  755. }
  756. editRESTCollection(
  757. collectionIndex,
  758. collectionUpdated as NodeCollection["data"]["data"]
  759. )
  760. displayModalEditCollection(false)
  761. } else if (hasTeamWriteAccess.value) {
  762. if (!editingCollection.value.id) return
  763. modalLoadingState.value = true
  764. pipe(
  765. renameCollection(editingCollection.value.id, newName),
  766. TE.match(
  767. (err: GQLError<string>) => {
  768. toast.error(`${getErrorMessage(err)}`)
  769. modalLoadingState.value = false
  770. },
  771. () => {
  772. modalLoadingState.value = false
  773. toast.success(t("collection.renamed"))
  774. displayModalEditCollection(false)
  775. }
  776. )
  777. )()
  778. }
  779. }
  780. const editFolder = (payload: {
  781. folderPath: string | undefined
  782. folder: HoppCollection<HoppRESTRequest> | TeamCollection
  783. }) => {
  784. const { folderPath, folder } = payload
  785. editingFolder.value = folder
  786. if (collectionsType.value.type === "my-collections" && folderPath) {
  787. editingFolderPath.value = folderPath
  788. editingFolderName.value = (folder as HoppCollection<HoppRESTRequest>).name
  789. } else {
  790. editingFolderName.value = (folder as TeamCollection).title
  791. }
  792. displayModalEditFolder(true)
  793. }
  794. const updateEditingFolder = (newName: string) => {
  795. if (!editingFolder.value) return
  796. if (collectionsType.value.type === "my-collections") {
  797. if (!editingFolderPath.value) return
  798. editRESTFolder(editingFolderPath.value, {
  799. ...(editingFolder.value as HoppCollection<HoppRESTRequest>),
  800. name: newName,
  801. })
  802. displayModalEditFolder(false)
  803. } else if (hasTeamWriteAccess.value) {
  804. if (!editingFolder.value.id) return
  805. modalLoadingState.value = true
  806. /* renameCollection can be used to rename both collections and folders
  807. since folder is treated as collection in the BE. */
  808. pipe(
  809. renameCollection(editingFolder.value.id, newName),
  810. TE.match(
  811. (err: GQLError<string>) => {
  812. if (err.error === "team_coll/short_title") {
  813. toast.error(t("folder.name_length_insufficient"))
  814. } else {
  815. toast.error(`${getErrorMessage(err)}`)
  816. }
  817. modalLoadingState.value = false
  818. },
  819. () => {
  820. modalLoadingState.value = false
  821. toast.success(t("folder.renamed"))
  822. displayModalEditFolder(false)
  823. }
  824. )
  825. )()
  826. }
  827. }
  828. const editRequest = (payload: {
  829. folderPath: string | undefined
  830. requestIndex: string
  831. request: HoppRESTRequest
  832. }) => {
  833. const { folderPath, requestIndex, request } = payload
  834. editingRequest.value = request
  835. editingRequestName.value = request.name ?? ""
  836. if (collectionsType.value.type === "my-collections" && folderPath) {
  837. editingFolderPath.value = folderPath
  838. editingRequestIndex.value = parseInt(requestIndex)
  839. } else {
  840. editingRequestID.value = requestIndex
  841. }
  842. displayModalEditRequest(true)
  843. }
  844. const updateEditingRequest = (newName: string) => {
  845. const request = editingRequest.value
  846. if (!request) return
  847. const requestUpdated = {
  848. ...request,
  849. name: newName || request.name,
  850. }
  851. if (collectionsType.value.type === "my-collections") {
  852. const folderPath = editingFolderPath.value
  853. const requestIndex = editingRequestIndex.value
  854. if (folderPath === null || requestIndex === null) return
  855. const possibleActiveTab = tabs.getTabRefWithSaveContext({
  856. originLocation: "user-collection",
  857. requestIndex,
  858. folderPath,
  859. })
  860. editRESTRequest(folderPath, requestIndex, requestUpdated)
  861. if (possibleActiveTab) {
  862. possibleActiveTab.value.document.request.name = requestUpdated.name
  863. nextTick(() => {
  864. possibleActiveTab.value.document.isDirty = false
  865. })
  866. }
  867. displayModalEditRequest(false)
  868. } else if (hasTeamWriteAccess.value) {
  869. modalLoadingState.value = true
  870. const requestID = editingRequestID.value
  871. const requestName = newName || request.name
  872. if (!requestID) return
  873. const data = {
  874. request: JSON.stringify(requestUpdated),
  875. title: requestName,
  876. }
  877. pipe(
  878. updateTeamRequest(requestID, data),
  879. TE.match(
  880. (err: GQLError<string>) => {
  881. toast.error(`${getErrorMessage(err)}`)
  882. modalLoadingState.value = false
  883. },
  884. () => {
  885. modalLoadingState.value = false
  886. toast.success(t("request.renamed"))
  887. displayModalEditRequest(false)
  888. }
  889. )
  890. )()
  891. const possibleTab = tabs.getTabRefWithSaveContext({
  892. originLocation: "team-collection",
  893. requestID,
  894. })
  895. if (possibleTab) {
  896. possibleTab.value.document.request.name = requestName
  897. nextTick(() => {
  898. possibleTab.value.document.isDirty = false
  899. })
  900. }
  901. }
  902. }
  903. const duplicateRequest = (payload: {
  904. folderPath: string
  905. request: HoppRESTRequest
  906. }) => {
  907. const { folderPath, request } = payload
  908. if (!folderPath) return
  909. const newRequest = {
  910. ...cloneDeep(request),
  911. name: `${request.name} - ${t("action.duplicate")}`,
  912. }
  913. if (collectionsType.value.type === "my-collections") {
  914. saveRESTRequestAs(folderPath, newRequest)
  915. toast.success(t("request.duplicated"))
  916. } else if (hasTeamWriteAccess.value) {
  917. duplicateLoading.value = true
  918. if (!collectionsType.value.selectedTeam) return
  919. const data = {
  920. request: JSON.stringify(newRequest),
  921. teamID: collectionsType.value.selectedTeam.id,
  922. title: `${request.name} - ${t("action.duplicate")}`,
  923. }
  924. pipe(
  925. createRequestInCollection(folderPath, data),
  926. TE.match(
  927. (err: GQLError<string>) => {
  928. toast.error(`${getErrorMessage(err)}`)
  929. duplicateLoading.value = false
  930. },
  931. () => {
  932. duplicateLoading.value = false
  933. toast.success(t("request.duplicated"))
  934. displayModalAddRequest(false)
  935. }
  936. )
  937. )()
  938. }
  939. }
  940. const removeCollection = (id: string) => {
  941. if (collectionsType.value.type === "my-collections")
  942. editingCollectionIndex.value = parseInt(id)
  943. else editingCollectionID.value = id
  944. confirmModalTitle.value = `${t("confirm.remove_collection")}`
  945. displayConfirmModal(true)
  946. }
  947. /**
  948. * Used to delete both collections and folders
  949. * since folder is treated as collection in the BE.
  950. * @param collectionID - ID of the collection or folder to be deleted.
  951. */
  952. const removeTeamCollectionOrFolder = async (collectionID: string) => {
  953. modalLoadingState.value = true
  954. await pipe(
  955. deleteCollection(collectionID),
  956. TE.match(
  957. (err: GQLError<string>) => {
  958. toast.error(`${getErrorMessage(err)}`)
  959. modalLoadingState.value = false
  960. },
  961. () => {
  962. modalLoadingState.value = false
  963. toast.success(t("state.deleted"))
  964. displayConfirmModal(false)
  965. }
  966. )
  967. )()
  968. }
  969. const onRemoveCollection = () => {
  970. if (collectionsType.value.type === "my-collections") {
  971. const collectionIndex = editingCollectionIndex.value
  972. const collectionToRemove =
  973. collectionIndex || collectionIndex === 0
  974. ? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
  975. collectionIndex,
  976. ])
  977. : undefined
  978. if (collectionIndex === null) return
  979. if (
  980. isSelected({
  981. collectionIndex,
  982. })
  983. ) {
  984. emit("select", null)
  985. }
  986. removeRESTCollection(
  987. collectionIndex,
  988. collectionToRemove ? collectionToRemove.id : undefined
  989. )
  990. resolveSaveContextOnCollectionReorder({
  991. lastIndex: collectionIndex,
  992. newIndex: -1,
  993. folderPath: "", // root folder
  994. length: myCollections.value.length,
  995. })
  996. toast.success(t("state.deleted"))
  997. displayConfirmModal(false)
  998. } else if (hasTeamWriteAccess.value) {
  999. const collectionID = editingCollectionID.value
  1000. if (!collectionID) return
  1001. if (
  1002. isSelected({
  1003. collectionID,
  1004. })
  1005. ) {
  1006. emit("select", null)
  1007. }
  1008. removeTeamCollectionOrFolder(collectionID).then(() => {
  1009. resetTeamRequestsContext()
  1010. })
  1011. }
  1012. }
  1013. const removeFolder = (id: string) => {
  1014. if (collectionsType.value.type === "my-collections")
  1015. editingFolderPath.value = id
  1016. else editingCollectionID.value = id
  1017. confirmModalTitle.value = `${t("confirm.remove_folder")}`
  1018. displayConfirmModal(true)
  1019. }
  1020. const onRemoveFolder = () => {
  1021. if (collectionsType.value.type === "my-collections") {
  1022. const folderPath = editingFolderPath.value
  1023. if (!folderPath) return
  1024. if (
  1025. isSelected({
  1026. folderPath,
  1027. })
  1028. ) {
  1029. emit("select", null)
  1030. }
  1031. const folderToRemove = folderPath
  1032. ? navigateToFolderWithIndexPath(
  1033. restCollectionStore.value.state,
  1034. folderPath.split("/").map((i) => parseInt(i))
  1035. )
  1036. : undefined
  1037. removeRESTFolder(folderPath, folderToRemove ? folderToRemove.id : undefined)
  1038. const parentFolder = folderPath.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
  1039. resolveSaveContextOnCollectionReorder({
  1040. lastIndex: pathToLastIndex(folderPath),
  1041. newIndex: -1,
  1042. folderPath: parentFolder,
  1043. length: getFoldersByPath(myCollections.value, parentFolder).length,
  1044. })
  1045. toast.success(t("state.deleted"))
  1046. displayConfirmModal(false)
  1047. } else if (hasTeamWriteAccess.value) {
  1048. const collectionID = editingCollectionID.value
  1049. if (!collectionID) return
  1050. if (
  1051. isSelected({
  1052. collectionID,
  1053. })
  1054. ) {
  1055. emit("select", null)
  1056. }
  1057. removeTeamCollectionOrFolder(collectionID).then(() => {
  1058. resetTeamRequestsContext()
  1059. })
  1060. }
  1061. }
  1062. const removeRequest = (payload: {
  1063. folderPath: string | null
  1064. requestIndex: string
  1065. }) => {
  1066. const { folderPath, requestIndex } = payload
  1067. if (collectionsType.value.type === "my-collections" && folderPath) {
  1068. editingFolderPath.value = folderPath
  1069. editingRequestIndex.value = parseInt(requestIndex)
  1070. } else {
  1071. editingRequestID.value = requestIndex
  1072. }
  1073. confirmModalTitle.value = `${t("confirm.remove_request")}`
  1074. displayConfirmModal(true)
  1075. }
  1076. const onRemoveRequest = () => {
  1077. if (collectionsType.value.type === "my-collections") {
  1078. const folderPath = editingFolderPath.value
  1079. const requestIndex = editingRequestIndex.value
  1080. if (folderPath === null || requestIndex === null) return
  1081. if (
  1082. isSelected({
  1083. folderPath,
  1084. requestIndex,
  1085. })
  1086. ) {
  1087. emit("select", null)
  1088. }
  1089. const possibleTab = tabs.getTabRefWithSaveContext({
  1090. originLocation: "user-collection",
  1091. folderPath,
  1092. requestIndex,
  1093. })
  1094. // If there is a tab attached to this request, dissociate its state and mark it dirty
  1095. if (possibleTab) {
  1096. possibleTab.value.document.saveContext = null
  1097. possibleTab.value.document.isDirty = true
  1098. }
  1099. const requestToRemove = navigateToFolderWithIndexPath(
  1100. restCollectionStore.value.state,
  1101. folderPath.split("/").map((i) => parseInt(i))
  1102. )?.requests[requestIndex]
  1103. removeRESTRequest(folderPath, requestIndex, requestToRemove?.id)
  1104. // the same function is used to reorder requests since after removing, it's basically doing reorder
  1105. resolveSaveContextOnRequestReorder({
  1106. lastIndex: requestIndex,
  1107. newIndex: -1,
  1108. folderPath,
  1109. length: getRequestsByPath(myCollections.value, folderPath).length,
  1110. })
  1111. toast.success(t("state.deleted"))
  1112. displayConfirmModal(false)
  1113. } else if (hasTeamWriteAccess.value) {
  1114. const requestID = editingRequestID.value
  1115. if (!requestID) return
  1116. if (
  1117. isSelected({
  1118. requestID,
  1119. })
  1120. ) {
  1121. emit("select", null)
  1122. }
  1123. modalLoadingState.value = true
  1124. pipe(
  1125. deleteTeamRequest(requestID),
  1126. TE.match(
  1127. (err: GQLError<string>) => {
  1128. toast.error(`${getErrorMessage(err)}`)
  1129. modalLoadingState.value = false
  1130. },
  1131. () => {
  1132. modalLoadingState.value = false
  1133. toast.success(t("state.deleted"))
  1134. displayConfirmModal(false)
  1135. }
  1136. )
  1137. )()
  1138. // If there is a tab attached to this request, dissociate its state and mark it dirty
  1139. const possibleTab = tabs.getTabRefWithSaveContext({
  1140. originLocation: "team-collection",
  1141. requestID,
  1142. })
  1143. if (possibleTab) {
  1144. possibleTab.value.document.saveContext = null
  1145. possibleTab.value.document.isDirty = true
  1146. }
  1147. }
  1148. }
  1149. // The request is picked in the save request as modal
  1150. const selectPicked = (payload: Picked | null) => {
  1151. emit("select", payload)
  1152. }
  1153. /**
  1154. * This function is called when the user clicks on a request
  1155. * @param selectedRequest The request that the user clicked on emited from the collection tree
  1156. */
  1157. const selectRequest = (selectedRequest: {
  1158. request: HoppRESTRequest
  1159. folderPath: string | undefined
  1160. requestIndex: string
  1161. isActive: boolean
  1162. }) => {
  1163. const { request, folderPath, requestIndex } = selectedRequest
  1164. // If there is a request with this save context, switch into it
  1165. let possibleTab = null
  1166. const { auth, headers } = cascaseParentCollectionForHeaderAuth(folderPath)
  1167. if (collectionsType.value.type === "team-collections") {
  1168. possibleTab = tabs.getTabRefWithSaveContext({
  1169. originLocation: "team-collection",
  1170. requestID: requestIndex,
  1171. })
  1172. if (possibleTab) {
  1173. tabs.setActiveTab(possibleTab.value.id)
  1174. } else {
  1175. tabs.createNewTab({
  1176. request: cloneDeep(request),
  1177. isDirty: false,
  1178. saveContext: {
  1179. originLocation: "team-collection",
  1180. requestID: requestIndex,
  1181. },
  1182. })
  1183. }
  1184. } else {
  1185. possibleTab = tabs.getTabRefWithSaveContext({
  1186. originLocation: "user-collection",
  1187. requestIndex: parseInt(requestIndex),
  1188. folderPath: folderPath!,
  1189. })
  1190. if (possibleTab) {
  1191. tabs.setActiveTab(possibleTab.value.id)
  1192. } else {
  1193. // If not, open the request in a new tab
  1194. tabs.createNewTab({
  1195. request: cloneDeep(request),
  1196. isDirty: false,
  1197. saveContext: {
  1198. originLocation: "user-collection",
  1199. folderPath: folderPath!,
  1200. requestIndex: parseInt(requestIndex),
  1201. },
  1202. inheritedProperties: {
  1203. auth,
  1204. headers,
  1205. },
  1206. })
  1207. }
  1208. }
  1209. }
  1210. /**
  1211. * Used to get the index of the request from the path
  1212. * @param path The path of the request
  1213. * @returns The index of the request
  1214. */
  1215. const pathToLastIndex = (path: string) => {
  1216. const pathArr = path.split("/")
  1217. return parseInt(pathArr[pathArr.length - 1])
  1218. }
  1219. /**
  1220. * This function is called when the user drops the request inside a collection
  1221. * @param payload Object that contains the folder path, request index and the destination collection index
  1222. */
  1223. const dropRequest = (payload: {
  1224. folderPath?: string | undefined
  1225. requestIndex: string
  1226. destinationCollectionIndex: string
  1227. }) => {
  1228. const { folderPath, requestIndex, destinationCollectionIndex } = payload
  1229. if (!requestIndex || !destinationCollectionIndex) return
  1230. let possibleTab = null
  1231. if (collectionsType.value.type === "my-collections" && folderPath) {
  1232. const { auth, headers } = cascaseParentCollectionForHeaderAuth(
  1233. destinationCollectionIndex
  1234. )
  1235. possibleTab = tabs.getTabRefWithSaveContext({
  1236. originLocation: "user-collection",
  1237. folderPath,
  1238. requestIndex: pathToLastIndex(requestIndex),
  1239. })
  1240. // If there is a tab attached to this request, change save its save context
  1241. if (possibleTab) {
  1242. possibleTab.value.document.saveContext = {
  1243. originLocation: "user-collection",
  1244. folderPath: destinationCollectionIndex,
  1245. requestIndex: getRequestsByPath(
  1246. myCollections.value,
  1247. destinationCollectionIndex
  1248. ).length,
  1249. }
  1250. possibleTab.value.document.inheritedProperties = {
  1251. auth,
  1252. headers,
  1253. }
  1254. }
  1255. // When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
  1256. resolveSaveContextOnRequestReorder({
  1257. lastIndex: pathToLastIndex(requestIndex),
  1258. newIndex: -1, // being deleted from last folder
  1259. folderPath,
  1260. length: getRequestsByPath(myCollections.value, folderPath).length,
  1261. })
  1262. moveRESTRequest(
  1263. folderPath,
  1264. pathToLastIndex(requestIndex),
  1265. destinationCollectionIndex
  1266. )
  1267. toast.success(`${t("request.moved")}`)
  1268. draggingToRoot.value = false
  1269. } else if (hasTeamWriteAccess.value) {
  1270. // add the request index to the loading array
  1271. requestMoveLoading.value.push(requestIndex)
  1272. pipe(
  1273. moveRESTTeamRequest(destinationCollectionIndex, requestIndex),
  1274. TE.match(
  1275. (err: GQLError<string>) => {
  1276. toast.error(`${getErrorMessage(err)}`)
  1277. requestMoveLoading.value.splice(
  1278. requestMoveLoading.value.indexOf(requestIndex),
  1279. 1
  1280. )
  1281. },
  1282. () => {
  1283. // remove the request index from the loading array
  1284. requestMoveLoading.value.splice(
  1285. requestMoveLoading.value.indexOf(requestIndex),
  1286. 1
  1287. )
  1288. possibleTab = tabs.getTabRefWithSaveContext({
  1289. originLocation: "team-collection",
  1290. requestID: requestIndex,
  1291. })
  1292. if (possibleTab) {
  1293. possibleTab.value.document.saveContext = {
  1294. originLocation: "team-collection",
  1295. requestID: requestIndex,
  1296. }
  1297. }
  1298. toast.success(`${t("request.moved")}`)
  1299. }
  1300. )
  1301. )()
  1302. }
  1303. }
  1304. /**
  1305. * @param path The path of the collection or request
  1306. * @returns The index of the collection or request
  1307. */
  1308. const pathToIndex = (path: string) => {
  1309. const pathArr = path.split("/")
  1310. return pathArr
  1311. }
  1312. /**
  1313. * Used to check if the collection exist as the parent of the childrens
  1314. * @param collectionIndexDragged The index of the collection dragged
  1315. * @param destinationCollectionIndex The index of the destination collection
  1316. * @returns True if the collection exist as the parent of the childrens
  1317. */
  1318. const checkIfCollectionIsAParentOfTheChildren = (
  1319. collectionIndexDragged: string,
  1320. destinationCollectionIndex: string
  1321. ) => {
  1322. const collectionDraggedPath = pathToIndex(collectionIndexDragged)
  1323. const destinationCollectionPath = pathToIndex(destinationCollectionIndex)
  1324. if (collectionDraggedPath.length < destinationCollectionPath.length) {
  1325. const slicedDestinationCollectionPath = destinationCollectionPath.slice(
  1326. 0,
  1327. collectionDraggedPath.length
  1328. )
  1329. if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
  1330. return true
  1331. }
  1332. return false
  1333. }
  1334. return false
  1335. }
  1336. const isMoveToSameLocation = (
  1337. draggedItemPath: string,
  1338. destinationPath: string
  1339. ) => {
  1340. const draggedItemPathArr = pathToIndex(draggedItemPath)
  1341. const destinationPathArr = pathToIndex(destinationPath)
  1342. if (draggedItemPathArr.length > 0) {
  1343. const draggedItemParentPathArr = draggedItemPathArr.slice(
  1344. 0,
  1345. draggedItemPathArr.length - 1
  1346. )
  1347. if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
  1348. return true
  1349. }
  1350. return false
  1351. }
  1352. }
  1353. /**
  1354. * This function is called when the user moves the collection
  1355. * to a different collection or folder
  1356. * @param payload - object containing the collection index dragged and the destination collection index
  1357. */
  1358. const dropCollection = (payload: {
  1359. collectionIndexDragged: string
  1360. destinationCollectionIndex: string
  1361. }) => {
  1362. const { collectionIndexDragged, destinationCollectionIndex } = payload
  1363. if (!collectionIndexDragged || !destinationCollectionIndex) return
  1364. if (collectionIndexDragged === destinationCollectionIndex) return
  1365. if (collectionsType.value.type === "my-collections") {
  1366. if (
  1367. checkIfCollectionIsAParentOfTheChildren(
  1368. collectionIndexDragged,
  1369. destinationCollectionIndex
  1370. )
  1371. ) {
  1372. toast.error(`${t("team.parent_coll_move")}`)
  1373. return
  1374. }
  1375. //check if the collection is being moved to its own parent
  1376. if (
  1377. isMoveToSameLocation(collectionIndexDragged, destinationCollectionIndex)
  1378. ) {
  1379. return
  1380. }
  1381. const parentFolder = collectionIndexDragged
  1382. .split("/")
  1383. .slice(0, -1)
  1384. .join("/") // remove last folder to get parent folder
  1385. const totalFoldersOfDestinationCollection =
  1386. getFoldersByPath(myCollections.value, destinationCollectionIndex).length -
  1387. (parentFolder === destinationCollectionIndex ? 1 : 0)
  1388. moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
  1389. resolveSaveContextOnCollectionReorder(
  1390. {
  1391. lastIndex: pathToLastIndex(collectionIndexDragged),
  1392. newIndex: -1,
  1393. folderPath: parentFolder,
  1394. length: getFoldersByPath(myCollections.value, parentFolder).length,
  1395. },
  1396. "drop"
  1397. )
  1398. updateSaveContextForAffectedRequests(
  1399. collectionIndexDragged,
  1400. `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
  1401. )
  1402. const { auth, headers } = cascaseParentCollectionForHeaderAuth(
  1403. `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
  1404. )
  1405. const inheritedProperty = {
  1406. auth,
  1407. headers,
  1408. }
  1409. updateInheritedPropertiesForAffectedRequests(
  1410. `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
  1411. inheritedProperty
  1412. )
  1413. draggingToRoot.value = false
  1414. toast.success(`${t("collection.moved")}`)
  1415. } else if (hasTeamWriteAccess.value) {
  1416. // add the collection index to the loading array
  1417. collectionMoveLoading.value.push(collectionIndexDragged)
  1418. pipe(
  1419. moveRESTTeamCollection(
  1420. collectionIndexDragged,
  1421. destinationCollectionIndex
  1422. ),
  1423. TE.match(
  1424. (err: GQLError<string>) => {
  1425. toast.error(`${getErrorMessage(err)}`)
  1426. collectionMoveLoading.value.splice(
  1427. collectionMoveLoading.value.indexOf(collectionIndexDragged),
  1428. 1
  1429. )
  1430. },
  1431. () => {
  1432. toast.success(`${t("collection.moved")}`)
  1433. // remove the collection index from the loading array
  1434. collectionMoveLoading.value.splice(
  1435. collectionMoveLoading.value.indexOf(collectionIndexDragged),
  1436. 1
  1437. )
  1438. }
  1439. )
  1440. )()
  1441. }
  1442. }
  1443. /**
  1444. * Checks if the collection is already in the root
  1445. * @param id - path of the collection
  1446. * @returns boolean - true if the collection is already in the root
  1447. */
  1448. const isAlreadyInRoot = (id: string) => {
  1449. const indexPath = pathToIndex(id)
  1450. return indexPath.length === 1
  1451. }
  1452. /**
  1453. * This function is called when the user drops the collection
  1454. * to the root
  1455. * @param payload - object containing the collection index dragged
  1456. */
  1457. const dropToRoot = ({ dataTransfer }: DragEvent) => {
  1458. if (dataTransfer) {
  1459. const collectionIndexDragged = dataTransfer.getData("collectionIndex")
  1460. if (!collectionIndexDragged) return
  1461. if (collectionsType.value.type === "my-collections") {
  1462. // check if the collection is already in the root
  1463. if (isAlreadyInRoot(collectionIndexDragged)) {
  1464. toast.error(`${t("collection.invalid_root_move")}`)
  1465. } else {
  1466. moveRESTFolder(collectionIndexDragged, null)
  1467. toast.success(`${t("collection.moved")}`)
  1468. }
  1469. draggingToRoot.value = false
  1470. } else if (hasTeamWriteAccess.value) {
  1471. // add the collection index to the loading array
  1472. collectionMoveLoading.value.push(collectionIndexDragged)
  1473. // destination collection index is null since we are moving to root
  1474. pipe(
  1475. moveRESTTeamCollection(collectionIndexDragged, null),
  1476. TE.match(
  1477. (err: GQLError<string>) => {
  1478. collectionMoveLoading.value.splice(
  1479. collectionMoveLoading.value.indexOf(collectionIndexDragged),
  1480. 1
  1481. )
  1482. toast.error(`${getErrorMessage(err)}`)
  1483. },
  1484. () => {
  1485. // remove the collection index from the loading array
  1486. collectionMoveLoading.value.splice(
  1487. collectionMoveLoading.value.indexOf(collectionIndexDragged),
  1488. 1
  1489. )
  1490. toast.success(`${t("collection.moved")}`)
  1491. }
  1492. )
  1493. )()
  1494. }
  1495. }
  1496. }
  1497. /**
  1498. * Used to check if the request/collection is being moved to the same parent since reorder is only allowed within the same parent
  1499. * @param draggedItem - path index of the dragged request
  1500. * @param destinationItem - path index of the destination request
  1501. * @param destinationCollectionIndex - index of the destination collection
  1502. * @returns boolean - true if the request is being moved to the same parent
  1503. */
  1504. const isSameSameParent = (
  1505. draggedItemPath: string,
  1506. destinationItemPath: string | null,
  1507. destinationCollectionIndex: string | null
  1508. ) => {
  1509. const draggedItemIndex = pathToIndex(draggedItemPath)
  1510. // if the destinationItemPath and destinationCollectionIndex is null, it means the request is being moved to the root
  1511. if (destinationItemPath === null && destinationCollectionIndex === null) {
  1512. return draggedItemIndex.length === 1
  1513. } else if (
  1514. destinationItemPath === null &&
  1515. destinationCollectionIndex !== null &&
  1516. draggedItemIndex.length === 1
  1517. ) {
  1518. return draggedItemIndex[0] === destinationCollectionIndex
  1519. } else if (
  1520. destinationItemPath === null &&
  1521. draggedItemIndex.length !== 1 &&
  1522. destinationCollectionIndex !== null
  1523. ) {
  1524. const dragedItemParent = draggedItemIndex.slice(0, -1)
  1525. return dragedItemParent.join("/") === destinationCollectionIndex
  1526. }
  1527. if (destinationItemPath === null) return false
  1528. const destinationItemIndex = pathToIndex(destinationItemPath)
  1529. // length of 1 means the request is in the root
  1530. if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
  1531. return true
  1532. } else if (draggedItemIndex.length === destinationItemIndex.length) {
  1533. const dragedItemParent = draggedItemIndex.slice(0, -1)
  1534. const destinationItemParent = destinationItemIndex.slice(0, -1)
  1535. if (isEqual(dragedItemParent, destinationItemParent)) {
  1536. return true
  1537. }
  1538. return false
  1539. }
  1540. return false
  1541. }
  1542. /**
  1543. * This function is called when the user updates the request order in a collection
  1544. * @param payload - object containing the request index dragged and the destination request index
  1545. * with the destination collection index
  1546. */
  1547. const updateRequestOrder = (payload: {
  1548. dragedRequestIndex: string
  1549. destinationRequestIndex: string | null
  1550. destinationCollectionIndex: string
  1551. }) => {
  1552. const {
  1553. dragedRequestIndex,
  1554. destinationRequestIndex,
  1555. destinationCollectionIndex,
  1556. } = payload
  1557. if (!dragedRequestIndex || !destinationCollectionIndex) return
  1558. if (dragedRequestIndex === destinationRequestIndex) return
  1559. if (collectionsType.value.type === "my-collections") {
  1560. if (
  1561. !isSameSameParent(
  1562. dragedRequestIndex,
  1563. destinationRequestIndex,
  1564. destinationCollectionIndex
  1565. )
  1566. ) {
  1567. toast.error(`${t("collection.different_parent")}`)
  1568. } else {
  1569. updateRESTRequestOrder(
  1570. pathToLastIndex(dragedRequestIndex),
  1571. destinationRequestIndex
  1572. ? pathToLastIndex(destinationRequestIndex)
  1573. : null,
  1574. destinationCollectionIndex
  1575. )
  1576. toast.success(`${t("request.order_changed")}`)
  1577. }
  1578. } else if (hasTeamWriteAccess.value) {
  1579. // add the request index to the loading array
  1580. requestMoveLoading.value.push(dragedRequestIndex)
  1581. pipe(
  1582. updateOrderRESTTeamRequest(
  1583. dragedRequestIndex,
  1584. destinationRequestIndex,
  1585. destinationCollectionIndex
  1586. ),
  1587. TE.match(
  1588. (err: GQLError<string>) => {
  1589. toast.error(`${getErrorMessage(err)}`)
  1590. requestMoveLoading.value.splice(
  1591. requestMoveLoading.value.indexOf(dragedRequestIndex),
  1592. 1
  1593. )
  1594. },
  1595. () => {
  1596. toast.success(`${t("request.order_changed")}`)
  1597. // remove the request index from the loading array
  1598. requestMoveLoading.value.splice(
  1599. requestMoveLoading.value.indexOf(dragedRequestIndex),
  1600. 1
  1601. )
  1602. }
  1603. )
  1604. )()
  1605. }
  1606. }
  1607. /**
  1608. * This function is called when the user updates the collection or folder order
  1609. * @param payload - object containing the collection index dragged and the destination collection index
  1610. */
  1611. const updateCollectionOrder = (payload: {
  1612. dragedCollectionIndex: string
  1613. destinationCollection: {
  1614. destinationCollectionIndex: string | null
  1615. destinationCollectionParentIndex: string | null
  1616. }
  1617. }) => {
  1618. const { dragedCollectionIndex, destinationCollection } = payload
  1619. const { destinationCollectionIndex, destinationCollectionParentIndex } =
  1620. destinationCollection
  1621. if (!dragedCollectionIndex) return
  1622. if (dragedCollectionIndex === destinationCollectionIndex) return
  1623. if (collectionsType.value.type === "my-collections") {
  1624. if (
  1625. !isSameSameParent(
  1626. dragedCollectionIndex,
  1627. destinationCollectionIndex,
  1628. destinationCollectionParentIndex
  1629. )
  1630. ) {
  1631. toast.error(`${t("collection.different_parent")}`)
  1632. } else {
  1633. updateRESTCollectionOrder(
  1634. dragedCollectionIndex,
  1635. destinationCollectionIndex
  1636. )
  1637. resolveSaveContextOnCollectionReorder({
  1638. lastIndex: pathToLastIndex(dragedCollectionIndex),
  1639. newIndex: pathToLastIndex(
  1640. destinationCollectionIndex ? destinationCollectionIndex : ""
  1641. ),
  1642. folderPath: dragedCollectionIndex.split("/").slice(0, -1).join("/"),
  1643. })
  1644. toast.success(`${t("collection.order_changed")}`)
  1645. }
  1646. } else if (hasTeamWriteAccess.value) {
  1647. collectionMoveLoading.value.push(dragedCollectionIndex)
  1648. pipe(
  1649. updateOrderRESTTeamCollection(
  1650. dragedCollectionIndex,
  1651. destinationCollectionIndex
  1652. ),
  1653. TE.match(
  1654. (err: GQLError<string>) => {
  1655. toast.error(`${getErrorMessage(err)}`)
  1656. collectionMoveLoading.value.splice(
  1657. collectionMoveLoading.value.indexOf(dragedCollectionIndex),
  1658. 1
  1659. )
  1660. },
  1661. () => {
  1662. toast.success(`${t("collection.order_changed")}`)
  1663. collectionMoveLoading.value.splice(
  1664. collectionMoveLoading.value.indexOf(dragedCollectionIndex),
  1665. 1
  1666. )
  1667. }
  1668. )
  1669. )()
  1670. }
  1671. }
  1672. // Import - Export Collection functions
  1673. /**
  1674. * Create a downloadable file from a collection and prompts the user to download it.
  1675. * @param collectionJSON - JSON string of the collection
  1676. * @param name - Name of the collection set as the file name
  1677. */
  1678. const initializeDownloadCollection = async (
  1679. collectionJSON: string,
  1680. name: string | null
  1681. ) => {
  1682. const result = await platform.io.saveFileWithDialog({
  1683. data: collectionJSON,
  1684. contentType: "application/json",
  1685. suggestedFilename: `${name ?? "collection"}.json`,
  1686. filters: [
  1687. {
  1688. name: "Hoppscotch Collection JSON file",
  1689. extensions: ["json"],
  1690. },
  1691. ],
  1692. })
  1693. if (result.type === "unknown" || result.type === "saved") {
  1694. toast.success(t("state.download_started").toString())
  1695. }
  1696. }
  1697. /**
  1698. * Export a specific collection or folder
  1699. * Triggered by the export button in the tippy menu
  1700. * @param collection - Collection or folder to be exported
  1701. */
  1702. const exportData = async (
  1703. collection: HoppCollection<HoppRESTRequest> | TeamCollection
  1704. ) => {
  1705. if (collectionsType.value.type === "my-collections") {
  1706. const collectionJSON = JSON.stringify(collection)
  1707. const name = (collection as HoppCollection<HoppRESTRequest>).name
  1708. initializeDownloadCollection(collectionJSON, name)
  1709. } else {
  1710. if (!collection.id) return
  1711. exportLoading.value = true
  1712. pipe(
  1713. getCompleteCollectionTree(collection.id),
  1714. TE.match(
  1715. (err: GQLError<string>) => {
  1716. toast.error(`${getErrorMessage(err)}`)
  1717. exportLoading.value = false
  1718. return
  1719. },
  1720. async (coll) => {
  1721. const hoppColl = teamCollToHoppRESTColl(coll)
  1722. const collectionJSONString = JSON.stringify(hoppColl)
  1723. await initializeDownloadCollection(
  1724. collectionJSONString,
  1725. hoppColl.name
  1726. )
  1727. exportLoading.value = false
  1728. }
  1729. )
  1730. )()
  1731. }
  1732. }
  1733. const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
  1734. if (currentUser.value) {
  1735. // opens the share request modal
  1736. invokeAction("share.request", {
  1737. request,
  1738. })
  1739. } else {
  1740. invokeAction("modals.login.toggle")
  1741. }
  1742. }
  1743. const editProperties = (payload: {
  1744. collectionIndex: string
  1745. collection: HoppCollection<HoppRESTRequest> | TeamCollection
  1746. }) => {
  1747. const { collection, collectionIndex } = payload
  1748. const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
  1749. let inheritedProperties = {}
  1750. if (parentIndex) {
  1751. const { auth, headers } = cascaseParentCollectionForHeaderAuth(parentIndex)
  1752. inheritedProperties = {
  1753. auth,
  1754. headers,
  1755. } as HoppInheritedProperty
  1756. }
  1757. editingProperties.value = {
  1758. collection,
  1759. isRootCollection: isAlreadyInRoot(collectionIndex),
  1760. path: collectionIndex,
  1761. inheritedProperties,
  1762. }
  1763. displayModalEditProperties(true)
  1764. }
  1765. const setCollectionProperties = (newCollection: {
  1766. collection: HoppCollection<HoppRESTRequest>
  1767. path: string
  1768. isRootCollection: boolean
  1769. }) => {
  1770. const { collection, path, isRootCollection } = newCollection
  1771. if (isRootCollection) {
  1772. editRESTCollection(parseInt(path), collection)
  1773. } else {
  1774. editRESTFolder(path, collection)
  1775. }
  1776. const { auth, headers } = cascaseParentCollectionForHeaderAuth(path)
  1777. nextTick(() => {
  1778. updateInheritedPropertiesForAffectedRequests(path, {
  1779. auth,
  1780. headers,
  1781. })
  1782. })
  1783. displayModalEditProperties(false)
  1784. }
  1785. const resolveConfirmModal = (title: string | null) => {
  1786. if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
  1787. else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()
  1788. else if (title === `${t("confirm.remove_folder")}`) onRemoveFolder()
  1789. else {
  1790. console.error(
  1791. `Confirm modal title ${title} is not handled by the component`
  1792. )
  1793. toast.error(t("error.something_went_wrong"))
  1794. displayConfirmModal(false)
  1795. }
  1796. }
  1797. const resetSelectedData = () => {
  1798. editingCollection.value = null
  1799. editingCollectionIndex.value = null
  1800. editingCollectionID.value = null
  1801. editingFolder.value = null
  1802. editingFolderPath.value = null
  1803. editingRequest.value = null
  1804. editingRequestIndex.value = null
  1805. editingRequestID.value = null
  1806. confirmModalTitle.value = null
  1807. }
  1808. const getErrorMessage = (err: GQLError<string>) => {
  1809. console.error(err)
  1810. if (err.type === "network_error") {
  1811. return t("error.network_error")
  1812. }
  1813. switch (err.error) {
  1814. case "team_coll/short_title":
  1815. return t("collection.name_length_insufficient")
  1816. case "team/invalid_coll_id":
  1817. case "bug/team_coll/no_coll_id":
  1818. case "team_req/invalid_target_id":
  1819. return t("team.invalid_coll_id")
  1820. case "team/not_required_role":
  1821. return t("profile.no_permission")
  1822. case "team_req/not_required_role":
  1823. return t("profile.no_permission")
  1824. case "Forbidden resource":
  1825. return t("profile.no_permission")
  1826. case "team_req/not_found":
  1827. return t("team.no_request_found")
  1828. case "bug/team_req/no_req_id":
  1829. return t("team.no_request_found")
  1830. case "team/collection_is_parent_coll":
  1831. return t("team.parent_coll_move")
  1832. case "team/target_and_destination_collection_are_same":
  1833. return t("team.same_target_destination")
  1834. case "team/target_collection_is_already_root_collection":
  1835. return t("collection.invalid_root_move")
  1836. case "team_req/requests_not_from_same_collection":
  1837. return t("request.different_collection")
  1838. case "team/team_collections_have_different_parents":
  1839. return t("collection.different_parent")
  1840. default:
  1841. return t("error.something_went_wrong")
  1842. }
  1843. }
  1844. defineActionHandler("collection.new", () => {
  1845. displayModalAdd(true)
  1846. })
  1847. </script>