123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import type {ReactNode} from 'react';
- import {Fragment} from 'react';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import type {ModalRenderProps} from 'sentry/actionCreators/modal';
- import Alert from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import InviteButton from 'sentry/components/modals/inviteMembersModal/inviteButton';
- import InviteRowControl from 'sentry/components/modals/inviteMembersModal/inviteRowControl';
- import InviteStatusMessage from 'sentry/components/modals/inviteMembersModal/inviteStatusMessage';
- import type {
- InviteRow,
- InviteStatus,
- NormalizedInvite,
- } from 'sentry/components/modals/inviteMembersModal/types';
- import {ORG_ROLES} from 'sentry/constants';
- import {IconAdd} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Member} from 'sentry/types';
- interface Props {
- Footer: ModalRenderProps['Footer'];
- addInviteRow: () => void;
- canSend: boolean;
- closeModal: ModalRenderProps['closeModal'];
- complete: boolean;
- headerInfo: ReactNode;
- inviteStatus: InviteStatus;
- invites: NormalizedInvite[];
- member: Member | undefined;
- pendingInvites: InviteRow[];
- removeInviteRow: (index: number) => void;
- reset: () => void;
- sendInvites: () => void;
- sendingInvites: boolean;
- setEmails: (emails: string[], index: number) => void;
- setRole: (role: string, index: number) => void;
- setTeams: (teams: string[], index: number) => void;
- willInvite: boolean;
- error?: string;
- }
- export default function InviteMembersModalView({
- addInviteRow,
- canSend,
- closeModal,
- complete,
- Footer,
- headerInfo,
- invites,
- inviteStatus,
- member,
- pendingInvites,
- removeInviteRow,
- reset,
- sendingInvites,
- sendInvites,
- setEmails,
- setRole,
- setTeams,
- willInvite,
- error,
- }: Props) {
- const disableInputs = sendingInvites || complete;
- const inviteEmails = invites.map(inv => inv.email);
- const hasDuplicateEmails = inviteEmails.length !== new Set(inviteEmails).size;
- const isValidInvites = invites.length > 0 && !hasDuplicateEmails;
- const errorAlert = error ? (
- <Alert type="error" showIcon>
- {error}
- </Alert>
- ) : null;
- return (
- <Fragment>
- {errorAlert}
- <Heading>{t('Invite New Members')}</Heading>
- {willInvite ? (
- <Subtext>{t('Invite new members by email to join your organization.')}</Subtext>
- ) : (
- <Alert type="warning" showIcon>
- {t(
- 'You can’t invite users directly, but we’ll forward your request to an org owner or manager for approval.'
- )}
- </Alert>
- )}
- {headerInfo}
- <InviteeHeadings>
- <div>{t('Email addresses')}</div>
- <div>{t('Role')}</div>
- <div>{t('Add to team')}</div>
- <div />
- </InviteeHeadings>
- <Rows>
- {pendingInvites.map(({emails, role, teams}, i) => (
- <StyledInviteRow
- key={i}
- disabled={disableInputs}
- emails={[...emails]}
- role={role}
- teams={[...teams]}
- roleOptions={member?.orgRoleList ?? ORG_ROLES}
- roleDisabledUnallowed={willInvite}
- inviteStatus={inviteStatus}
- onRemove={() => removeInviteRow(i)}
- onChangeEmails={opts => setEmails(opts?.map(v => v.value) ?? [], i)}
- onChangeRole={value => setRole(value?.value, i)}
- onChangeTeams={opts => setTeams(opts ? opts.map(v => v.value) : [], i)}
- disableRemove={disableInputs || pendingInvites.length === 1}
- />
- ))}
- </Rows>
- <AddButton
- disabled={disableInputs}
- size="sm"
- borderless
- onClick={addInviteRow}
- icon={<IconAdd isCircled />}
- >
- {t('Add another')}
- </AddButton>
- <Footer>
- <FooterContent>
- <div>
- <InviteStatusMessage
- complete={complete}
- hasDuplicateEmails={hasDuplicateEmails}
- inviteStatus={inviteStatus}
- sendingInvites={sendingInvites}
- willInvite={willInvite}
- />
- </div>
- <ButtonBar gap={1}>
- {complete ? (
- <Fragment>
- <Button data-test-id="send-more" size="sm" onClick={reset}>
- {t('Send more invites')}
- </Button>
- <Button
- data-test-id="close"
- priority="primary"
- size="sm"
- onClick={closeModal}
- >
- {t('Close')}
- </Button>
- </Fragment>
- ) : (
- <Fragment>
- <Button
- data-test-id="cancel"
- size="sm"
- onClick={closeModal}
- disabled={disableInputs}
- >
- {t('Cancel')}
- </Button>
- <InviteButton
- invites={invites}
- willInvite={willInvite}
- size="sm"
- data-test-id="send-invites"
- priority="primary"
- disabled={!canSend || !isValidInvites || disableInputs}
- onClick={sendInvites}
- />
- </Fragment>
- )}
- </ButtonBar>
- </FooterContent>
- </Footer>
- </Fragment>
- );
- }
- const Heading = styled('h1')`
- font-weight: 400;
- font-size: ${p => p.theme.headerFontSize};
- margin-top: 0;
- margin-bottom: ${space(0.75)};
- `;
- const Subtext = styled('p')`
- color: ${p => p.theme.subText};
- margin-bottom: ${space(3)};
- `;
- const inviteRowGrid = css`
- display: grid;
- gap: ${space(1.5)};
- grid-template-columns: 3fr 180px 2fr 0.5fr;
- align-items: start;
- `;
- const InviteeHeadings = styled('div')`
- ${inviteRowGrid};
- margin-bottom: ${space(1)};
- font-weight: 600;
- text-transform: uppercase;
- font-size: ${p => p.theme.fontSizeSmall};
- `;
- const Rows = styled('ul')`
- list-style: none;
- padding: 0;
- margin: 0;
- `;
- const StyledInviteRow = styled(InviteRowControl)`
- ${inviteRowGrid};
- margin-bottom: ${space(1.5)};
- `;
- const AddButton = styled(Button)`
- margin-top: ${space(3)};
- `;
- const FooterContent = styled('div')`
- display: flex;
- gap: ${space(1)};
- align-items: center;
- justify-content: space-between;
- flex: 1;
- `;
|