Edit.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <SmartModal v-if="show" :title="t('team.edit')" @close="hideModal">
  3. <template #body>
  4. <div class="flex flex-col px-2">
  5. <div class="relative flex">
  6. <input
  7. id="selectLabelTeamEdit"
  8. v-model="name"
  9. v-focus
  10. class="input floating-input"
  11. placeholder=" "
  12. type="text"
  13. autocomplete="off"
  14. @keyup.enter="saveTeam"
  15. />
  16. <label for="selectLabelTeamEdit">
  17. {{ t("action.label") }}
  18. </label>
  19. </div>
  20. <div class="flex items-center justify-between flex-1 pt-4">
  21. <label for="memberList" class="p-4">
  22. {{ t("team.members") }}
  23. </label>
  24. <div class="flex">
  25. <ButtonSecondary
  26. svg="user-plus"
  27. :label="t('team.invite')"
  28. filled
  29. @click.native="
  30. () => {
  31. $emit('invite-team')
  32. }
  33. "
  34. />
  35. </div>
  36. </div>
  37. <div
  38. v-if="teamDetails.loading"
  39. class="flex flex-col items-center justify-center"
  40. >
  41. <SmartSpinner class="mb-4" />
  42. <span class="text-secondaryLight">{{ t("state.loading") }}</span>
  43. </div>
  44. <div
  45. v-if="
  46. !teamDetails.loading &&
  47. E.isRight(teamDetails.data) &&
  48. teamDetails.data.right.team.teamMembers
  49. "
  50. class="divide-dividerLight border-divider border divide-y rounded"
  51. >
  52. <div
  53. v-if="teamDetails.data.right.team.teamMembers === 0"
  54. class="text-secondaryLight flex flex-col items-center justify-center p-4"
  55. >
  56. <img
  57. :src="`/images/states/${$colorMode.value}/add_group.svg`"
  58. loading="lazy"
  59. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  60. :alt="`${t('empty.members')}`"
  61. />
  62. <span class="pb-4 text-center">
  63. {{ t("empty.members") }}
  64. </span>
  65. <ButtonSecondary
  66. svg="user-plus"
  67. :label="t('team.invite')"
  68. @click.native="
  69. () => {
  70. emit('invite-team')
  71. }
  72. "
  73. />
  74. </div>
  75. <div v-else>
  76. <div
  77. v-for="(member, index) in membersList"
  78. :key="`member-${index}`"
  79. class="divide-dividerLight flex divide-x"
  80. >
  81. <input
  82. class="flex flex-1 px-4 py-2 bg-transparent"
  83. :placeholder="`${t('team.email')}`"
  84. :name="'param' + index"
  85. :value="member.email"
  86. readonly
  87. />
  88. <span>
  89. <tippy
  90. ref="memberOptions"
  91. interactive
  92. trigger="click"
  93. theme="popover"
  94. arrow
  95. >
  96. <template #trigger>
  97. <span class="select-wrapper">
  98. <input
  99. class="flex flex-1 px-4 py-2 bg-transparent cursor-pointer"
  100. :placeholder="`${t('team.permissions')}`"
  101. :name="'value' + index"
  102. :value="
  103. typeof member.role === 'string'
  104. ? member.role
  105. : JSON.stringify(member.role)
  106. "
  107. readonly
  108. />
  109. </span>
  110. </template>
  111. <SmartItem
  112. label="OWNER"
  113. @click.native="
  114. () => {
  115. updateMemberRole(member.userID, 'OWNER')
  116. memberOptions[index].tippy().hide()
  117. }
  118. "
  119. />
  120. <SmartItem
  121. label="EDITOR"
  122. @click.native="
  123. () => {
  124. updateMemberRole(member.userID, 'EDITOR')
  125. memberOptions[index].tippy().hide()
  126. }
  127. "
  128. />
  129. <SmartItem
  130. label="VIEWER"
  131. @click.native="
  132. () => {
  133. updateMemberRole(member.userID, 'VIEWER')
  134. memberOptions[index].tippy().hide()
  135. }
  136. "
  137. />
  138. </tippy>
  139. </span>
  140. <div class="flex">
  141. <ButtonSecondary
  142. id="member"
  143. v-tippy="{ theme: 'tooltip' }"
  144. :title="t('action.remove')"
  145. svg="trash"
  146. color="red"
  147. @click.native="removeExistingTeamMember(member.userID)"
  148. />
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. <div
  154. v-if="!teamDetails.loading && E.isLeft(teamDetails.data)"
  155. class="flex flex-col items-center"
  156. >
  157. <i class="material-icons mb-4">help_outline</i>
  158. {{ t("error.something_went_wrong") }}
  159. </div>
  160. </div>
  161. </template>
  162. <template #footer>
  163. <span>
  164. <ButtonPrimary :label="t('action.save')" @click.native="saveTeam" />
  165. <ButtonSecondary
  166. :label="t('action.cancel')"
  167. @click.native="hideModal"
  168. />
  169. </span>
  170. </template>
  171. </SmartModal>
  172. </template>
  173. <script setup lang="ts">
  174. import { computed, ref, toRef, watch } from "@nuxtjs/composition-api"
  175. import * as E from "fp-ts/Either"
  176. import {
  177. GetTeamDocument,
  178. GetTeamQuery,
  179. GetTeamQueryVariables,
  180. TeamMemberAddedDocument,
  181. TeamMemberRemovedDocument,
  182. TeamMemberRole,
  183. TeamMemberUpdatedDocument,
  184. } from "~/helpers/backend/graphql"
  185. import {
  186. removeTeamMember,
  187. renameTeam,
  188. updateTeamMemberRole,
  189. } from "~/helpers/backend/mutations/Team"
  190. import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
  191. import { useGQLQuery } from "~/helpers/backend/GQLClient"
  192. import { useI18n, useToast } from "~/helpers/utils/composables"
  193. const t = useI18n()
  194. const emit = defineEmits<{
  195. (e: "hide-modal"): void
  196. }>()
  197. const memberOptions = ref<any | null>(null)
  198. const props = defineProps<{
  199. show: boolean
  200. editingTeam: {
  201. name: string
  202. }
  203. editingTeamID: string
  204. }>()
  205. const toast = useToast()
  206. const name = toRef(props.editingTeam, "name")
  207. watch(
  208. () => props.editingTeam.name,
  209. (newName: string) => {
  210. name.value = newName
  211. }
  212. )
  213. watch(
  214. () => props.editingTeamID,
  215. (teamID: string) => {
  216. teamDetails.execute({ teamID })
  217. }
  218. )
  219. const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({
  220. query: GetTeamDocument,
  221. variables: {
  222. teamID: props.editingTeamID,
  223. },
  224. defer: true,
  225. updateSubs: computed(() => {
  226. if (props.editingTeamID) {
  227. return [
  228. {
  229. key: 1,
  230. query: TeamMemberAddedDocument,
  231. variables: {
  232. teamID: props.editingTeamID,
  233. },
  234. },
  235. {
  236. key: 2,
  237. query: TeamMemberUpdatedDocument,
  238. variables: {
  239. teamID: props.editingTeamID,
  240. },
  241. },
  242. {
  243. key: 3,
  244. query: TeamMemberRemovedDocument,
  245. variables: {
  246. teamID: props.editingTeamID,
  247. },
  248. },
  249. ]
  250. } else return []
  251. }),
  252. })
  253. const roleUpdates = ref<
  254. {
  255. userID: string
  256. role: TeamMemberRole
  257. }[]
  258. >([])
  259. watch(
  260. () => teamDetails,
  261. () => {
  262. if (teamDetails.loading) return
  263. const data = teamDetails.data
  264. if (E.isRight(data)) {
  265. const members = data.right.team?.teamMembers ?? []
  266. // Remove deleted members
  267. roleUpdates.value = roleUpdates.value.filter(
  268. (update) =>
  269. members.findIndex((y) => y.user.uid === update.userID) !== -1
  270. )
  271. }
  272. }
  273. )
  274. const updateMemberRole = (userID: string, role: TeamMemberRole) => {
  275. const updateIndex = roleUpdates.value.findIndex(
  276. (item) => item.userID === userID
  277. )
  278. if (updateIndex !== -1) {
  279. // Role Update exists
  280. roleUpdates.value[updateIndex].role = role
  281. } else {
  282. // Role Update does not exist
  283. roleUpdates.value.push({
  284. userID,
  285. role,
  286. })
  287. }
  288. }
  289. const membersList = computed(() => {
  290. if (teamDetails.loading) return []
  291. const data = teamDetails.data
  292. if (E.isLeft(data)) return []
  293. if (E.isRight(data)) {
  294. const members = (data.right.team?.teamMembers ?? []).map((member) => {
  295. const updatedRole = roleUpdates.value.find(
  296. (update) => update.userID === member.user.uid
  297. )
  298. return {
  299. userID: member.user.uid,
  300. email: member.user.email!,
  301. role: updatedRole?.role ?? member.role,
  302. }
  303. })
  304. return members
  305. }
  306. return []
  307. })
  308. const removeExistingTeamMember = async (userID: string) => {
  309. const removeTeamMemberResult = await removeTeamMember(
  310. userID,
  311. props.editingTeamID
  312. )()
  313. if (E.isLeft(removeTeamMemberResult)) {
  314. toast.error(`${t("error.something_went_wrong")}`)
  315. } else {
  316. toast.success(`${t("team.member_removed")}`)
  317. }
  318. }
  319. const saveTeam = async () => {
  320. if (name.value !== "") {
  321. if (TeamNameCodec.is(name.value)) {
  322. const updateTeamNameResult = await renameTeam(
  323. props.editingTeamID,
  324. name.value
  325. )()
  326. if (E.isLeft(updateTeamNameResult)) {
  327. toast.error(`${t("error.something_went_wrong")}`)
  328. } else {
  329. roleUpdates.value.forEach(async (update) => {
  330. const updateMemberRoleResult = await updateTeamMemberRole(
  331. update.userID,
  332. props.editingTeamID,
  333. update.role
  334. )()
  335. if (E.isLeft(updateMemberRoleResult)) {
  336. toast.error(`${t("error.something_went_wrong")}`)
  337. console.error(updateMemberRoleResult.left.error)
  338. }
  339. })
  340. }
  341. hideModal()
  342. toast.success(`${t("team.saved")}`)
  343. } else {
  344. return toast.error(`${t("team.name_length_insufficient")}`)
  345. }
  346. } else {
  347. return toast.error(`${t("empty.team_name")}`)
  348. }
  349. }
  350. const hideModal = () => {
  351. emit("hide-modal")
  352. }
  353. </script>