Team.vue 5.1 KB

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