Team.vue 4.8 KB

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