Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat send email students course #1

Closed
wants to merge 11 commits into from
1 change: 0 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<title>Communications | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
</head>
<body>
<div id="root"></div>
Expand Down
43 changes: 43 additions & 0 deletions src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function BulkEmailForm(props) {
const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false);
const [isScheduled, toggleScheduled] = useState(false);
const isMobile = useMobileResponsive();
const [learnersEmailList, setLearnersEmailList] = useState([]);

/**
* Since we are working with both an old and new API endpoint, the body for the POST
Expand All @@ -69,12 +70,14 @@ function BulkEmailForm(props) {
* @returns formatted Data
*/
const formatDataForFormAction = (action) => {
const individualLearnersList = learnersEmailList.map(({ email }) => email);
if (action === FORM_ACTIONS.POST) {
const emailData = new FormData();
emailData.append('action', 'send');
emailData.append('send_to', JSON.stringify(editor.emailRecipients));
emailData.append('subject', editor.emailSubject);
emailData.append('message', editor.emailBody);
emailData.append('individual_learners_emails', JSON.stringify(individualLearnersList));
if (isScheduled) {
emailData.append('schedule', new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toISOString());
}
Expand All @@ -87,6 +90,7 @@ function BulkEmailForm(props) {
subject: editor.emailSubject,
message: editor.emailBody,
id: editor.emailId,
individual_learners_emails: individualLearnersList,
},
schedule: isScheduled ? new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toISOString() : null,
};
Expand Down Expand Up @@ -125,6 +129,8 @@ function BulkEmailForm(props) {
dispatch(addRecipient(event.target.value));
// if "All Learners" is checked then we want to remove any cohorts, verified learners, and audit learners
if (event.target.value === 'learners') {
// Clean the emails list when select "All Learners"
setLearnersEmailList([]);
editor.emailRecipients.forEach(recipient => {
if (/^cohort/.test(recipient) || /^track/.test(recipient)) {
dispatch(removeRecipient(recipient));
Expand All @@ -136,6 +142,20 @@ function BulkEmailForm(props) {
}
};

// When the user clicks the button "Add email" gets the email and an id
const handleEmailLearnersSelected = (emailSelected) => {
const isEmailAdded = learnersEmailList.some(({ email }) => email === emailSelected.email);
if (!isEmailAdded) {
setLearnersEmailList([...learnersEmailList, emailSelected]);
}
};

// To delete an email from learners list, that list is on the bottom of the input autocomplete
const handleDeleteEmailLearnerSelected = (idDelete) => {
const setLearnersEmailListUpdated = learnersEmailList.filter(({ id }) => id !== idDelete);
setLearnersEmailList(setLearnersEmailListUpdated);
};

const validateDateTime = (date, time) => {
if (isScheduled) {
const now = new Date();
Expand Down Expand Up @@ -209,6 +229,26 @@ function BulkEmailForm(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isScheduled, editor.editMode, editor.isLoading, editor.errorRetrievingData, editor.formComplete]);

/*
This will be checking if there are emails added to the learnersEmailList state
if so, we will delete emailRecipients "learners" because that is for all learners
if not, we will delete the individual-learners from emailRecipients because of we won't use the emails
*/
useEffect(() => {
const hasLearners = learnersEmailList.length > 0;
const hasIndividualLearners = editor.emailRecipients.includes('individual-learners');
const hasLearnersGroup = editor.emailRecipients.includes('learners');

if (hasLearners && !hasIndividualLearners) {
dispatch(addRecipient('individual-learners'));
if (hasLearnersGroup) {
dispatch(removeRecipient('learners'));
}
} else if (!hasLearners && hasIndividualLearners) {
dispatch(removeRecipient('individual-learners'));
}
}, [dispatch, editor.emailRecipients, learnersEmailList]);

const AlertMessage = () => (
<>
<p>{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject: editor.emailSubject })}</p>
Expand Down Expand Up @@ -272,6 +312,9 @@ function BulkEmailForm(props) {
handleCheckboxes={onRecipientChange}
additionalCohorts={cohorts}
isValid={emailFormValidation.recipients}
learnersEmailList={learnersEmailList}
handleEmailLearnersSelected={handleEmailLearnersSelected}
handleDeleteEmailLearnerSelected={handleDeleteEmailLearnerSelected}
/>
<Form.Group controlId="emailSubject">
<Form.Label className="h3 text-primary-500">{intl.formatMessage(messages.bulkEmailSubjectLabel)}</Form.Label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import EmailList from './components/EmailList';
import './bulkEmailRecepient.scss';

const DEFAULT_GROUPS = {
Expand All @@ -11,10 +12,14 @@ const DEFAULT_GROUPS = {
ALL_LEARNERS: 'learners',
VERIFIED: 'track:verified',
AUDIT: 'track:audit',
INDIVIDUAL_LEARNERS: 'individual-learners',
};

export default function BulkEmailRecipient(props) {
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
const {
handleCheckboxes, selectedGroups, additionalCohorts, handleEmailLearnersSelected,
learnersEmailList, handleDeleteEmailLearnerSelected,
} = props;
return (
<Form.Group>
<Form.Label>
Expand Down Expand Up @@ -103,6 +108,12 @@ export default function BulkEmailRecipient(props) {
description="A selectable choice from a list of potential email recipients"
/>
</Form.Checkbox>

<EmailList
handleEmailLearnersSelected={handleEmailLearnersSelected}
learnersEmailList={learnersEmailList}
handleDeleteEmailLearnerSelected={handleDeleteEmailLearnerSelected}
/>
</Form.CheckboxSet>
{!props.isValid && (
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
Expand All @@ -120,11 +131,22 @@ export default function BulkEmailRecipient(props) {
BulkEmailRecipient.defaultProps = {
isValid: true,
additionalCohorts: [],
handleEmailLearnersSelected: () => {},
handleDeleteEmailLearnerSelected: () => {},
learnersEmailList: [],
};

BulkEmailRecipient.propTypes = {
selectedGroups: PropTypes.arrayOf(PropTypes.string).isRequired,
handleCheckboxes: PropTypes.func.isRequired,
isValid: PropTypes.bool,
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
handleEmailLearnersSelected: PropTypes.func,
handleDeleteEmailLearnerSelected: PropTypes.func,
learnersEmailList: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
email: PropTypes.string.isRequired,
}),
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useState } from 'react';
import {
Form, Chip, Container, Button,
} from '@edx/paragon';
import { Person, Close, SupervisedUserCircle } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../../../messages';

import { isValidEmail } from './utils';
import './styles.scss';

function EmailList(props) {
const {
handleEmailLearnersSelected, learnersEmailList, handleDeleteEmailLearnerSelected,
intl,
} = props;

const [emailListAdded, setEmailListAdded] = useState(false);
const [emailInputValue, setEmailInputValue] = useState('');
const [invalidEmailError, setInvalidEmailError] = useState(false);

const handleDeleteEmailSelected = (id) => {
if (handleDeleteEmailLearnerSelected) {
handleDeleteEmailLearnerSelected(id);
}
};

const handleChangeEmailInput = ({ target: { value } }) => {
setEmailInputValue(value);
if (isValidEmail(value)) {
setInvalidEmailError(false);
}
};

const handleAddEmail = () => {
if (!emailInputValue.length) { return; }
if (!emailListAdded) {
setEmailListAdded(true);
}
if (isValidEmail(emailInputValue)) {
const emailFormatted = emailInputValue.toLocaleLowerCase();
const currentDateTime = new Date().getTime();
const data = { id: currentDateTime, email: emailFormatted };
handleEmailLearnersSelected(data);
setInvalidEmailError(false);
setEmailInputValue('');
} else {
setInvalidEmailError(true);
}
};

return (
<Container className="col-12 my-3">
<Form.Group controlId="emailIndividualLearners">
<Form.Label className="mt-3" data-testid="learners-email-input-label">
{intl.formatMessage(messages.bulkEmailTaskEmailLearnersInputLabel)}
</Form.Label>
<Container className="row">
<Form.Control
data-testid="learners-email-input"
name="emailSubject"
className="w-lg-50"
onChange={handleChangeEmailInput}
value={emailInputValue}
placeholder={intl.formatMessage(
messages.bulkEmailTaskEmailLearnersInputPlaceholder,
)}
/>
<Button
data-testid="learners-email-add-button"
variant="primary"
iconAfter={SupervisedUserCircle}
className="mb-2 mb-sm-0"
onClick={handleAddEmail}
>
{intl.formatMessage(
messages.bulkEmailTaskEmailLearnersAddEmailButton,
)}
</Button>
</Container>
{invalidEmailError && (
<Form.Control.Feedback className="px-3 mt-1" hasIcon type="invalid">
{intl.formatMessage(
messages.bulkEmailTaskEmailLearnersErrorMessage,
)}
</Form.Control.Feedback>
)}
</Form.Group>
<Container className="email-list">
<Form.Label className="col-12" data-testid="learners-email-list-label">
{intl.formatMessage(messages.bulkEmailTaskEmailLearnersListLabel)}
</Form.Label>
{learnersEmailList.map(({ id, email }) => (
<Chip
className="email-chip"
iconBefore={Person}
iconAfter={Close}
onIconAfterClick={() => handleDeleteEmailSelected(id)}
key={id}
data-testid="email-list-chip"
>
{email}
</Chip>
))}
</Container>
</Container>
);
}

EmailList.defaultProps = {
handleEmailLearnersSelected: () => {},
handleDeleteEmailLearnerSelected: () => {},
learnersEmailList: [],
};

EmailList.propTypes = {
handleEmailLearnersSelected: PropTypes.func,
handleDeleteEmailLearnerSelected: PropTypes.func,
learnersEmailList: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
email: PropTypes.string.isRequired,
}),
),
intl: intlShape.isRequired,
};

export default injectIntl(EmailList);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.email-list {
width: 100%;
display: flex;
flex-wrap: wrap;
border: 1px solid #ccc;
padding: 10px;
margin-top: 16px;
}

.email-chip {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 20px;
padding: 5px 10px;
margin: 5px;
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable import/prefer-default-export */
// ref: https://github.com/openedx/frontend-app-authn/blob/master/src/data/constants.js#L31
const emailRegex = /^[-!#$%&'*+/=?^_`{}|~0-9A-Za-z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Za-z]+)*'|^"([\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*"|@((?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+)(?:[A-Za-z0-9-]{2,63})|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}]$/;

export const isValidEmail = (email) => emailRegex.test(email);
Loading