-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #200 from openedx/ansab/PROD-2478
feat: add link program enrollment to v2
- Loading branch information
Showing
15 changed files
with
727 additions
and
7 deletions.
There are no files selected for viewing
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,95 @@ | ||
import { Input, Button } from '@edx/paragon'; | ||
import React, { useState, useCallback } from 'react'; | ||
import getLinkProgramEnrollmentDetails from './data/api'; | ||
import LinkProgramEnrollmentsTable from './LinkProgramEnrollmentsTable'; | ||
|
||
export default function LinkProgramEnrollments() { | ||
const [programID, setProgramID] = useState(undefined); | ||
const [usernamePairText, setUsernamePairText] = useState(undefined); | ||
const [successMessage, setSuccessMessage] = useState(undefined); | ||
const [errorMessage, setErrorMessage] = useState(undefined); | ||
const [isFetchingData, setIsFetchingData] = useState(false); | ||
|
||
const onProgramChange = (e) => { | ||
if (e.currentTarget.value) { | ||
setProgramID(e.currentTarget.value); | ||
} else { | ||
setProgramID(undefined); | ||
} | ||
}; | ||
|
||
const onUserTextChange = (e) => { | ||
if (e.currentTarget.value) { | ||
setUsernamePairText(e.currentTarget.value); | ||
} else { | ||
setUsernamePairText(undefined); | ||
} | ||
}; | ||
|
||
const handleSubmit = () => { | ||
setIsFetchingData(true); | ||
getLinkProgramEnrollmentDetails({ programID, usernamePairText }).then((response) => { | ||
setSuccessMessage(response.successes); | ||
setErrorMessage(response.errors); | ||
setIsFetchingData(false); | ||
}); | ||
}; | ||
|
||
const submit = useCallback((event) => { | ||
event.preventDefault(); | ||
handleSubmit(); | ||
return false; | ||
}); | ||
|
||
return ( | ||
<> | ||
<h3>Link Program Enrollments</h3> | ||
<section className="my-3"> | ||
<form> | ||
<div className="my-2"> | ||
<label htmlFor="programUUID">Program UUID</label> | ||
<Input | ||
className="mr-1 col-sm-12" | ||
name="programUUID" | ||
type="text" | ||
defaultValue={programID} | ||
onChange={onProgramChange} | ||
/> | ||
</div> | ||
<div className="my-4"> | ||
<label | ||
className="d-flex align-items-start" | ||
htmlFor="usernamePairText" | ||
> | ||
List of External key and username pairings (one per line) | ||
</label> | ||
<Input | ||
className="mr-1 col-sm-12" | ||
name="usernamePairText" | ||
type="textarea" | ||
rows="10" | ||
onChange={onUserTextChange} | ||
defaultValue={usernamePairText} | ||
placeholder="external_user_key,lms_username" | ||
/> | ||
</div> | ||
<Button | ||
type="submit" | ||
onClick={submit} | ||
disabled={isFetchingData} | ||
> | ||
Submit | ||
</Button> | ||
</form> | ||
</section> | ||
{((errorMessage && errorMessage.length > 0) | ||
|| (successMessage && successMessage.length > 0)) && ( | ||
<LinkProgramEnrollmentsTable | ||
successMessage={successMessage} | ||
errorMessage={errorMessage} | ||
usernamePairText={usernamePairText} | ||
/> | ||
)} | ||
</> | ||
); | ||
} |
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,173 @@ | ||
import { mount } from 'enzyme'; | ||
import React from 'react'; | ||
import { MemoryRouter } from 'react-router-dom'; | ||
import { history } from '@edx/frontend-platform'; | ||
import { waitForComponentToPaint } from '../setupTest'; | ||
import LinkProgramEnrollments from './LinkProgramEnrollments'; | ||
import UserMessagesProvider from '../userMessages/UserMessagesProvider'; | ||
import { | ||
lpeSuccessResponse, | ||
lpeErrorResponseInvalidUUID, | ||
lpeErrorResponseEmptyValues, | ||
lpeErrorResponseInvalidUsername, | ||
lpeErrorResponseInvalidExternalKey, | ||
lpeErrorResponseAlreadyLinked, | ||
} from './data/test/linkProgramEnrollment'; | ||
|
||
import * as api from './data/api'; | ||
|
||
const LinkProgramEnrollmentsWrapper = (props) => ( | ||
<MemoryRouter> | ||
<UserMessagesProvider> | ||
<LinkProgramEnrollments {...props} /> | ||
</UserMessagesProvider> | ||
</MemoryRouter> | ||
); | ||
|
||
describe('Link Program Enrollments', () => { | ||
let wrapper; | ||
let apiMock; | ||
const data = { | ||
programID: '8bee627e-d85e-4a76-be41-d58921da666e', | ||
usernamePairText: 'testuser,verified', | ||
}; | ||
|
||
beforeEach(() => { | ||
if (apiMock) { | ||
apiMock.mockReset(); | ||
} | ||
}); | ||
|
||
it('default page render', async () => { | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
const programIdInput = wrapper.find("input[name='programUUID']"); | ||
const usernamePairInput = wrapper.find("textarea[name='usernamePairText']"); | ||
const submitButton = wrapper.find('button.btn-primary'); | ||
|
||
expect(programIdInput.prop('defaultValue')).toEqual(undefined); | ||
expect(usernamePairInput.prop('defaultValue')).toEqual(undefined); | ||
expect(submitButton.text()).toEqual('Submit'); | ||
}); | ||
|
||
it('valid search value', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeSuccessResponse)); | ||
history.push = jest.fn(); | ||
|
||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('api call made on each click', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementation(() => Promise.resolve(lpeSuccessResponse)); | ||
history.push = jest.fn(); | ||
|
||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
|
||
wrapper.find('button.btn-primary').simulate('click'); | ||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('empty search value yields error response', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseEmptyValues)); | ||
history.replace = jest.fn(); | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = ''; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = ''; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find('.error-message')).toHaveLength(1); | ||
expect(wrapper.find('.success-message')).toHaveLength(0); | ||
}); | ||
|
||
it('Invalid Program UUID value', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidUUID)); | ||
history.replace = jest.fn(); | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find('.error-message')).toHaveLength(1); | ||
expect(wrapper.find('.success-message')).toHaveLength(0); | ||
}); | ||
|
||
it('Invalid Username value', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidUsername)); | ||
history.replace = jest.fn(); | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find('.error-message')).toHaveLength(1); | ||
expect(wrapper.find('.success-message')).toHaveLength(0); | ||
}); | ||
|
||
it('Invalid External User Key value', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidExternalKey)); | ||
history.replace = jest.fn(); | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find('.error-message')).toHaveLength(1); | ||
expect(wrapper.find('.success-message')).toHaveLength(0); | ||
}); | ||
|
||
it('Program Already Linked', async () => { | ||
apiMock = jest | ||
.spyOn(api, 'default') | ||
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseAlreadyLinked)); | ||
history.replace = jest.fn(); | ||
wrapper = mount(<LinkProgramEnrollmentsWrapper />); | ||
|
||
wrapper.find('input[name="programUUID"]').instance().value = data.programID; | ||
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText; | ||
wrapper.find('button.btn-primary').simulate('click'); | ||
|
||
await waitForComponentToPaint(wrapper); | ||
expect(apiMock).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find('.error-message')).toHaveLength(1); | ||
expect(wrapper.find('.success-message')).toHaveLength(0); | ||
}); | ||
}); |
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,69 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import TableV2 from '../components/Table'; | ||
import { extractMessageTuple } from '../utils/index'; | ||
|
||
export default function LinkProgramEnrollmentsTable({ | ||
successMessage, | ||
errorMessage, | ||
}) { | ||
return ( | ||
<> | ||
{successMessage && successMessage.length > 0 && ( | ||
<div className="my-2 success-message"> | ||
<h4>Successes</h4> | ||
<TableV2 | ||
columns={[ | ||
{ | ||
Header: 'External User Key', | ||
accessor: 'external_user_key', | ||
}, | ||
{ | ||
Header: 'LMS Username', | ||
accessor: 'lms_username', | ||
}, | ||
{ | ||
Header: 'Message', | ||
accessor: 'message', | ||
}, | ||
]} | ||
data={successMessage.map((text) => { | ||
const pair = extractMessageTuple(text); | ||
return { | ||
external_user_key: pair[0], | ||
lms_username: pair[1], | ||
message: 'Linkage Successfully Created', | ||
}; | ||
})} | ||
styleName="custom-table success-table" | ||
/> | ||
</div> | ||
)} | ||
{errorMessage && errorMessage.length > 0 && ( | ||
<div className="my-2 error-message"> | ||
<h4>Errors</h4> | ||
<TableV2 | ||
columns={[ | ||
{ | ||
Header: 'Error Messages', | ||
accessor: 'message', | ||
}, | ||
]} | ||
data={errorMessage.map((text) => ({ message: text }))} | ||
styleName="custom-table error-table" | ||
/> | ||
</div> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
LinkProgramEnrollmentsTable.propTypes = { | ||
successMessage: PropTypes.arrayOf(PropTypes.string), | ||
errorMessage: PropTypes.arrayOf(PropTypes.string), | ||
}; | ||
|
||
LinkProgramEnrollmentsTable.defaultProps = { | ||
successMessage: [], | ||
errorMessage: [], | ||
}; |
Oops, something went wrong.