Team.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. color="red"
  93. :label="t('action.delete')"
  94. @click.native="
  95. () => {
  96. confirmRemove = true
  97. $refs.options.tippy().hide()
  98. }
  99. "
  100. />
  101. <SmartItem
  102. v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
  103. svg="trash"
  104. :label="t('team.exit')"
  105. @click.native="
  106. () => {
  107. confirmExit = true
  108. $refs.options.tippy().hide()
  109. }
  110. "
  111. />
  112. </tippy>
  113. </span>
  114. </div>
  115. <SmartConfirmModal
  116. :show="confirmRemove"
  117. :title="t('confirm.remove_team')"
  118. @hide-modal="confirmRemove = false"
  119. @resolve="deleteTeam()"
  120. />
  121. <SmartConfirmModal
  122. :show="confirmExit"
  123. :title="t('confirm.exit_team')"
  124. @hide-modal="confirmExit = false"
  125. @resolve="exitTeam()"
  126. />
  127. </div>
  128. </template>
  129. <script setup lang="ts">
  130. import { ref } from "@nuxtjs/composition-api"
  131. import { pipe } from "fp-ts/function"
  132. import * as TE from "fp-ts/TaskEither"
  133. import { TeamMemberRole } from "~/helpers/backend/graphql"
  134. import {
  135. deleteTeam as backendDeleteTeam,
  136. leaveTeam,
  137. } from "~/helpers/backend/mutations/Team"
  138. import { useI18n, useToast } from "~/helpers/utils/composables"
  139. const t = useI18n()
  140. const props = defineProps<{
  141. team: {
  142. name: string
  143. myRole: TeamMemberRole
  144. ownersCount: number
  145. teamMembers: Array<{
  146. user: {
  147. displayName: string
  148. photoURL: string | null
  149. }
  150. }>
  151. }
  152. teamID: string
  153. compact: boolean
  154. }>()
  155. const emit = defineEmits<{
  156. (e: "edit-team"): void
  157. }>()
  158. const toast = useToast()
  159. const confirmRemove = ref(false)
  160. const confirmExit = ref(false)
  161. const deleteTeam = () => {
  162. pipe(
  163. backendDeleteTeam(props.teamID),
  164. TE.match(
  165. (err) => {
  166. // TODO: Better errors ? We know the possible errors now
  167. toast.error(`${t("error.something_went_wrong")}`)
  168. console.error(err)
  169. },
  170. () => {
  171. toast.success(`${t("team.deleted")}`)
  172. }
  173. )
  174. )() // Tasks (and TEs) are lazy, so call the function returned
  175. }
  176. const exitTeam = () => {
  177. pipe(
  178. leaveTeam(props.teamID),
  179. TE.match(
  180. (err) => {
  181. // TODO: Better errors ?
  182. toast.error(`${t("error.something_went_wrong")}`)
  183. console.error(err)
  184. },
  185. () => {
  186. toast.success(`${t("team.left")}`)
  187. }
  188. )
  189. )() // Tasks (and TEs) are lazy, so call the function returned
  190. }
  191. const noPermission = () => {
  192. toast.error(`${t("profile.no_permission")}`)
  193. }
  194. </script>