dropdown-button.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import {
  2. useState,
  3. type ComponentProps,
  4. useCallback,
  5. type Dispatch,
  6. type SetStateAction,
  7. } from 'react'
  8. import { useTranslation } from 'react-i18next'
  9. import { Dropdown as BS3Dropdown, MenuItem } from 'react-bootstrap'
  10. import {
  11. Dropdown,
  12. DropdownItem,
  13. DropdownMenu,
  14. DropdownToggle,
  15. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  16. import { User } from '../../../../../../types/group-management/user'
  17. import useAsync from '@/shared/hooks/use-async'
  18. import { type FetchError, postJSON } from '@/infrastructure/fetch-json'
  19. import Icon from '@/shared/components/icon'
  20. import { GroupUserAlert } from '../../utils/types'
  21. import { useGroupMembersContext } from '../../context/group-members-context'
  22. import getMeta from '@/utils/meta'
  23. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  24. import MaterialIcon from '@/shared/components/material-icon'
  25. import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
  26. import { Spinner } from 'react-bootstrap-5'
  27. type resendInviteResponse = {
  28. success: boolean
  29. }
  30. type ManagedUserDropdownButtonProps = {
  31. user: User
  32. openOffboardingModalForUser: (user: User) => void
  33. openUnlinkUserModal: (user: User) => void
  34. groupId: string
  35. setGroupUserAlert: Dispatch<SetStateAction<GroupUserAlert>>
  36. }
  37. export default function DropdownButton({
  38. user,
  39. openOffboardingModalForUser,
  40. openUnlinkUserModal,
  41. groupId,
  42. setGroupUserAlert,
  43. }: ManagedUserDropdownButtonProps) {
  44. const { t } = useTranslation()
  45. const { removeMember } = useGroupMembersContext()
  46. const [isOpened, setIsOpened] = useState(false)
  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 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 => {
  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. setIsOpened(false)
  79. }
  80. } catch (err) {
  81. if ((err as FetchError)?.response?.status === 429) {
  82. setGroupUserAlert({
  83. variant: 'resendInviteTooManyRequests',
  84. email: user.email,
  85. })
  86. } else {
  87. setGroupUserAlert({
  88. variant: 'resendManagedUserInviteFailed',
  89. email: user.email,
  90. })
  91. }
  92. setIsOpened(false)
  93. }
  94. },
  95. [setGroupUserAlert, groupId, runResendManagedUserInviteAsync]
  96. )
  97. const handleResendLinkSSOInviteAsync = useCallback(
  98. async 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. setIsOpened(false)
  109. }
  110. } catch (err) {
  111. if ((err as FetchError)?.response?.status === 429) {
  112. setGroupUserAlert({
  113. variant: 'resendInviteTooManyRequests',
  114. email: user.email,
  115. })
  116. } else {
  117. setGroupUserAlert({
  118. variant: 'resendSSOLinkInviteFailed',
  119. email: user.email,
  120. })
  121. }
  122. setIsOpened(false)
  123. }
  124. },
  125. [setGroupUserAlert, groupId, runResendLinkSSOInviteAsync]
  126. )
  127. const handleResendGroupInvite = useCallback(
  128. async user => {
  129. try {
  130. await runResendGroupInviteAsync(
  131. postJSON(`/manage/groups/${groupId}/resendInvite/`, {
  132. body: {
  133. email: user.email,
  134. },
  135. })
  136. )
  137. setGroupUserAlert({
  138. variant: 'resendGroupInviteSuccess',
  139. email: user.email,
  140. })
  141. setIsOpened(false)
  142. } catch (err) {
  143. if ((err as FetchError)?.response?.status === 429) {
  144. setGroupUserAlert({
  145. variant: 'resendInviteTooManyRequests',
  146. email: user.email,
  147. })
  148. } else {
  149. setGroupUserAlert({
  150. variant: 'resendGroupInviteFailed',
  151. email: user.email,
  152. })
  153. }
  154. setIsOpened(false)
  155. }
  156. },
  157. [setGroupUserAlert, groupId, runResendGroupInviteAsync]
  158. )
  159. const onResendManagedUserInviteClick = () => {
  160. handleResendManagedUserInvite(user)
  161. }
  162. const onResendSSOLinkInviteClick = () => {
  163. handleResendLinkSSOInviteAsync(user)
  164. }
  165. const onResendGroupInviteClick = () => {
  166. handleResendGroupInvite(user)
  167. }
  168. const onDeleteUserClick = () => {
  169. openOffboardingModalForUser(user)
  170. }
  171. const onRemoveFromGroup = () => {
  172. removeMember(user)
  173. }
  174. const onUnlinkUserClick = () => {
  175. openUnlinkUserModal(user)
  176. }
  177. const buttons = []
  178. if (userPending) {
  179. buttons.push(
  180. <MenuItemButton
  181. onClick={onResendGroupInviteClick}
  182. key="resend-group-invite-action"
  183. isLoading={isResendingGroupInvite}
  184. data-testid="resend-group-invite-action"
  185. >
  186. {t('resend_group_invite')}
  187. </MenuItemButton>
  188. )
  189. }
  190. if (managedUsersActive && !isUserManaged && !userPending) {
  191. buttons.push(
  192. <MenuItemButton
  193. onClick={onResendManagedUserInviteClick}
  194. key="resend-managed-user-invite-action"
  195. isLoading={isResendingManagedUserInvite}
  196. data-testid="resend-managed-user-invite-action"
  197. >
  198. {t('resend_managed_user_invite')}
  199. </MenuItemButton>
  200. )
  201. }
  202. if (groupSSOActive && isGroupSSOLinked) {
  203. buttons.push(
  204. <MenuItemButton
  205. onClick={onUnlinkUserClick}
  206. key="unlink-user-action"
  207. data-testid="unlink-user-action"
  208. >
  209. {t('unlink_user')}
  210. </MenuItemButton>
  211. )
  212. }
  213. if (groupSSOActive && !isGroupSSOLinked && !userPending) {
  214. buttons.push(
  215. <MenuItemButton
  216. onClick={onResendSSOLinkInviteClick}
  217. key="resend-sso-link-invite-action"
  218. isLoading={isResendingSSOLinkInvite}
  219. data-testid="resend-sso-link-invite-action"
  220. >
  221. {t('resend_link_sso')}
  222. </MenuItemButton>
  223. )
  224. }
  225. if (isUserManaged && !user.isEntityAdmin) {
  226. buttons.push(
  227. <MenuItemButton
  228. className="delete-user-action"
  229. key="delete-user-action"
  230. data-testid="delete-user-action"
  231. onClick={onDeleteUserClick}
  232. >
  233. {t('delete_user')}
  234. </MenuItemButton>
  235. )
  236. } else if (!isUserManaged) {
  237. buttons.push(
  238. <MenuItemButton
  239. key="remove-user-action"
  240. data-testid="remove-user-action"
  241. onClick={onRemoveFromGroup}
  242. className="delete-user-action"
  243. variant="danger"
  244. >
  245. {t('remove_from_group')}
  246. </MenuItemButton>
  247. )
  248. }
  249. if (buttons.length === 0) {
  250. buttons.push(
  251. <BootstrapVersionSwitcher
  252. bs3={
  253. <MenuItem
  254. key="no-actions-available"
  255. data-testid="no-actions-available"
  256. >
  257. <span className="text-muted">{t('no_actions')}</span>
  258. </MenuItem>
  259. }
  260. bs5={
  261. <DropdownListItem>
  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. )
  274. }
  275. return (
  276. <BootstrapVersionSwitcher
  277. bs3={
  278. <div className="managed-user-actions">
  279. <BS3Dropdown
  280. id={`managed-user-dropdown-${user.email}`}
  281. open={isOpened}
  282. onToggle={open => setIsOpened(open)}
  283. >
  284. <BS3Dropdown.Toggle
  285. bsStyle={null}
  286. className="btn btn-link action-btn"
  287. noCaret
  288. >
  289. <Icon type="ellipsis-v" accessibilityLabel={t('actions')} />
  290. </BS3Dropdown.Toggle>
  291. <BS3Dropdown.Menu className="dropdown-menu-right managed-user-dropdown-menu">
  292. {buttons}
  293. </BS3Dropdown.Menu>
  294. </BS3Dropdown>
  295. </div>
  296. }
  297. bs5={
  298. <Dropdown align="end">
  299. <DropdownToggle
  300. id={`managed-user-dropdown-${user.email}`}
  301. bsPrefix="dropdown-table-button-toggle"
  302. >
  303. <MaterialIcon type="more_vert" accessibilityLabel={t('actions')} />
  304. </DropdownToggle>
  305. <DropdownMenu flip={false}>{buttons}</DropdownMenu>
  306. </Dropdown>
  307. }
  308. />
  309. )
  310. }
  311. type MenuItemButtonProps = {
  312. isLoading?: boolean
  313. 'data-testid'?: string
  314. } & Pick<ComponentProps<'button'>, 'children' | 'onClick' | 'className'> &
  315. Pick<ComponentProps<typeof DropdownItem>, 'variant'>
  316. function MenuItemButton({
  317. children,
  318. onClick,
  319. className,
  320. isLoading,
  321. variant,
  322. 'data-testid': dataTestId,
  323. }: MenuItemButtonProps) {
  324. return (
  325. <BootstrapVersionSwitcher
  326. bs3={
  327. <li role="presentation" className={className}>
  328. <button
  329. className="managed-user-menu-item-button"
  330. role="menuitem"
  331. onClick={onClick}
  332. data-testid={dataTestId}
  333. >
  334. {children}
  335. </button>
  336. </li>
  337. }
  338. bs5={
  339. <DropdownListItem>
  340. <DropdownItem
  341. as="button"
  342. tabIndex={-1}
  343. onClick={onClick}
  344. leadingIcon={
  345. isLoading ? (
  346. <Spinner
  347. animation="border"
  348. aria-hidden="true"
  349. size="sm"
  350. role="status"
  351. />
  352. ) : null
  353. }
  354. data-testid={dataTestId}
  355. variant={variant}
  356. >
  357. {children}
  358. </DropdownItem>
  359. </DropdownListItem>
  360. }
  361. />
  362. )
  363. }