Edit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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="flex relative">
  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 flex-1 pt-4 items-center justify-between">
  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 divide-y border border-divider rounded"
  51. >
  52. <div
  53. v-if="teamDetails.data.right.team.teamMembers === 0"
  54. class="flex flex-col text-secondaryLight p-4 items-center justify-center"
  55. >
  56. <img
  57. :src="`/images/states/${$colorMode.value}/add_group.svg`"
  58. loading="lazy"
  59. class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
  60. :alt="`${t('empty.members')}`"
  61. />
  62. <span class="text-center pb-4">
  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 divide-x flex"
  80. >
  81. <input
  82. class="bg-transparent flex flex-1 py-2 px-4"
  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="bg-transparent cursor-pointer flex flex-1 py-2 px-4"
  100. :placeholder="`${t('team.permissions')}`"
  101. :name="'value' + index"
  102. :value="member.role"
  103. readonly
  104. />
  105. </span>
  106. </template>
  107. <SmartItem
  108. label="OWNER"
  109. :icon="
  110. member.role === 'OWNER'
  111. ? 'radio_button_checked'
  112. : 'radio_button_unchecked'
  113. "
  114. @click.native="
  115. () => {
  116. updateMemberRole(member.userID, 'OWNER')
  117. memberOptions[index].tippy().hide()
  118. }
  119. "
  120. />
  121. <SmartItem
  122. label="EDITOR"
  123. :icon="
  124. member.role === 'EDITOR'
  125. ? 'radio_button_checked'
  126. : 'radio_button_unchecked'
  127. "
  128. @click.native="
  129. () => {
  130. updateMemberRole(member.userID, 'EDITOR')
  131. memberOptions[index].tippy().hide()
  132. }
  133. "
  134. />
  135. <SmartItem
  136. label="VIEWER"
  137. :icon="
  138. member.role === 'VIEWER'
  139. ? 'radio_button_checked'
  140. : 'radio_button_unchecked'
  141. "
  142. @click.native="
  143. () => {
  144. updateMemberRole(member.userID, 'VIEWER')
  145. memberOptions[index].tippy().hide()
  146. }
  147. "
  148. />
  149. </tippy>
  150. </span>
  151. <div class="flex">
  152. <ButtonSecondary
  153. id="member"
  154. v-tippy="{ theme: 'tooltip' }"
  155. :title="t('action.remove')"
  156. svg="trash"
  157. color="red"
  158. @click.native="removeExistingTeamMember(member.userID)"
  159. />
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. <div
  165. v-if="!teamDetails.loading && E.isLeft(teamDetails.data)"
  166. class="flex flex-col items-center"
  167. >
  168. <i class="mb-4 material-icons">help_outline</i>
  169. {{ t("error.something_went_wrong") }}
  170. </div>
  171. </div>
  172. </template>
  173. <template #footer>
  174. <span>
  175. <ButtonPrimary :label="t('action.save')" @click.native="saveTeam" />
  176. <ButtonSecondary
  177. :label="t('action.cancel')"
  178. @click.native="hideModal"
  179. />
  180. </span>
  181. </template>
  182. </SmartModal>
  183. </template>
  184. <script setup lang="ts">
  185. import { computed, ref, toRef, watch } from "@nuxtjs/composition-api"
  186. import * as E from "fp-ts/Either"
  187. import {
  188. GetTeamDocument,
  189. GetTeamQuery,
  190. GetTeamQueryVariables,
  191. TeamMemberAddedDocument,
  192. TeamMemberRemovedDocument,
  193. TeamMemberRole,
  194. TeamMemberUpdatedDocument,
  195. } from "~/helpers/backend/graphql"
  196. import {
  197. removeTeamMember,
  198. renameTeam,
  199. updateTeamMemberRole,
  200. } from "~/helpers/backend/mutations/Team"
  201. import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
  202. import { useGQLQuery } from "~/helpers/backend/GQLClient"
  203. import { useI18n, useToast } from "~/helpers/utils/composables"
  204. const t = useI18n()
  205. const emit = defineEmits<{
  206. (e: "hide-modal"): void
  207. }>()
  208. const memberOptions = ref<any | null>(null)
  209. const props = defineProps<{
  210. show: boolean
  211. editingTeam: {
  212. name: string
  213. }
  214. editingTeamID: string
  215. }>()
  216. const toast = useToast()
  217. const name = toRef(props.editingTeam, "name")
  218. watch(
  219. () => props.editingTeam.name,
  220. (newName: string) => {
  221. name.value = newName
  222. }
  223. )
  224. watch(
  225. () => props.editingTeamID,
  226. (teamID: string) => {
  227. teamDetails.execute({ teamID })
  228. }
  229. )
  230. const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({
  231. query: GetTeamDocument,
  232. variables: {
  233. teamID: props.editingTeamID,
  234. },
  235. defer: true,
  236. updateSubs: computed(() => {
  237. if (props.editingTeamID) {
  238. return [
  239. {
  240. key: 1,
  241. query: TeamMemberAddedDocument,
  242. variables: {
  243. teamID: props.editingTeamID,
  244. },
  245. },
  246. {
  247. key: 2,
  248. query: TeamMemberUpdatedDocument,
  249. variables: {
  250. teamID: props.editingTeamID,
  251. },
  252. },
  253. {
  254. key: 3,
  255. query: TeamMemberRemovedDocument,
  256. variables: {
  257. teamID: props.editingTeamID,
  258. },
  259. },
  260. ]
  261. } else return []
  262. }),
  263. })
  264. const roleUpdates = ref<
  265. {
  266. userID: string
  267. role: TeamMemberRole
  268. }[]
  269. >([])
  270. watch(
  271. () => teamDetails,
  272. () => {
  273. if (teamDetails.loading) return
  274. const data = teamDetails.data
  275. if (E.isRight(data)) {
  276. const members = data.right.team?.teamMembers ?? []
  277. // Remove deleted members
  278. roleUpdates.value = roleUpdates.value.filter(
  279. (update) =>
  280. members.findIndex((y) => y.user.uid === update.userID) !== -1
  281. )
  282. }
  283. }
  284. )
  285. const updateMemberRole = (userID: string, role: TeamMemberRole) => {
  286. const updateIndex = roleUpdates.value.findIndex(
  287. (item) => item.userID === userID
  288. )
  289. if (updateIndex !== -1) {
  290. // Role Update exists
  291. roleUpdates.value[updateIndex].role = role
  292. } else {
  293. // Role Update does not exist
  294. roleUpdates.value.push({
  295. userID,
  296. role,
  297. })
  298. }
  299. }
  300. const membersList = computed(() => {
  301. if (teamDetails.loading) return []
  302. const data = teamDetails.data
  303. if (E.isLeft(data)) return []
  304. if (E.isRight(data)) {
  305. const members = (data.right.team?.teamMembers ?? []).map((member) => {
  306. const updatedRole = roleUpdates.value.find(
  307. (update) => update.userID === member.user.uid
  308. )
  309. return {
  310. userID: member.user.uid,
  311. email: member.user.email!,
  312. role: updatedRole?.role ?? member.role,
  313. }
  314. })
  315. return members
  316. }
  317. return []
  318. })
  319. const removeExistingTeamMember = async (userID: string) => {
  320. const removeTeamMemberResult = await removeTeamMember(
  321. userID,
  322. props.editingTeamID
  323. )()
  324. if (E.isLeft(removeTeamMemberResult)) {
  325. toast.error(`${t("error.something_went_wrong")}`)
  326. } else {
  327. toast.success(`${t("team.member_removed")}`)
  328. }
  329. }
  330. const saveTeam = async () => {
  331. if (name.value !== "") {
  332. if (TeamNameCodec.is(name.value)) {
  333. const updateTeamNameResult = await renameTeam(
  334. props.editingTeamID,
  335. name.value
  336. )()
  337. if (E.isLeft(updateTeamNameResult)) {
  338. toast.error(`${t("error.something_went_wrong")}`)
  339. } else {
  340. roleUpdates.value.forEach(async (update) => {
  341. const updateMemberRoleResult = await updateTeamMemberRole(
  342. update.userID,
  343. props.editingTeamID,
  344. update.role
  345. )()
  346. if (E.isLeft(updateMemberRoleResult)) {
  347. toast.error(`${t("error.something_went_wrong")}`)
  348. console.error(updateMemberRoleResult.left.error)
  349. }
  350. })
  351. }
  352. hideModal()
  353. toast.success(`${t("team.saved")}`)
  354. } else {
  355. return toast.error(`${t("team.name_length_insufficient")}`)
  356. }
  357. } else {
  358. return toast.error(`${t("empty.team_name")}`)
  359. }
  360. }
  361. const hideModal = () => {
  362. emit("hide-modal")
  363. }
  364. </script>