Skip to content

Commit

Permalink
feat: inidivual learners email
Browse files Browse the repository at this point in the history
  • Loading branch information
johnvente committed Dec 29, 2023
1 parent 0d94489 commit 36a3b69
Show file tree
Hide file tree
Showing 12 changed files with 527 additions and 9 deletions.
74 changes: 72 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@loadable/component": "^5.15.3",
"@openedx-plugins/communications-app-body-email-form": "file:plugins/communications-app/BodyForm",
"@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-instructions-pro-freading": "file:plugins/communications-app/InstructionsProfreading",
"@openedx-plugins/communications-app-recipients-checks": "file:plugins/communications-app/RecipientsForm",
Expand All @@ -63,6 +64,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",
Expand Down
16 changes: 16 additions & 0 deletions plugins/communications-app/IndividualEmails/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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, page = 1, pageSize = 10) {
const endpointUrl = `${
getConfig().LMS_BASE_URL
}/platform-plugin-communications/${courseId}/api/search_learners?query=${search}&page=${page}&page_size=${pageSize}`;
try {
const response = await getAuthenticatedHttpClient().get(endpointUrl);
return response;
} catch (error) {
logError(error);
throw new Error(error);
}
}
44 changes: 44 additions & 0 deletions plugins/communications-app/IndividualEmails/api.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});
123 changes: 123 additions & 0 deletions plugins/communications-app/IndividualEmails/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';

import { getLearnersEmailInstructorTask } from './api';
import messages from './messages';

import './styles.scss';

const IndividualEmails = ({
courseId,
handleEmailSelected,
emailList,
handleDeleteEmail,
}) => {
const intl = useIntl();
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) {
logError(error);
} 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 (
<Container className="col-12 my-5">
<Form.Label className="mt-3" data-testid="learners-email-input-label">{intl.formatMessage(messages.individualEmailsLabelLearnersInputLabel)}</Form.Label>
<AsyncTypeahead
filterBy={filterBy}
id="async-autocompleinput"
isLoading={isLoading}
labelKey="username"
minLength={3}
onSearch={handleSearchEmailLearners}
options={options}
name="studentEmail"
selected={inputValue}
data-testid="input-typeahead"
placeholder={intl.formatMessage(messages.individualEmailsLabelLearnersInputPlaceholder)}
onChange={handleSelectedLearnerEmail}
renderMenuItemChildren={({ name, email, username }) => (
<span data-testid="autocomplete-email-option">{name ? `${name} -` : name} {username ? `${username} -` : username} {email}</span>
)}
/>
{emailList.length > 0 && (
<Container className="email-list">
<Form.Label className="col-12" data-testid="learners-email-list-label">{intl.formatMessage(messages.individualEmailsLabelLearnersListLabel)}</Form.Label>
{emailList.map(({ id, email }) => (
<Chip
variant="light"
className="email-chip"
iconBefore={Person}
iconAfter={Close}
onIconAfterClick={() => handleDeleteEmailSelected(id)}
key={id}
data-testid="email-list-chip"
>
{email}
</Chip>
))}
</Container>
) }

</Container>
);
};

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,
}),
),
};

export default IndividualEmails;
Loading

0 comments on commit 36a3b69

Please sign in to comment.