Edit.vue 11 KB

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