invite.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { useCallback } from 'react'
  2. import { useShareProjectContext } from './share-project-modal'
  3. import { useTranslation } from 'react-i18next'
  4. import MemberPrivileges from './member-privileges'
  5. import { resendInvite, revokeInvite } from '../utils/api'
  6. import { useProjectContext } from '@/shared/context/project-context'
  7. import { sendMB } from '@/infrastructure/event-tracking'
  8. import OLRow from '@/shared/components/ol/ol-row'
  9. import OLCol from '@/shared/components/ol/ol-col'
  10. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  11. import OLButton from '@/shared/components/ol/ol-button'
  12. import OLBadge from '@/shared/components/ol/ol-badge'
  13. import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
  14. import {
  15. Dropdown,
  16. DropdownMenu,
  17. DropdownToggle,
  18. } from '@/shared/components/dropdown/dropdown-menu'
  19. import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row'
  20. import MaterialIcon from '@/shared/components/material-icon'
  21. import { ProjectMember } from '@/shared/context/types/project-metadata'
  22. import { useFeatureFlag } from '@/shared/context/split-test-context'
  23. export default function Invite({
  24. invite,
  25. isProjectOwner,
  26. }: {
  27. invite: ProjectMember
  28. isProjectOwner: boolean
  29. }) {
  30. const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
  31. const { t } = useTranslation()
  32. return isSharingUpdatesEnabled ? (
  33. <ShareProjectModalRow>
  34. <div className="d-inline-flex align-items-center h5 m-0 gap-2">
  35. <MaterialIcon type="person" unfilled />
  36. <div className="px-2">{invite.email}</div>
  37. <OLBadge bg="light" text="dark">
  38. {t('pending_invite')}
  39. </OLBadge>
  40. </div>
  41. {isProjectOwner ? (
  42. <Dropdown align="end" onSelect={() => {}}>
  43. <DropdownToggle
  44. variant="ghost"
  45. className="d-flex align-items-center gap-2 no-default-caret"
  46. >
  47. <MemberPrivileges privileges={invite.privileges} />
  48. <MaterialIcon type="keyboard_arrow_down" />
  49. </DropdownToggle>
  50. <DropdownMenu>
  51. <ResendInvite invite={invite} />
  52. <RevokeInvite invite={invite} />
  53. </DropdownMenu>
  54. </Dropdown>
  55. ) : (
  56. <div className="h5 m-0 px-4 fw-semibold">
  57. <div className="form-control-plaintext border-0">
  58. <MemberPrivileges privileges={invite.privileges} />
  59. </div>
  60. </div>
  61. )}
  62. </ShareProjectModalRow>
  63. ) : (
  64. <OLRow className="project-invite">
  65. <OLCol xs={8}>
  66. <div>{invite.email}</div>
  67. <div className="small">
  68. {t('invite_not_accepted')}
  69. .&nbsp;
  70. {isProjectOwner && <ResendInvite invite={invite} />}
  71. </div>
  72. </OLCol>
  73. <OLCol xs={3} className="text-end">
  74. <MemberPrivileges privileges={invite.privileges} />
  75. </OLCol>
  76. {isProjectOwner && (
  77. <OLCol xs={1} className="text-center">
  78. <RevokeInvite invite={invite} />
  79. </OLCol>
  80. )}
  81. </OLRow>
  82. )
  83. }
  84. function ResendInvite({ invite }: { invite: ProjectMember }) {
  85. const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
  86. const { t } = useTranslation()
  87. const { monitorRequest, setError, inFlight, setSuccessActionMessage } =
  88. useShareProjectContext()
  89. const { projectId } = useProjectContext()
  90. // const buttonRef = useRef(null)
  91. //
  92. const handleClick = useCallback(
  93. () =>
  94. monitorRequest(() => resendInvite(projectId, invite))
  95. .then(() => setSuccessActionMessage(t('invite_resent')))
  96. .catch(error => {
  97. if (error?.response?.status === 404) {
  98. setError('invite_expired')
  99. }
  100. if (error?.response?.status === 429) {
  101. setError('invite_resend_limit_hit')
  102. }
  103. })
  104. .finally(() => {
  105. // NOTE: disabled as react-bootstrap v0.33.1 isn't forwarding the ref to the `button`
  106. // if (buttonRef.current) {
  107. // buttonRef.current.blur()
  108. // }
  109. if (document.activeElement) {
  110. ;(document.activeElement as HTMLElement).blur()
  111. }
  112. }),
  113. [invite, monitorRequest, projectId, setError, setSuccessActionMessage, t]
  114. )
  115. return isSharingUpdatesEnabled ? (
  116. <OLDropdownMenuItem
  117. as="button"
  118. leadingIcon={<MaterialIcon type="mail" unfilled />}
  119. onClick={handleClick}
  120. disabled={inFlight}
  121. >
  122. {t('resend_invite')}
  123. </OLDropdownMenuItem>
  124. ) : (
  125. <OLButton
  126. variant="link"
  127. className="btn-inline-link"
  128. onClick={handleClick}
  129. disabled={inFlight}
  130. // ref={buttonRef}
  131. >
  132. {t('resend')}
  133. </OLButton>
  134. )
  135. }
  136. function RevokeInvite({ invite }: { invite: ProjectMember }) {
  137. const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
  138. const { t } = useTranslation()
  139. const { monitorRequest, setSuccessActionMessage } = useShareProjectContext()
  140. const { projectId, project, updateProject } = useProjectContext()
  141. const { invites, members } = project || {}
  142. function handleClick(event: React.MouseEvent) {
  143. event.preventDefault()
  144. monitorRequest(() => revokeInvite(projectId, invite)).then(() => {
  145. const updatedInvites =
  146. invites?.filter(existing => existing !== invite) || []
  147. updateProject({
  148. invites: updatedInvites,
  149. })
  150. sendMB('collaborator-invite-revoked', {
  151. project_id: projectId,
  152. current_invites_amount: updatedInvites.length,
  153. current_collaborators_amount: members?.length || 0,
  154. })
  155. setSuccessActionMessage(t('invite_revoked'))
  156. })
  157. }
  158. return isSharingUpdatesEnabled ? (
  159. <OLDropdownMenuItem
  160. as="button"
  161. leadingIcon={<MaterialIcon type="block" unfilled />}
  162. variant="danger"
  163. onClick={handleClick}
  164. >
  165. {t('revoke_invite')}
  166. </OLDropdownMenuItem>
  167. ) : (
  168. <OLTooltip
  169. id="revoke-invite"
  170. description={t('revoke_invite')}
  171. overlayProps={{ placement: 'bottom' }}
  172. >
  173. <OLButton
  174. variant="link"
  175. onClick={handleClick}
  176. aria-label={t('revoke')}
  177. className="btn-inline-link text-decoration-none"
  178. >
  179. <MaterialIcon type="clear" />
  180. </OLButton>
  181. </OLTooltip>
  182. )
  183. }