Team.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <template>
  2. <div
  3. class="border border-divider rounded flex flex-col flex-1"
  4. @contextmenu.prevent="!compact ? $refs.options.tippy().show() : null"
  5. >
  6. <div
  7. class="flex flex-1 items-start"
  8. :class="
  9. compact
  10. ? team.myRole === 'OWNER'
  11. ? 'cursor-pointer hover:bg-primaryDark transition hover:border-dividerDark focus-visible:border-dividerDark'
  12. : 'cursor-not-allowed bg-primaryLight'
  13. : ''
  14. "
  15. @click="
  16. compact
  17. ? team.myRole === 'OWNER'
  18. ? $emit('invite-team')
  19. : noPermission()
  20. : ''
  21. "
  22. >
  23. <div class="p-4">
  24. <label
  25. class="font-semibold text-secondaryDark"
  26. :class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }"
  27. >
  28. {{ team.name || t("state.nothing_found") }}
  29. </label>
  30. <div class="flex -space-x-1 mt-2 overflow-hidden">
  31. <img
  32. v-for="(member, index) in team.teamMembers"
  33. :key="`member-${index}`"
  34. v-tippy="{ theme: 'tooltip' }"
  35. :title="member.user.displayName"
  36. :src="member.user.photoURL || undefined"
  37. :alt="member.user.displayName"
  38. class="rounded-full h-5 ring-primary ring-2 w-5 inline-block"
  39. loading="lazy"
  40. />
  41. </div>
  42. </div>
  43. </div>
  44. <div v-if="!compact" class="flex flex-shrink-0 items-end justify-between">
  45. <span>
  46. <ButtonSecondary
  47. v-if="team.myRole === 'OWNER'"
  48. svg="edit"
  49. class="rounded-none"
  50. :label="t('action.edit')"
  51. @click.native="
  52. () => {
  53. $emit('edit-team')
  54. }
  55. "
  56. />
  57. <ButtonSecondary
  58. v-if="team.myRole === 'OWNER'"
  59. svg="user-plus"
  60. class="rounded-none"
  61. :label="t('team.invite')"
  62. @click.native="
  63. () => {
  64. emit('invite-team')
  65. }
  66. "
  67. />
  68. </span>
  69. <span>
  70. <tippy ref="options" interactive trigger="click" theme="popover" arrow>
  71. <template #trigger>
  72. <ButtonSecondary
  73. v-tippy="{ theme: 'tooltip' }"
  74. :title="t('action.more')"
  75. svg="more-vertical"
  76. />
  77. </template>
  78. <SmartItem
  79. v-if="team.myRole === 'OWNER'"
  80. svg="edit"
  81. :label="t('action.edit')"
  82. @click.native="
  83. () => {
  84. $emit('edit-team')
  85. $refs.options.tippy().hide()
  86. }
  87. "
  88. />
  89. <SmartItem
  90. v-if="team.myRole === 'OWNER'"
  91. svg="trash-2"
  92. :label="t('action.delete')"
  93. @click.native="
  94. () => {
  95. confirmRemove = true
  96. $refs.options.tippy().hide()
  97. }
  98. "
  99. />
  100. <SmartItem
  101. v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
  102. svg="trash"
  103. :label="t('team.exit')"
  104. @click.native="
  105. () => {
  106. confirmExit = true
  107. $refs.options.tippy().hide()
  108. }
  109. "
  110. />
  111. </tippy>
  112. </span>
  113. </div>
  114. <SmartConfirmModal
  115. :show="confirmRemove"
  116. :title="t('confirm.remove_team')"
  117. @hide-modal="confirmRemove = false"
  118. @resolve="deleteTeam()"
  119. />
  120. <SmartConfirmModal
  121. :show="confirmExit"
  122. :title="t('confirm.exit_team')"
  123. @hide-modal="confirmExit = false"
  124. @resolve="exitTeam()"
  125. />
  126. </div>
  127. </template>
  128. <script setup lang="ts">
  129. import { ref } from "@nuxtjs/composition-api"
  130. import { pipe } from "fp-ts/function"
  131. import * as TE from "fp-ts/TaskEither"
  132. import { TeamMemberRole } from "~/helpers/backend/graphql"
  133. import {
  134. deleteTeam as backendDeleteTeam,
  135. leaveTeam,
  136. } from "~/helpers/backend/mutations/Team"
  137. import { useI18n, useToast } from "~/helpers/utils/composables"
  138. const t = useI18n()
  139. const props = defineProps<{
  140. team: {
  141. name: string
  142. myRole: TeamMemberRole
  143. ownersCount: number
  144. teamMembers: Array<{
  145. user: {
  146. displayName: string
  147. photoURL: string | null
  148. }
  149. }>
  150. }
  151. teamID: string
  152. compact: boolean
  153. }>()
  154. const emit = defineEmits<{
  155. (e: "edit-team"): void
  156. }>()
  157. const toast = useToast()
  158. const confirmRemove = ref(false)
  159. const confirmExit = ref(false)
  160. const deleteTeam = () => {
  161. pipe(
  162. backendDeleteTeam(props.teamID),
  163. TE.match(
  164. (err) => {
  165. // TODO: Better errors ? We know the possible errors now
  166. toast.error(`${t("error.something_went_wrong")}`)
  167. console.error(err)
  168. },
  169. () => {
  170. toast.success(`${t("team.deleted")}`)
  171. }
  172. )
  173. )() // Tasks (and TEs) are lazy, so call the function returned
  174. }
  175. const exitTeam = () => {
  176. pipe(
  177. leaveTeam(props.teamID),
  178. TE.match(
  179. (err) => {
  180. // TODO: Better errors ?
  181. toast.error(`${t("error.something_went_wrong")}`)
  182. console.error(err)
  183. },
  184. () => {
  185. toast.success(`${t("team.left")}`)
  186. }
  187. )
  188. )() // Tasks (and TEs) are lazy, so call the function returned
  189. }
  190. const noPermission = () => {
  191. toast.error(`${t("profile.no_permission")}`)
  192. }
  193. </script>