Team.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <div
  3. class="flex flex-col flex-1 border rounded border-divider"
  4. @contextmenu.prevent="!compact ? options.tippy().show() : null"
  5. >
  6. <div
  7. class="flex items-start flex-1"
  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 mt-2 overflow-hidden -space-x-1">
  31. <div
  32. v-for="(member, index) in team.teamMembers"
  33. :key="`member-${index}`"
  34. v-tippy="{ theme: 'tooltip' }"
  35. :title="member.user.displayName"
  36. class="inline-flex"
  37. >
  38. <ProfilePicture
  39. v-if="member.user.photoURL"
  40. :url="member.user.photoURL"
  41. :alt="member.user.displayName"
  42. class="ring-primary ring-2"
  43. />
  44. <ProfilePicture
  45. v-else
  46. :initial="member.user.displayName"
  47. class="ring-primary ring-2"
  48. />
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div v-if="!compact" class="flex items-end justify-between flex-shrink-0">
  54. <span>
  55. <ButtonSecondary
  56. v-if="team.myRole === 'OWNER'"
  57. svg="edit"
  58. class="rounded-none"
  59. :label="t('action.edit')"
  60. @click.native="
  61. () => {
  62. $emit('edit-team')
  63. }
  64. "
  65. />
  66. <ButtonSecondary
  67. v-if="team.myRole === 'OWNER'"
  68. svg="user-plus"
  69. class="rounded-none"
  70. :label="t('team.invite')"
  71. @click.native="
  72. () => {
  73. emit('invite-team')
  74. }
  75. "
  76. />
  77. </span>
  78. <span>
  79. <tippy
  80. ref="options"
  81. interactive
  82. trigger="click"
  83. theme="popover"
  84. arrow
  85. :on-shown="() => tippyActions.focus()"
  86. >
  87. <template #trigger>
  88. <ButtonSecondary
  89. v-tippy="{ theme: 'tooltip' }"
  90. :title="t('action.more')"
  91. svg="more-vertical"
  92. />
  93. </template>
  94. <div
  95. ref="tippyActions"
  96. class="flex flex-col focus:outline-none"
  97. tabindex="0"
  98. role="menu"
  99. @keyup.e="team.myRole === 'OWNER' ? edit.$el.click() : null"
  100. @keyup.x="
  101. !(team.myRole === 'OWNER' && team.ownersCount == 1)
  102. ? exit.$el.click()
  103. : null
  104. "
  105. @keyup.delete="
  106. team.myRole === 'OWNER' ? deleteAction.$el.click() : null
  107. "
  108. @keyup.escape="options.tippy().hide()"
  109. >
  110. <SmartItem
  111. v-if="team.myRole === 'OWNER'"
  112. ref="edit"
  113. svg="edit"
  114. :label="t('action.edit')"
  115. :shortcut="['E']"
  116. @click.native="
  117. () => {
  118. $emit('edit-team')
  119. options.tippy().hide()
  120. }
  121. "
  122. />
  123. <SmartItem
  124. v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
  125. ref="exit"
  126. svg="user-x"
  127. :label="t('team.exit')"
  128. :shortcut="['X']"
  129. @click.native="
  130. () => {
  131. confirmExit = true
  132. options.tippy().hide()
  133. }
  134. "
  135. />
  136. <SmartItem
  137. v-if="team.myRole === 'OWNER'"
  138. ref="deleteAction"
  139. svg="trash-2"
  140. :label="t('action.delete')"
  141. :shortcut="['⌫']"
  142. @click.native="
  143. () => {
  144. confirmRemove = true
  145. options.tippy().hide()
  146. }
  147. "
  148. />
  149. </div>
  150. </tippy>
  151. </span>
  152. </div>
  153. <SmartConfirmModal
  154. :show="confirmRemove"
  155. :title="t('confirm.remove_team')"
  156. @hide-modal="confirmRemove = false"
  157. @resolve="deleteTeam()"
  158. />
  159. <SmartConfirmModal
  160. :show="confirmExit"
  161. :title="t('confirm.exit_team')"
  162. @hide-modal="confirmExit = false"
  163. @resolve="exitTeam()"
  164. />
  165. </div>
  166. </template>
  167. <script setup lang="ts">
  168. import { ref } from "@nuxtjs/composition-api"
  169. import { pipe } from "fp-ts/function"
  170. import * as TE from "fp-ts/TaskEither"
  171. import { TeamMemberRole } from "~/helpers/backend/graphql"
  172. import {
  173. deleteTeam as backendDeleteTeam,
  174. leaveTeam,
  175. } from "~/helpers/backend/mutations/Team"
  176. import { useI18n, useToast } from "~/helpers/utils/composables"
  177. const t = useI18n()
  178. const props = defineProps<{
  179. team: {
  180. name: string
  181. myRole: TeamMemberRole
  182. ownersCount: number
  183. teamMembers: Array<{
  184. user: {
  185. displayName: string
  186. photoURL: string | null
  187. }
  188. }>
  189. }
  190. teamID: string
  191. compact: boolean
  192. }>()
  193. const emit = defineEmits<{
  194. (e: "edit-team"): void
  195. }>()
  196. const toast = useToast()
  197. const confirmRemove = ref(false)
  198. const confirmExit = ref(false)
  199. const deleteTeam = () => {
  200. pipe(
  201. backendDeleteTeam(props.teamID),
  202. TE.match(
  203. (err) => {
  204. // TODO: Better errors ? We know the possible errors now
  205. toast.error(`${t("error.something_went_wrong")}`)
  206. console.error(err)
  207. },
  208. () => {
  209. toast.success(`${t("team.deleted")}`)
  210. }
  211. )
  212. )() // Tasks (and TEs) are lazy, so call the function returned
  213. }
  214. const exitTeam = () => {
  215. pipe(
  216. leaveTeam(props.teamID),
  217. TE.match(
  218. (err) => {
  219. // TODO: Better errors ?
  220. toast.error(`${t("error.something_went_wrong")}`)
  221. console.error(err)
  222. },
  223. () => {
  224. toast.success(`${t("team.left")}`)
  225. }
  226. )
  227. )() // Tasks (and TEs) are lazy, so call the function returned
  228. }
  229. const noPermission = () => {
  230. toast.error(`${t("profile.no_permission")}`)
  231. }
  232. // Template refs
  233. const tippyActions = ref<any | null>(null)
  234. const options = ref<any | null>(null)
  235. const edit = ref<any | null>(null)
  236. const deleteAction = ref<any | null>(null)
  237. const exit = ref<any | null>(null)
  238. </script>