inviteMembersModalview.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import type {ReactNode} from 'react';
  2. import {Fragment} from 'react';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import Alert from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import InviteButton from 'sentry/components/modals/inviteMembersModal/inviteButton';
  10. import InviteRowControl from 'sentry/components/modals/inviteMembersModal/inviteRowControl';
  11. import InviteStatusMessage from 'sentry/components/modals/inviteMembersModal/inviteStatusMessage';
  12. import type {
  13. InviteRow,
  14. InviteStatus,
  15. NormalizedInvite,
  16. } from 'sentry/components/modals/inviteMembersModal/types';
  17. import {ORG_ROLES} from 'sentry/constants';
  18. import {IconAdd} from 'sentry/icons';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import type {Member} from 'sentry/types';
  22. interface Props {
  23. Footer: ModalRenderProps['Footer'];
  24. addInviteRow: () => void;
  25. canSend: boolean;
  26. closeModal: ModalRenderProps['closeModal'];
  27. complete: boolean;
  28. headerInfo: ReactNode;
  29. inviteStatus: InviteStatus;
  30. invites: NormalizedInvite[];
  31. member: Member | undefined;
  32. pendingInvites: InviteRow[];
  33. removeInviteRow: (index: number) => void;
  34. reset: () => void;
  35. sendInvites: () => void;
  36. sendingInvites: boolean;
  37. setEmails: (emails: string[], index: number) => void;
  38. setRole: (role: string, index: number) => void;
  39. setTeams: (teams: string[], index: number) => void;
  40. willInvite: boolean;
  41. error?: string;
  42. }
  43. export default function InviteMembersModalView({
  44. addInviteRow,
  45. canSend,
  46. closeModal,
  47. complete,
  48. Footer,
  49. headerInfo,
  50. invites,
  51. inviteStatus,
  52. member,
  53. pendingInvites,
  54. removeInviteRow,
  55. reset,
  56. sendingInvites,
  57. sendInvites,
  58. setEmails,
  59. setRole,
  60. setTeams,
  61. willInvite,
  62. error,
  63. }: Props) {
  64. const disableInputs = sendingInvites || complete;
  65. const inviteEmails = invites.map(inv => inv.email);
  66. const hasDuplicateEmails = inviteEmails.length !== new Set(inviteEmails).size;
  67. const isValidInvites = invites.length > 0 && !hasDuplicateEmails;
  68. const errorAlert = error ? (
  69. <Alert type="error" showIcon>
  70. {error}
  71. </Alert>
  72. ) : null;
  73. return (
  74. <Fragment>
  75. {errorAlert}
  76. <Heading>{t('Invite New Members')}</Heading>
  77. {willInvite ? (
  78. <Subtext>{t('Invite new members by email to join your organization.')}</Subtext>
  79. ) : (
  80. <Alert type="warning" showIcon>
  81. {t(
  82. 'You can’t invite users directly, but we’ll forward your request to an org owner or manager for approval.'
  83. )}
  84. </Alert>
  85. )}
  86. {headerInfo}
  87. <InviteeHeadings>
  88. <div>{t('Email addresses')}</div>
  89. <div>{t('Role')}</div>
  90. <div>{t('Add to team')}</div>
  91. <div />
  92. </InviteeHeadings>
  93. <Rows>
  94. {pendingInvites.map(({emails, role, teams}, i) => (
  95. <StyledInviteRow
  96. key={i}
  97. disabled={disableInputs}
  98. emails={[...emails]}
  99. role={role}
  100. teams={[...teams]}
  101. roleOptions={member?.orgRoleList ?? ORG_ROLES}
  102. roleDisabledUnallowed={willInvite}
  103. inviteStatus={inviteStatus}
  104. onRemove={() => removeInviteRow(i)}
  105. onChangeEmails={opts => setEmails(opts?.map(v => v.value) ?? [], i)}
  106. onChangeRole={value => setRole(value?.value, i)}
  107. onChangeTeams={opts => setTeams(opts ? opts.map(v => v.value) : [], i)}
  108. disableRemove={disableInputs || pendingInvites.length === 1}
  109. />
  110. ))}
  111. </Rows>
  112. <AddButton
  113. disabled={disableInputs}
  114. size="sm"
  115. borderless
  116. onClick={addInviteRow}
  117. icon={<IconAdd isCircled />}
  118. >
  119. {t('Add another')}
  120. </AddButton>
  121. <Footer>
  122. <FooterContent>
  123. <div>
  124. <InviteStatusMessage
  125. complete={complete}
  126. hasDuplicateEmails={hasDuplicateEmails}
  127. inviteStatus={inviteStatus}
  128. sendingInvites={sendingInvites}
  129. willInvite={willInvite}
  130. />
  131. </div>
  132. <ButtonBar gap={1}>
  133. {complete ? (
  134. <Fragment>
  135. <Button data-test-id="send-more" size="sm" onClick={reset}>
  136. {t('Send more invites')}
  137. </Button>
  138. <Button
  139. data-test-id="close"
  140. priority="primary"
  141. size="sm"
  142. onClick={closeModal}
  143. >
  144. {t('Close')}
  145. </Button>
  146. </Fragment>
  147. ) : (
  148. <Fragment>
  149. <Button
  150. data-test-id="cancel"
  151. size="sm"
  152. onClick={closeModal}
  153. disabled={disableInputs}
  154. >
  155. {t('Cancel')}
  156. </Button>
  157. <InviteButton
  158. invites={invites}
  159. willInvite={willInvite}
  160. size="sm"
  161. data-test-id="send-invites"
  162. priority="primary"
  163. disabled={!canSend || !isValidInvites || disableInputs}
  164. onClick={sendInvites}
  165. />
  166. </Fragment>
  167. )}
  168. </ButtonBar>
  169. </FooterContent>
  170. </Footer>
  171. </Fragment>
  172. );
  173. }
  174. const Heading = styled('h1')`
  175. font-weight: 400;
  176. font-size: ${p => p.theme.headerFontSize};
  177. margin-top: 0;
  178. margin-bottom: ${space(0.75)};
  179. `;
  180. const Subtext = styled('p')`
  181. color: ${p => p.theme.subText};
  182. margin-bottom: ${space(3)};
  183. `;
  184. const inviteRowGrid = css`
  185. display: grid;
  186. gap: ${space(1.5)};
  187. grid-template-columns: 3fr 180px 2fr 0.5fr;
  188. align-items: start;
  189. `;
  190. const InviteeHeadings = styled('div')`
  191. ${inviteRowGrid};
  192. margin-bottom: ${space(1)};
  193. font-weight: 600;
  194. text-transform: uppercase;
  195. font-size: ${p => p.theme.fontSizeSmall};
  196. `;
  197. const Rows = styled('ul')`
  198. list-style: none;
  199. padding: 0;
  200. margin: 0;
  201. `;
  202. const StyledInviteRow = styled(InviteRowControl)`
  203. ${inviteRowGrid};
  204. margin-bottom: ${space(1.5)};
  205. `;
  206. const AddButton = styled(Button)`
  207. margin-top: ${space(3)};
  208. `;
  209. const FooterContent = styled('div')`
  210. display: flex;
  211. gap: ${space(1)};
  212. align-items: center;
  213. justify-content: space-between;
  214. flex: 1;
  215. `;