Skip to content

Commit

Permalink
fix: rules for invalid emails in group invite modal (#1227)
Browse files Browse the repository at this point in the history
test: changed invite behavior

fix: Allow inviting learners when error/duplicate entries exist

test: Test invalid/duplicate email case
  • Loading branch information
marlonkeating authored Jun 4, 2024
1 parent b438ffd commit 85e2cdf
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 49 deletions.
15 changes: 9 additions & 6 deletions src/components/learner-credit-management/cards/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,8 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
// Validate the email address
if (!isEmail(email)) {
invalidEmails.push(email);
}

// Check for duplicates (case-insensitive)
if (lowerCasedEmails.includes(lowerCasedEmail)) {
} else if (lowerCasedEmails.includes(lowerCasedEmail)) {
// Check for duplicates (case-insensitive)
duplicateEmails.push(email);
} else {
// Add to list of lower-cased emails already handled
Expand All @@ -144,7 +142,7 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
});

const isValidInput = invalidEmails.length === 0 && learnerEmailsCount < MAX_EMAIL_ENTRY_LIMIT;
const canInvite = learnerEmailsCount > 0 && learnerEmailsCount < MAX_EMAIL_ENTRY_LIMIT && isValidInput;
const canInvite = lowerCasedEmails.length > 0 && learnerEmailsCount < MAX_EMAIL_ENTRY_LIMIT;

const ensureValidationErrorObjectExists = () => {
if (!validationError) {
Expand All @@ -161,7 +159,11 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
}
if (invalidEmails.length > 0) {
validationError.reason = 'invalid_email';
validationError.message = `${invalidEmails[0]} is not a valid email.`;
if (invalidEmails.length === 1) {
validationError.message = `${invalidEmails[0]} is not a valid email.`;
} else {
validationError.message = `${invalidEmails[0]} and ${invalidEmails.length - 1} other email addresses are not valid.`;
}
}
} else if (duplicateEmails.length > 0) {
let message = `${duplicateEmails[0]} was entered more than once.`;
Expand All @@ -177,6 +179,7 @@ export const isInviteEmailAddressesInputValueValid = ({ learnerEmails }) => {
canInvite,
lowerCasedEmails,
duplicateEmails,
invalidEmails,
isValidInput,
validationError,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Card, Stack } from '@openedx/paragon';
import isEmpty from 'lodash/isEmpty';

import InviteModalSummaryEmptyState from './InviteModalSummaryEmptyState';
import InviteModalSummaryLearnerList from './InviteModalSummaryLearnerList';
import InviteModalSummaryErrorState from './InviteModalSummaryErrorState';
import InviteModalSummaryDuplicate from './InviteModalSummaryDuplicate';

const InviteModalSummaryContents = ({
hasLearnerEmails,
learnerEmails,
hasInputValidationError,
}) => {
if (hasLearnerEmails) {
return (
<InviteModalSummaryLearnerList
learnerEmails={learnerEmails}
/>
);
}
if (hasInputValidationError) {
return <InviteModalSummaryErrorState />;
}
return <InviteModalSummaryEmptyState />;
};

const InviteModalSummary = ({
memberInviteMetadata,
}) => {
Expand All @@ -34,7 +17,40 @@ const InviteModalSummary = ({
lowerCasedEmails,
duplicateEmails,
} = memberInviteMetadata;
const hasLearnerEmails = lowerCasedEmails?.length > 0 && isValidInput;
const renderCard = (contents, showErrorHighlight) => (
<Stack gap={2.5} className="mb-4">
<Card
className={classNames(
'invite-modal-summary-card rounded-0 shadow-none',
{ invalid: showErrorHighlight && !isValidInput },
)}
>
<Card.Section>
{contents}
</Card.Section>
</Card>
</Stack>
);

const hasLearnerEmails = lowerCasedEmails?.length > 0;
let cardSections = [];
if (hasLearnerEmails) {
cardSections = cardSections.concat(
renderCard(<InviteModalSummaryLearnerList learnerEmails={lowerCasedEmails} />),
);
}

if (!isValidInput) {
cardSections = cardSections.concat(
renderCard(<InviteModalSummaryErrorState />, true),
);
}

if (isEmpty(cardSections)) {
cardSections = cardSections.concat(
renderCard(<InviteModalSummaryEmptyState />),
);
}

let summaryHeading = 'Summary';
if (hasLearnerEmails) {
Expand All @@ -43,33 +59,12 @@ const InviteModalSummary = ({
return (
<>
<h5 className="mb-4">{summaryHeading}</h5>
<Stack gap={2.5}>
<Card
className={classNames(
'invite-modal-summary-card rounded-0 shadow-none',
{ invalid: !isValidInput },
)}
>
<Card.Section>
<InviteModalSummaryContents
learnerEmails={lowerCasedEmails}
hasLearnerEmails={hasLearnerEmails}
hasInputValidationError={!isValidInput}
/>
</Card.Section>
</Card>
{duplicateEmails?.length > 0 && <InviteModalSummaryDuplicate />}
</Stack>
{cardSections}
{duplicateEmails?.length > 0 && <InviteModalSummaryDuplicate />}
</>
);
};

InviteModalSummaryContents.propTypes = {
hasLearnerEmails: PropTypes.bool.isRequired,
learnerEmails: PropTypes.arrayOf(PropTypes.string),
hasInputValidationError: PropTypes.bool.isRequired,
};

InviteModalSummary.propTypes = {
memberInviteMetadata: PropTypes.shape({
isValidInput: PropTypes.bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ describe('<InviteMemberModal />', () => {
});
});
});
it('throws up errors for incorrectly formatted emails', async () => {
it('throws up errors for incorrectly formatted email', async () => {
render(<InviteModalWrapper />);
const textareaInputLabel = screen.getByLabelText('Member email addresses');
const textareaInput = textareaInputLabel.closest('textarea');
Expand All @@ -243,6 +243,38 @@ describe('<InviteMemberModal />', () => {
expect(inviteButton).toBeDisabled();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
});
it('throws up errors for incorrectly formatted emails', async () => {
render(<InviteModalWrapper />);
const textareaInputLabel = screen.getByLabelText('Member email addresses');
const textareaInput = textareaInputLabel.closest('textarea');
userEvent.type(textareaInput, 'sillygoosethisisntanemail');
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, 'neitheristhis');
await waitFor(() => {
expect(screen.getByText('Members can\'t be invited as entered.')).toBeInTheDocument();
expect(screen.getByText('Please check your member emails and try again.')).toBeInTheDocument();
expect(screen.getByText('sillygoosethisisntanemail and 1 other email addresses are not valid.')).toBeInTheDocument();
const inviteButton = screen.getByRole('button', { name: 'Invite' });
expect(inviteButton).toBeDisabled();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
});
it('throws up errors for incorrectly formatted emails but allows inviting valid email', async () => {
render(<InviteModalWrapper />);
const textareaInputLabel = screen.getByLabelText('Member email addresses');
const textareaInput = textareaInputLabel.closest('textarea');
userEvent.type(textareaInput, 'sillygoosethisisntanemail');
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, 'neitheristhis');
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, '[email protected]');
await waitFor(() => {
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('Members can\'t be invited as entered.')).toBeInTheDocument();
expect(screen.getByText('sillygoosethisisntanemail and 1 other email addresses are not valid.')).toBeInTheDocument();
const inviteButton = screen.getByRole('button', { name: 'Invite' });
expect(inviteButton).not.toBeDisabled();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
});
it('throws up warning for duplicated emails', async () => {
render(<InviteModalWrapper />);
const textareaInputLabel = screen.getByLabelText('Member email addresses');
Expand All @@ -251,10 +283,29 @@ describe('<InviteMemberModal />', () => {
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, '[email protected]');
await waitFor(() => {
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('[email protected] was entered more than once.')).toBeInTheDocument();
expect(screen.getByText('Only 1 invite per email address will be sent.')).toBeInTheDocument();
const inviteButton = screen.getByRole('button', { name: 'Invite' });
expect(inviteButton).not.toBeDisabled();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
});
it('throws up warning for invalid/duplicated emails', async () => {
render(<InviteModalWrapper />);
const textareaInputLabel = screen.getByLabelText('Member email addresses');
const textareaInput = textareaInputLabel.closest('textarea');
userEvent.type(textareaInput, '[email protected]');
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, '[email protected]');
userEvent.type(textareaInput, '{enter}');
userEvent.type(textareaInput, 'sillygoosethisisntanemail');
await waitFor(() => {
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('sillygoosethisisntanemail is not a valid email.')).toBeInTheDocument();
expect(screen.getByText('Members can\'t be invited as entered.')).toBeInTheDocument();
expect(screen.getByText('Only 1 invite per email address will be sent.')).toBeInTheDocument();
const inviteButton = screen.getByRole('button', { name: 'Invite' });
expect(inviteButton).not.toBeDisabled();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
});
});

0 comments on commit 85e2cdf

Please sign in to comment.