forked from openedx/frontend-app-communications
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
527 additions
and
9 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.