diff --git a/package-lock.json b/package-lock.json
index 5366aaf2..331d8d18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@loadable/component": "^5.15.3",
"@openedx-plugins/communications-app-check-box-form": "file:plugins/communications-app/CheckBoxForm",
+ "@openedx-plugins/communications-app-individual-emails": "file:plugins/communications-app/IndividualEmails",
"@openedx-plugins/communications-app-input-form": "file:plugins/communications-app/InputForm",
"@openedx-plugins/communications-app-test-component": "file:plugins/communications-app/TestComponent",
"@tinymce/tinymce-react": "3.14.0",
@@ -33,6 +34,7 @@
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
+ "react-bootstrap-typeahead": "^6.3.2",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
@@ -41,7 +43,8 @@
"redux": "4.2.0",
"regenerator-runtime": "0.13.11",
"tinymce": "5.10.7",
- "use-deep-compare-effect": "^1.8.1"
+ "use-deep-compare-effect": "^1.8.1",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
@@ -5467,6 +5470,10 @@
"resolved": "plugins/communications-app/CheckBoxForm",
"link": true
},
+ "node_modules/@openedx-plugins/communications-app-individual-emails": {
+ "resolved": "plugins/communications-app/IndividualEmails",
+ "link": true
+ },
"node_modules/@openedx-plugins/communications-app-input-form": {
"resolved": "plugins/communications-app/InputForm",
"link": true
@@ -8327,6 +8334,11 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
+ "node_modules/compute-scroll-into-view": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
+ "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg=="
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"license": "MIT"
@@ -18338,6 +18350,32 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/react-bootstrap-typeahead": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.3.2.tgz",
+ "integrity": "sha512-N5Mb0WlSSMcD7Z0pcCypILgIuECybev0hl4lsnCa5lbXTnN4QdkuHLGuTLSlXBwm1ZMFpOc2SnsdSRgeFiF+Ow==",
+ "dependencies": {
+ "@babel/runtime": "^7.14.6",
+ "@popperjs/core": "^2.10.2",
+ "@restart/hooks": "^0.4.0",
+ "classnames": "^2.2.0",
+ "fast-deep-equal": "^3.1.1",
+ "invariant": "^2.2.1",
+ "lodash.debounce": "^4.0.8",
+ "prop-types": "^15.5.8",
+ "react-overlays": "^5.2.0",
+ "react-popper": "^2.2.5",
+ "scroll-into-view-if-needed": "^3.1.0",
+ "warning": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-clientside-effect": {
"version": "1.2.6",
"license": "MIT",
@@ -19703,6 +19741,14 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/scroll-into-view-if-needed": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+ "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+ "dependencies": {
+ "compute-scroll-into-view": "^3.0.2"
+ }
+ },
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -21649,8 +21695,13 @@
}
},
"node_modules/uuid": {
- "version": "9.0.0",
- "license": "MIT",
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -22425,6 +22476,26 @@
}
}
},
+ "plugins/communications-app/IndividualEmails": {
+ "name": "@openedx-plugins/communications-app-individual-emails",
+ "version": "1.0.0",
+ "dependencies": {
+ "react-bootstrap-typeahead": "^6.3.2",
+ "uuid": "^9.0.1"
+ },
+ "peerDependencies": {
+ "@edx/frontend-app-communications": "*",
+ "@edx/frontend-platform": "*",
+ "@edx/paragon": "*",
+ "prop-types": "*",
+ "react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edx/frontend-app-communications": {
+ "optional": true
+ }
+ }
+ },
"plugins/communications-app/InputForm": {
"name": "@openedx-plugins/communications-app-input-form",
"version": "1.0.0",
@@ -22441,6 +22512,21 @@
}
}
},
- "plugins/communications-app/TestComponent": {}
+ "plugins/communications-app/TestComponent": {
+ "name": "@openedx-plugins/communications-app-test-component",
+ "version": "1.0.0",
+ "peerDependencies": {
+ "@edx/frontend-app-communications": "*",
+ "@edx/frontend-platform": "*",
+ "@edx/paragon": "*",
+ "prop-types": "*",
+ "react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edx/frontend-app-communications": {
+ "optional": true
+ }
+ }
+ }
}
}
diff --git a/package.json b/package.json
index a077a7da..af5f5624 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@loadable/component": "^5.15.3",
"@openedx-plugins/communications-app-check-box-form": "file:plugins/communications-app/CheckBoxForm",
+ "@openedx-plugins/communications-app-individual-emails": "file:plugins/communications-app/IndividualEmails",
"@openedx-plugins/communications-app-input-form": "file:plugins/communications-app/InputForm",
"@openedx-plugins/communications-app-test-component": "file:plugins/communications-app/TestComponent",
"@tinymce/tinymce-react": "3.14.0",
@@ -57,6 +58,7 @@
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
+ "react-bootstrap-typeahead": "^6.3.2",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
@@ -65,7 +67,8 @@
"redux": "4.2.0",
"regenerator-runtime": "0.13.11",
"tinymce": "5.10.7",
- "use-deep-compare-effect": "^1.8.1"
+ "use-deep-compare-effect": "^1.8.1",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
diff --git a/plugins/communications-app/IndividualEmails/api.js b/plugins/communications-app/IndividualEmails/api.js
new file mode 100644
index 00000000..7fb2fc57
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/api.js
@@ -0,0 +1,14 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { logError } from '@edx/frontend-platform/logging';
+
+export async function getLearnersEmailInstructorTask(courseId, search) {
+ const endpointUrl = `${getConfig().LMS_BASE_URL}/platform-plugin-communications/${courseId}/api/search_learners?query=${search}&page=1&page_size=10`;
+ try {
+ const response = await getAuthenticatedHttpClient().get(endpointUrl);
+ return response;
+ } catch (error) {
+ logError(error);
+ throw new Error(error);
+ }
+}
diff --git a/plugins/communications-app/IndividualEmails/api.test.js b/plugins/communications-app/IndividualEmails/api.test.js
new file mode 100644
index 00000000..4609e34a
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/api.test.js
@@ -0,0 +1,44 @@
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { getConfig } from '@edx/frontend-platform';
+import { logError } from '@edx/frontend-platform/logging';
+
+import { getLearnersEmailInstructorTask } from './api';
+
+jest.mock('@edx/frontend-platform/auth', () => ({
+ getAuthenticatedHttpClient: jest.fn(),
+}));
+jest.mock('@edx/frontend-platform', () => ({
+ getConfig: jest.fn(),
+}));
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+}));
+
+describe('getLearnersEmailInstructorTask', () => {
+ const mockCourseId = 'course123';
+ const mockSearch = 'testuser';
+ const mockResponseData = { data: 'someData' };
+ const mockConfig = { LMS_BASE_URL: 'http://localhost' };
+
+ beforeEach(() => {
+ getConfig.mockReturnValue(mockConfig);
+ getAuthenticatedHttpClient.mockReturnValue({
+ get: jest.fn().mockResolvedValue(mockResponseData),
+ });
+ });
+
+ it('successfully fetches data', async () => {
+ const data = await getLearnersEmailInstructorTask(mockCourseId, mockSearch);
+ expect(data).toEqual(mockResponseData);
+ expect(getAuthenticatedHttpClient().get).toHaveBeenCalledWith(
+ `http://localhost/platform-plugin-communications/${mockCourseId}/api/search_learners?query=${mockSearch}&page=1&page_size=10`,
+ );
+ });
+
+ it('handles an error', async () => {
+ getAuthenticatedHttpClient().get.mockRejectedValue(new Error('Network error'));
+
+ await expect(getLearnersEmailInstructorTask(mockCourseId, mockSearch)).rejects.toThrow('Network error');
+ expect(logError).toHaveBeenCalledWith(new Error('Network error'));
+ });
+});
diff --git a/plugins/communications-app/IndividualEmails/index.jsx b/plugins/communications-app/IndividualEmails/index.jsx
new file mode 100644
index 00000000..28bd5d84
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/index.jsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+import { v4 as uuidv4 } from 'uuid';
+import { Form, Chip, Container } from '@edx/paragon';
+import { Person, Close } from '@edx/paragon/icons';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { getLearnersEmailInstructorTask } from './api';
+import messages from './messages';
+
+import './styles.scss';
+
+function IndividualEmails(props) {
+ const {
+ courseId, handleEmailSelected, emailList, handleDeleteEmail,
+ intl,
+ } = props;
+ const [isLoading, setIsLoading] = useState(false);
+ const [options, setOptions] = useState([]);
+ const [inputValue] = useState([]);
+
+ const handleSearchEmailLearners = async (userEmail) => {
+ setIsLoading(true);
+ try {
+ const response = await getLearnersEmailInstructorTask(courseId, userEmail);
+ const { results } = response.data;
+ const formatResult = results.map((item) => ({ id: uuidv4(), ...item }));
+ setOptions(formatResult);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('error autocomplete input', error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const filterBy = (option) => option.name || option.email || option.username;
+ const handleDeleteEmailSelected = (id) => {
+ if (handleDeleteEmail) {
+ handleDeleteEmail(id);
+ }
+ };
+
+ const handleSelectedLearnerEmail = (selected) => {
+ const [itemSelected] = selected || [{ email: '' }];
+ const isEmailAdded = emailList.some((item) => item.email === itemSelected.email);
+
+ if (selected && !isEmailAdded) {
+ handleEmailSelected(selected);
+ }
+ };
+
+ return (
+
+ {intl.formatMessage(messages.individualEmailsLabelLearnersInputLabel)}
+ (
+ {name ? `${name} -` : name} {email}
+ )}
+ />
+
+ {intl.formatMessage(messages.individualEmailsLabelLearnersListLabel)}
+ {emailList.map(({ id, email }) => (
+ handleDeleteEmailSelected(id)}
+ key={id}
+ data-testid="email-list-chip"
+ >
+ {email}
+
+ ))}
+
+
+
+ );
+}
+
+IndividualEmails.defaultProps = {
+ courseId: '',
+ handleEmailSelected: () => {},
+ handleDeleteEmail: () => {},
+ emailList: [],
+};
+
+IndividualEmails.propTypes = {
+ courseId: PropTypes.string,
+ handleEmailSelected: PropTypes.func,
+ handleDeleteEmail: PropTypes.func,
+ emailList: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ email: PropTypes.string,
+ username: PropTypes.string,
+ }),
+ ),
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(IndividualEmails);
diff --git a/plugins/communications-app/IndividualEmails/index.test.jsx b/plugins/communications-app/IndividualEmails/index.test.jsx
new file mode 100644
index 00000000..393ace21
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/index.test.jsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import {
+ render, screen, fireEvent, waitFor,
+} from '@testing-library/react';
+import { IntlProvider } from 'react-intl';
+
+import IndividualEmails from '.';
+import * as api from './api';
+import messages from './messages';
+
+jest.mock('./api');
+describe('IndividualEmails Component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ api.getLearnersEmailInstructorTask.mockResolvedValue({
+ data: {
+ results: [
+ { email: 'test@email.com', username: 'test', name: 'test' },
+ { email: 'test2@email.com', username: 'test2', name: 'test2' },
+ ],
+ },
+ });
+ });
+
+ const mockEmailList = [
+ { id: '1', email: 'user1@example.com' },
+ { id: '2', email: 'user2@example.com' },
+ ];
+
+ // eslint-disable-next-line react/prop-types
+ const IntlProviderWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ it('renders the component without errors', () => {
+ render(
+
+
+ ,
+ );
+ });
+
+ it('displays the correct internationalized messages', () => {
+ render(
+
+
+ ,
+ );
+
+ const emailInput = screen.getByRole('combobox');
+
+ const {
+ individualEmailsLabelLearnersInputLabel,
+ individualEmailsLabelLearnersInputPlaceholder,
+ individualEmailsLabelLearnersListLabel,
+ } = messages;
+
+ expect(screen.getByTestId('learners-email-input-label')).toHaveTextContent(individualEmailsLabelLearnersInputLabel.defaultMessage);
+ expect(emailInput).toHaveAttribute('placeholder', individualEmailsLabelLearnersInputPlaceholder.defaultMessage);
+ expect(screen.getByTestId('learners-email-list-label')).toHaveTextContent(individualEmailsLabelLearnersListLabel.defaultMessage);
+ });
+
+ it('renders the component with main components ', () => {
+ render(
+
+
+ ,
+ );
+
+ const emailInputLabel = screen.getByTestId('learners-email-input-label');
+ const emailInput = screen.getByRole('combobox');
+ const emailListLabel = screen.getByTestId('learners-email-list-label');
+
+ expect(emailInputLabel).toBeInTheDocument();
+ expect(emailInput).toBeInTheDocument();
+ expect(emailListLabel).toBeInTheDocument();
+ });
+
+ it('should render two email chips', () => {
+ render(
+
+
+ ,
+ );
+
+ const emailChips = screen.getAllByTestId('email-list-chip');
+ expect(emailChips).toHaveLength(2);
+ });
+
+ it('triggers search on typing in search box', async () => {
+ const mockHandleEmailSelected = jest.fn();
+ const mockCourseId = 'course123';
+ render(
+
+
+ ,
+ );
+
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'test' } });
+ await waitFor(() => {
+ expect(api.getLearnersEmailInstructorTask).toHaveBeenCalled();
+ expect(api.getLearnersEmailInstructorTask).toHaveBeenCalledWith(mockCourseId, 'test');
+ const learnersToSelect = screen.getAllByTestId('autocomplete-email-option');
+ expect(learnersToSelect).toHaveLength(2);
+
+ const [firstOption] = learnersToSelect;
+ fireEvent.click(firstOption);
+ expect(mockHandleEmailSelected).toHaveBeenCalled();
+ });
+ });
+
+ it('invokes handleDeleteEmail when clicking on delete icons', () => {
+ const mockHandleDeleteEmail = jest.fn();
+ render(
+
+
+ ,
+ );
+
+ const emailChips = screen.getAllByTestId('email-list-chip');
+
+ emailChips.forEach((chip) => {
+ const deleteButton = chip.querySelector('[role="button"]');
+ fireEvent.click(deleteButton);
+ });
+
+ expect(mockHandleDeleteEmail).toHaveBeenCalledTimes(mockEmailList.length);
+
+ expect(mockHandleDeleteEmail).toHaveBeenCalledWith(mockEmailList[0].id);
+ expect(mockHandleDeleteEmail).toHaveBeenCalledWith(mockEmailList[1].id);
+ });
+});
diff --git a/plugins/communications-app/IndividualEmails/messages.js b/plugins/communications-app/IndividualEmails/messages.js
new file mode 100644
index 00000000..932e0a04
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/messages.js
@@ -0,0 +1,22 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ /* index.jsx Messages */
+ individualEmailsLabelLearnersInputLabel: {
+ id: 'individual.emails.learners.input.label',
+ defaultMessage: 'Add individual learner',
+ description: 'Input autocomplete label for learners email',
+ },
+ individualEmailsLabelLearnersInputPlaceholder: {
+ id: 'individual.emails.learners.input.placeholder',
+ defaultMessage: 'Search for individual email',
+ description: 'Placeholder for autocomplete input for learners email',
+ },
+ individualEmailsLabelLearnersListLabel: {
+ id: 'individual.emails.learners.list.label',
+ defaultMessage: 'Recipients',
+ description: 'Title for learners email list',
+ },
+});
+
+export default messages;
diff --git a/plugins/communications-app/IndividualEmails/package.json b/plugins/communications-app/IndividualEmails/package.json
new file mode 100644
index 00000000..81a26f24
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@openedx-plugins/communications-app-individual-emails",
+ "version": "1.0.0",
+ "description": "edx input and list of expecifict email",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "dependencies": {
+ "react-bootstrap-typeahead": "^6.3.2",
+ "uuid": "^9.0.1"
+ },
+ "peerDependencies": {
+ "@edx/frontend-app-communications": "*",
+ "@edx/frontend-platform": "*",
+ "@edx/paragon": "*",
+ "prop-types": "*",
+ "react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edx/frontend-app-communications": {
+ "optional": true
+ }
+ }
+}
diff --git a/plugins/communications-app/IndividualEmails/styles.scss b/plugins/communications-app/IndividualEmails/styles.scss
new file mode 100644
index 00000000..efa0b22d
--- /dev/null
+++ b/plugins/communications-app/IndividualEmails/styles.scss
@@ -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;
+}
diff --git a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
index 590d4c95..5461c12c 100644
--- a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
+++ b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import {
Button,
Form, Icon, StatefulButton, Toast, useToggle,
- Card,
} from '@edx/paragon';
import {
SpinnerSimple, Cancel, Send, Event, Check,
@@ -16,7 +15,6 @@ import TextEditor from '../text-editor/TextEditor';
import BulkEmailRecipient from './bulk-email-recipient';
import TaskAlertModal from '../task-alert-modal';
import useTimeout from '../../../utils/useTimeout';
-import PluggableComponent from '../../PluggableComponent';
import useMobileResponsive from '../../../utils/useMobileResponsive';
import ScheduleEmailForm from './ScheduleEmailForm';
import messages from './messages';
@@ -62,6 +60,7 @@ function BulkEmailForm(props) {
const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false);
const [isScheduled, toggleScheduled] = useState(false);
const isMobile = useMobileResponsive();
+ const [emailLearnersList, setEmailLearnersList] = useState([]);
/**
* Since we are working with both an old and new API endpoint, the body for the POST
@@ -71,12 +70,17 @@ function BulkEmailForm(props) {
* @returns formatted Data
*/
const formatDataForFormAction = (action) => {
+ const emailsIndividualLearners = emailLearnersList.map(({ email }) => email);
+ const extraTargets = { emails: emailsIndividualLearners };
+ const emailRecipients = editor.emailRecipients.filter((recipient) => recipient !== 'individual-learners-emails');
+
if (action === FORM_ACTIONS.POST) {
const emailData = new FormData();
emailData.append('action', 'send');
- emailData.append('send_to', JSON.stringify(editor.emailRecipients));
+ emailData.append('send_to', JSON.stringify(emailRecipients));
emailData.append('subject', editor.emailSubject);
emailData.append('message', editor.emailBody);
+ emailData.append('extra_targets', JSON.stringify(extraTargets));
if (isScheduled) {
emailData.append('schedule', new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toISOString());
}
@@ -85,7 +89,7 @@ function BulkEmailForm(props) {
if (action === FORM_ACTIONS.PATCH) {
return {
email: {
- targets: editor.emailRecipients,
+ targets: emailRecipients,
subject: editor.emailSubject,
message: editor.emailBody,
id: editor.emailId,
@@ -127,6 +131,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"
+ setEmailLearnersList([]);
editor.emailRecipients.forEach(recipient => {
if (/^cohort/.test(recipient) || /^track/.test(recipient)) {
dispatch(removeRecipient(recipient));
@@ -211,6 +217,36 @@ 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 emailLearnersList state
+ if so, we will delete emailRecipients "learners" because that is for all learners
+ if not, we will delete the individual-learners-emails from emailRecipients because of we won't use the emails
+ */
+ useEffect(() => {
+ if (emailLearnersList.length && !editor.emailRecipients.includes('individual-learners-emails')) {
+ dispatch(addRecipient('individual-learners-emails'));
+ if (editor.emailRecipients.includes('learners')) {
+ dispatch(removeRecipient('learners'));
+ }
+ } else if (!emailLearnersList.length && editor.emailRecipients.includes('individual-learners-emails')) {
+ dispatch(removeRecipient('individual-learners-emails'));
+ }
+ }, [dispatch, editor.emailRecipients, emailLearnersList]);
+
+ // When the user selects an email from input autocomplete list
+ const handleEmailLearnersSelected = (emailSelected) => {
+ const [firstItem] = emailSelected;
+ if (firstItem) {
+ setEmailLearnersList([...emailLearnersList, firstItem]);
+ }
+ };
+
+ // To delete an email from learners list, that list is on the bottom of the input autocomplete
+ const handleDeleteEmailLearnerSelected = (idToDelete) => {
+ const setEmailLearnersListUpdated = emailLearnersList.filter(({ id }) => id !== idToDelete);
+ setEmailLearnersList(setEmailLearnersListUpdated);
+ };
+
const AlertMessage = () => (
<>
{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject: editor.emailSubject })}
@@ -269,49 +305,15 @@ function BulkEmailForm(props) {
}}
/>
{intl.formatMessage(messages.bulkEmailSubjectLabel)}
diff --git a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx b/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx
index 2bafefc9..af26a440 100644
--- a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx
+++ b/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import PluggableComponent from '../../../PluggableComponent';
import './bulkEmailRecepient.scss';
@@ -14,7 +15,10 @@ const DEFAULT_GROUPS = {
};
export default function BulkEmailRecipient(props) {
- const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
+ const {
+ handleCheckboxes, selectedGroups, additionalCohorts, handleLearnersEmailSelected,
+ emailLearnersList, handleLearnersDeleteEmail, courseId,
+ } = props;
return (
@@ -104,6 +108,16 @@ export default function BulkEmailRecipient(props) {
/>
+
+
+
{!props.isValid && (
{},
+ handleLearnersDeleteEmail: () => {},
+ emailLearnersList: [],
};
BulkEmailRecipient.propTypes = {
@@ -127,4 +145,14 @@ BulkEmailRecipient.propTypes = {
handleCheckboxes: PropTypes.func.isRequired,
isValid: PropTypes.bool,
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
+ courseId: PropTypes.string,
+ handleLearnersEmailSelected: PropTypes.func,
+ handleLearnersDeleteEmail: PropTypes.func,
+ emailLearnersList: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
+ username: PropTypes.string.isRequired,
+ }),
+ ),
};
diff --git a/src/components/bulk-email-tool/bulk-email-form/data/api.js b/src/components/bulk-email-tool/bulk-email-form/data/api.js
index 5d5aff96..4da5d4b9 100644
--- a/src/components/bulk-email-tool/bulk-email-form/data/api.js
+++ b/src/components/bulk-email-tool/bulk-email-form/data/api.js
@@ -24,3 +24,14 @@ export async function patchScheduledBulkEmailInstructorTask(emailData, courseId,
throw new Error(error);
}
}
+
+export async function postBulkEmailInstructorTaskSendEmails(emailData, courseId) {
+ try {
+ const url = `${getConfig().LMS_BASE_URL}/platform-plugin-communications/${courseId}/api/send_email`;
+ const response = await getAuthenticatedHttpClient().post(url, emailData);
+ return response;
+ } catch (error) {
+ logError(error);
+ throw new Error(error);
+ }
+}
diff --git a/src/components/bulk-email-tool/bulk-email-form/data/thunks.js b/src/components/bulk-email-tool/bulk-email-form/data/thunks.js
index aa76fab4..01a9a163 100644
--- a/src/components/bulk-email-tool/bulk-email-form/data/thunks.js
+++ b/src/components/bulk-email-tool/bulk-email-form/data/thunks.js
@@ -8,7 +8,7 @@ import {
postBulkEmailError,
postBulkEmailStart,
} from './actions';
-import { patchScheduledBulkEmailInstructorTask, postBulkEmailInstructorTask } from './api';
+import { patchScheduledBulkEmailInstructorTask, postBulkEmailInstructorTaskSendEmails } from './api';
export function postBulkEmailThunk(emailData, courseId) {
return async (dispatch) => {
@@ -23,7 +23,7 @@ export function postBulkEmailThunk(emailData, courseId) {
return error;
}
try {
- const data = await postBulkEmailInstructorTask(emailData, courseId);
+ const data = await postBulkEmailInstructorTaskSendEmails(emailData, courseId);
return onComplete(data);
} catch (error) {
return onError(error);
diff --git a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx
index 95e521c9..557462ce 100644
--- a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx
+++ b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx
@@ -123,7 +123,7 @@ describe('bulk-email-form', () => {
expect(screen.getByText('Date and time cannot be blank, and must be a date in the future'));
});
test('Adds scheduling data to POST requests when schedule is selected', async () => {
- const postBulkEmailInstructorTask = jest.spyOn(bulkEmailFormApi, 'postBulkEmailInstructorTask');
+ const postBulkEmailInstructorTaskSendEmails = jest.spyOn(bulkEmailFormApi, 'postBulkEmailInstructorTaskSendEmails');
render(renderBulkEmailForm());
fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' }));
fireEvent.change(screen.getByRole('textbox', { name: 'Subject' }), { target: { value: 'test subject' } });
@@ -139,7 +139,7 @@ describe('bulk-email-form', () => {
const continueButton = await screen.findByRole('button', { name: /continue/i });
fireEvent.click(continueButton);
expect(appendMock).toHaveBeenCalledWith('schedule', expect.stringContaining(formatDate(tomorrow)));
- expect(postBulkEmailInstructorTask).toHaveBeenCalledWith(expect.any(FormData), expect.stringContaining('test'));
+ expect(postBulkEmailInstructorTaskSendEmails).toHaveBeenCalledWith(expect.any(FormData), expect.stringContaining('test'));
});
test('will PATCH instead of POST when in edit mode', async () => {
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());