dropdown-button.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import {
  2. type ComponentProps,
  3. useCallback,
  4. type Dispatch,
  5. type SetStateAction,
  6. } from 'react'
  7. import { useTranslation } from 'react-i18next'
  8. import {
  9. Dropdown,
  10. DropdownItem,
  11. DropdownMenu,
  12. DropdownToggle,
  13. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  14. import { User } from '../../../../../../types/group-management/user'
  15. import useAsync from '@/shared/hooks/use-async'
  16. import { type FetchError, postJSON } from '@/infrastructure/fetch-json'
  17. import { GroupUserAlert } from '../../utils/types'
  18. import { useGroupMembersContext } from '../../context/group-members-context'
  19. import getMeta from '@/utils/meta'
  20. import MaterialIcon from '@/shared/components/material-icon'
  21. import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
  22. import { Spinner } from 'react-bootstrap'
  23. import { sendMB } from '@/infrastructure/event-tracking'
  24. type resendInviteResponse = {
  25. success: boolean
  26. }
  27. type ManagedUserDropdownButtonProps = {
  28. user: User
  29. openOffboardingModalForUser: (user: User) => void
  30. openRemoveModalForUser: (user: User) => void
  31. openUnlinkUserModal: (user: User) => void
  32. groupId: string
  33. setGroupUserAlert: Dispatch<SetStateAction<GroupUserAlert>>
  34. }
  35. export default function DropdownButton({
  36. user,
  37. openOffboardingModalForUser,
  38. openRemoveModalForUser,
  39. openUnlinkUserModal,
  40. groupId,
  41. setGroupUserAlert,
  42. }: ManagedUserDropdownButtonProps) {
  43. const { t } = useTranslation()
  44. const { removeMember } = useGroupMembersContext()
  45. const {
  46. runAsync: runResendManagedUserInviteAsync,
  47. isLoading: isResendingManagedUserInvite,
  48. } = useAsync<resendInviteResponse>()
  49. const {
  50. runAsync: runResendLinkSSOInviteAsync,
  51. isLoading: isResendingSSOLinkInvite,
  52. } = useAsync<resendInviteResponse>()
  53. const {
  54. runAsync: runResendGroupInviteAsync,
  55. isLoading: isResendingGroupInvite,
  56. } = useAsync<resendInviteResponse>()
  57. const managedUsersActive = getMeta('ol-managedUsersActive')
  58. const groupSSOActive = getMeta('ol-groupSSOActive')
  59. const userId = getMeta('ol-user_id')
  60. const isUserGroupManager = getMeta('ol-isUserGroupManager')
  61. const userPending = user.invite
  62. const isGroupSSOLinked =
  63. !userPending && user.enrollment?.sso?.some(sso => sso.groupId === groupId)
  64. const isUserManaged = !userPending && user.enrollment?.managedBy === groupId
  65. const handleResendManagedUserInvite = useCallback(
  66. async (user: User) => {
  67. try {
  68. const result = await runResendManagedUserInviteAsync(
  69. postJSON(
  70. `/manage/groups/${groupId}/resendManagedUserInvite/${user._id}`
  71. )
  72. )
  73. if (result.success) {
  74. setGroupUserAlert({
  75. variant: 'resendManagedUserInviteSuccess',
  76. email: user.email,
  77. })
  78. }
  79. } catch (err) {
  80. if ((err as FetchError)?.response?.status === 429) {
  81. setGroupUserAlert({
  82. variant: 'resendInviteTooManyRequests',
  83. email: user.email,
  84. })
  85. } else {
  86. setGroupUserAlert({
  87. variant: 'resendManagedUserInviteFailed',
  88. email: user.email,
  89. })
  90. }
  91. }
  92. },
  93. [setGroupUserAlert, groupId, runResendManagedUserInviteAsync]
  94. )
  95. const handleResendLinkSSOInviteAsync = useCallback(
  96. async (user: User) => {
  97. try {
  98. const result = await runResendLinkSSOInviteAsync(
  99. postJSON(`/manage/groups/${groupId}/resendSSOLinkInvite/${user._id}`)
  100. )
  101. if (result.success) {
  102. setGroupUserAlert({
  103. variant: 'resendSSOLinkInviteSuccess',
  104. email: user.email,
  105. })
  106. }
  107. } catch (err) {
  108. if ((err as FetchError)?.response?.status === 429) {
  109. setGroupUserAlert({
  110. variant: 'resendInviteTooManyRequests',
  111. email: user.email,
  112. })
  113. } else {
  114. setGroupUserAlert({
  115. variant: 'resendSSOLinkInviteFailed',
  116. email: user.email,
  117. })
  118. }
  119. }
  120. },
  121. [setGroupUserAlert, groupId, runResendLinkSSOInviteAsync]
  122. )
  123. const handleResendGroupInvite = useCallback(
  124. async (user: User) => {
  125. try {
  126. await runResendGroupInviteAsync(
  127. postJSON(`/manage/groups/${groupId}/resendInvite/`, {
  128. body: {
  129. email: user.email,
  130. },
  131. })
  132. )
  133. setGroupUserAlert({
  134. variant: 'resendGroupInviteSuccess',
  135. email: user.email,
  136. })
  137. } catch (err) {
  138. if ((err as FetchError)?.response?.status === 429) {
  139. setGroupUserAlert({
  140. variant: 'resendInviteTooManyRequests',
  141. email: user.email,
  142. })
  143. } else {
  144. setGroupUserAlert({
  145. variant: 'resendGroupInviteFailed',
  146. email: user.email,
  147. })
  148. }
  149. }
  150. },
  151. [setGroupUserAlert, groupId, runResendGroupInviteAsync]
  152. )
  153. const onResendManagedUserInviteClick = () => {
  154. handleResendManagedUserInvite(user)
  155. }
  156. const onResendSSOLinkInviteClick = () => {
  157. handleResendLinkSSOInviteAsync(user)
  158. }
  159. const onResendGroupInviteClick = () => {
  160. handleResendGroupInvite(user)
  161. }
  162. const onDeleteUserClick = () => {
  163. sendMB('delete-managed-user-selected')
  164. openOffboardingModalForUser(user)
  165. }
  166. const onReleaseUserClick = () => {
  167. sendMB('remove-managed-user-selected')
  168. openRemoveModalForUser(user)
  169. }
  170. const onRemoveFromGroup = () => {
  171. removeMember(user)
  172. }
  173. const onUnlinkUserClick = () => {
  174. openUnlinkUserModal(user)
  175. }
  176. const buttons = []
  177. if (userPending) {
  178. buttons.push(
  179. <MenuItemButton
  180. onClick={onResendGroupInviteClick}
  181. key="resend-group-invite-action"
  182. isLoading={isResendingGroupInvite}
  183. data-testid="resend-group-invite-action"
  184. >
  185. {t('resend_group_invite')}
  186. </MenuItemButton>
  187. )
  188. }
  189. if (managedUsersActive && !isUserManaged && !userPending) {
  190. buttons.push(
  191. <MenuItemButton
  192. onClick={onResendManagedUserInviteClick}
  193. key="resend-managed-user-invite-action"
  194. isLoading={isResendingManagedUserInvite}
  195. data-testid="resend-managed-user-invite-action"
  196. >
  197. {t('resend_managed_user_invite')}
  198. </MenuItemButton>
  199. )
  200. }
  201. if (groupSSOActive && isGroupSSOLinked) {
  202. buttons.push(
  203. <MenuItemButton
  204. onClick={onUnlinkUserClick}
  205. key="unlink-user-action"
  206. data-testid="unlink-user-action"
  207. >
  208. {t('unlink_user')}
  209. </MenuItemButton>
  210. )
  211. }
  212. if (groupSSOActive && !isGroupSSOLinked && !userPending) {
  213. buttons.push(
  214. <MenuItemButton
  215. onClick={onResendSSOLinkInviteClick}
  216. key="resend-sso-link-invite-action"
  217. isLoading={isResendingSSOLinkInvite}
  218. data-testid="resend-sso-link-invite-action"
  219. >
  220. {t('resend_link_sso')}
  221. </MenuItemButton>
  222. )
  223. }
  224. if (
  225. isUserManaged &&
  226. !user.isEntityAdmin &&
  227. (!isUserGroupManager || userId !== user._id)
  228. ) {
  229. buttons.push(
  230. <MenuItemButton
  231. key="delete-user-action"
  232. data-testid="delete-user-action"
  233. onClick={onDeleteUserClick}
  234. >
  235. {t('delete_user')}
  236. </MenuItemButton>
  237. )
  238. buttons.push(
  239. <MenuItemButton
  240. key="release-user-action"
  241. data-testid="release-user-action"
  242. onClick={onReleaseUserClick}
  243. >
  244. {t('remove_user')}
  245. </MenuItemButton>
  246. )
  247. } else if (!isUserManaged) {
  248. buttons.push(
  249. <MenuItemButton
  250. key="remove-user-action"
  251. data-testid="remove-user-action"
  252. onClick={onRemoveFromGroup}
  253. variant="danger"
  254. >
  255. {t('remove_from_group')}
  256. </MenuItemButton>
  257. )
  258. }
  259. if (buttons.length === 0) {
  260. buttons.push(
  261. <DropdownListItem key="no-actions-available">
  262. <DropdownItem
  263. as="button"
  264. tabIndex={-1}
  265. data-testid="no-actions-available"
  266. disabled
  267. >
  268. {t('no_actions')}
  269. </DropdownItem>
  270. </DropdownListItem>
  271. )
  272. }
  273. return (
  274. <Dropdown align="end">
  275. <DropdownToggle
  276. id={`managed-user-dropdown-${user.email}`}
  277. bsPrefix="dropdown-table-button-toggle"
  278. >
  279. <MaterialIcon type="more_vert" accessibilityLabel={t('actions')} />
  280. </DropdownToggle>
  281. <DropdownMenu flip={false}>{buttons}</DropdownMenu>
  282. </Dropdown>
  283. )
  284. }
  285. type MenuItemButtonProps = {
  286. isLoading?: boolean
  287. 'data-testid'?: string
  288. } & Pick<ComponentProps<'button'>, 'children' | 'onClick' | 'className'> &
  289. Pick<ComponentProps<typeof DropdownItem>, 'variant'>
  290. function MenuItemButton({
  291. children,
  292. onClick,
  293. isLoading,
  294. variant,
  295. 'data-testid': dataTestId,
  296. }: MenuItemButtonProps) {
  297. return (
  298. <DropdownListItem>
  299. <DropdownItem
  300. as="button"
  301. tabIndex={-1}
  302. onClick={onClick}
  303. leadingIcon={
  304. isLoading ? (
  305. <Spinner
  306. animation="border"
  307. aria-hidden="true"
  308. size="sm"
  309. role="status"
  310. />
  311. ) : null
  312. }
  313. data-testid={dataTestId}
  314. variant={variant}
  315. >
  316. {children}
  317. </DropdownItem>
  318. </DropdownListItem>
  319. )
  320. }