dropdown-button.tsx 9.8 KB

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