diff --git a/src/ProgramEnrollments/LinkProgramEnrollments.jsx b/src/ProgramEnrollments/LinkProgramEnrollments.jsx new file mode 100644 index 000000000..b781664ee --- /dev/null +++ b/src/ProgramEnrollments/LinkProgramEnrollments.jsx @@ -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 ( + <> +

Link Program Enrollments

+
+
+
+ + +
+
+ + +
+ +
+
+ {((errorMessage && errorMessage.length > 0) + || (successMessage && successMessage.length > 0)) && ( + + )} + + ); +} diff --git a/src/ProgramEnrollments/LinkProgramEnrollments.test.jsx b/src/ProgramEnrollments/LinkProgramEnrollments.test.jsx new file mode 100644 index 000000000..bdd34269c --- /dev/null +++ b/src/ProgramEnrollments/LinkProgramEnrollments.test.jsx @@ -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) => ( + + + + + +); + +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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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); + }); +}); diff --git a/src/ProgramEnrollments/LinkProgramEnrollmentsTable.jsx b/src/ProgramEnrollments/LinkProgramEnrollmentsTable.jsx new file mode 100644 index 000000000..22c4104a1 --- /dev/null +++ b/src/ProgramEnrollments/LinkProgramEnrollmentsTable.jsx @@ -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 && ( +
+

Successes

+ { + const pair = extractMessageTuple(text); + return { + external_user_key: pair[0], + lms_username: pair[1], + message: 'Linkage Successfully Created', + }; + })} + styleName="custom-table success-table" + /> +
+ )} + {errorMessage && errorMessage.length > 0 && ( +
+

Errors

+ ({ message: text }))} + styleName="custom-table error-table" + /> +
+ )} + + ); +} + +LinkProgramEnrollmentsTable.propTypes = { + successMessage: PropTypes.arrayOf(PropTypes.string), + errorMessage: PropTypes.arrayOf(PropTypes.string), +}; + +LinkProgramEnrollmentsTable.defaultProps = { + successMessage: [], + errorMessage: [], +}; diff --git a/src/ProgramEnrollments/LinkProgramEnrollmentsTable.test.jsx b/src/ProgramEnrollments/LinkProgramEnrollmentsTable.test.jsx new file mode 100644 index 000000000..22a659fdf --- /dev/null +++ b/src/ProgramEnrollments/LinkProgramEnrollmentsTable.test.jsx @@ -0,0 +1,122 @@ +import { mount } from 'enzyme'; +import React from 'react'; +import LinkProgramEnrollmentsTable from './LinkProgramEnrollmentsTable'; +import { + lpeSuccessResponse, + lpeErrorResponseInvalidUUID, + lpeErrorResponseEmptyValues, + lpeErrorResponseInvalidUsername, + lpeErrorResponseInvalidExternalKey, + lpeErrorResponseAlreadyLinked, +} from './data/test/linkProgramEnrollment'; + +describe('Link Program Enrollment Tables component', () => { + let wrapper; + + afterEach(() => { + wrapper.unmount(); + }); + + describe('Success Table', () => { + it('Success Table exists', () => { + wrapper = mount( + , + ); + + const header = wrapper.find('.success-message h4'); + const dataTable = wrapper.find('table.success-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Successes'); + + expect(headingRow.find('th').at(0).text()).toEqual('External User Key'); + expect(headingRow.find('th').at(1).text()).toEqual('LMS Username'); + expect(headingRow.find('th').at(2).text()).toEqual('Message'); + + expect(dataRow.find('td').at(0).text()).toEqual('testuser'); + expect(dataRow.find('td').at(1).text()).toEqual('verified'); + expect(dataRow.find('td').at(2).text()).toEqual('Linkage Successfully Created'); + }); + }); + + describe('Error Table', () => { + it('Error when empty value', () => { + wrapper = mount( + , + ); + const header = wrapper.find('.error-message h4'); + const dataTable = wrapper.find('table.error-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Errors'); + expect(headingRow.find('th').at(0).text()).toEqual('Error Messages'); + expect(dataRow.find('td').at(0).text()).toEqual("You must provide both a program uuid and a series of lines with the format 'external_user_key,lms_username'."); + }); + it('Error when Invalid Program ID', () => { + wrapper = mount( + , + ); + const header = wrapper.find('.error-message h4'); + const dataTable = wrapper.find('table.error-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Errors'); + expect(headingRow.find('th').at(0).text()).toEqual('Error Messages'); + expect(dataRow.find('td').at(0).text()).toEqual("Supplied program UUID '8bee627e-d85e-4a76-be41-d58921da666e' is not a valid UUID."); + }); + it('Error when Invalid Username', () => { + wrapper = mount( + , + ); + const header = wrapper.find('.error-message h4'); + const dataTable = wrapper.find('table.error-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Errors'); + expect(headingRow.find('th').at(0).text()).toEqual('Error Messages'); + expect(dataRow.find('td').at(0).text()).toEqual('No user found with username verified'); + }); + it('Error when Invalid External Key', () => { + wrapper = mount( + , + ); + const header = wrapper.find('.error-message h4'); + const dataTable = wrapper.find('table.error-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Errors'); + expect(headingRow.find('th').at(0).text()).toEqual('Error Messages'); + expect(dataRow.find('td').at(0).text()).toEqual('No program enrollment found for program uuid=8bee627e-d85e-4a76-be41-d58921da666e and external student key=testuser'); + }); + it('Error when Already Linked ID', () => { + wrapper = mount( + , + ); + const header = wrapper.find('.error-message h4'); + const dataTable = wrapper.find('table.error-table tr'); + const headingRow = dataTable.at(0); + const dataRow = dataTable.at(1); + + expect(header.text()).toEqual('Errors'); + expect(headingRow.find('th').at(0).text()).toEqual('Error Messages'); + expect(dataRow.find('td').at(0).text()).toEqual('Program enrollment with external_student_key=testuser1 is already linked to target account username=verified'); + }); + }); +}); diff --git a/src/ProgramEnrollments/ProgramEnrollmentsIndexPage.jsx b/src/ProgramEnrollments/ProgramEnrollmentsIndexPage.jsx new file mode 100644 index 000000000..16dcb2884 --- /dev/null +++ b/src/ProgramEnrollments/ProgramEnrollmentsIndexPage.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import LinkProgramEnrollments from './LinkProgramEnrollments'; + +export default function ProgramEnrollmentsIndexPage({ location }) { + return ( +
+
+ +
+
+ ); +} + +ProgramEnrollmentsIndexPage.propTypes = { + location: PropTypes.shape({ + pathname: PropTypes.string, + search: PropTypes.string, + }).isRequired, +}; diff --git a/src/ProgramEnrollments/data/api.js b/src/ProgramEnrollments/data/api.js new file mode 100644 index 000000000..b1f5d5809 --- /dev/null +++ b/src/ProgramEnrollments/data/api.js @@ -0,0 +1,30 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform'; + +const { LMS_BASE_URL } = getConfig(); + +export default async function getLinkProgramEnrollmentDetails({ + programID, + usernamePairText, +}) { + const apiUrl = `${LMS_BASE_URL}/support/link_program_enrollments_details/`; + const formData = new FormData(); + formData.append('program_uuid', programID); + formData.append('username_pair_text', usernamePairText); + try { + const { data } = await getAuthenticatedHttpClient().post(apiUrl, formData); + return data; + } catch (error) { + return { + error: [ + { + code: null, + dismissible: true, + text: `Unexpected error while linking program enrollments for Program ${programID}`, + type: 'error', + topic: 'linkProgramEnrollment', + }, + ], + }; + } +} diff --git a/src/ProgramEnrollments/data/api.test.js b/src/ProgramEnrollments/data/api.test.js new file mode 100644 index 000000000..836a39973 --- /dev/null +++ b/src/ProgramEnrollments/data/api.test.js @@ -0,0 +1,62 @@ +import MockAdapter from 'axios-mock-adapter'; +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { lpeSuccessResponse } from './test/linkProgramEnrollment'; +import * as api from './api'; + +describe('Link Program Enrollments API', () => { + let mockAdapter; + + const data = { + programID: '8bee627e-d85e-4a76-be41-d58921da666e', + usernamePairText: 'testuser,verified', + }; + const lpeDetailsApiUrl = `${ + getConfig().LMS_BASE_URL + }/support/link_program_enrollments_details/`; + + const throwError = (errorCode, dataString) => { + const error = new Error(); + error.customAttributes = { + httpErrorStatus: errorCode, + httpErrorResponseData: JSON.stringify(dataString), + }; + throw error; + }; + + beforeEach(() => { + mockAdapter = new MockAdapter(getAuthenticatedHttpClient(), { + onNoMatch: 'throwException', + }); + }); + + afterEach(() => { + mockAdapter.reset(); + }); + + it('Successful api fetch', async () => { + mockAdapter.onPost(lpeDetailsApiUrl).reply(200, lpeSuccessResponse); + + const response = await api.default(data); + expect(response).toEqual(lpeSuccessResponse); + }); + + it('Unsuccessful api fetch', async () => { + const expectedErrors = [ + { + code: null, + dismissible: true, + text: 'Unexpected error while linking program enrollments for Program 8bee627e-d85e-4a76-be41-d58921da666e', + type: 'error', + topic: 'linkProgramEnrollment', + }, + ]; + + mockAdapter + .onPost(lpeDetailsApiUrl) + .reply(() => throwError(500, 'Server Error')); + + const response = await api.default(data); + expect(response.error).toEqual(expectedErrors); + }); +}); diff --git a/src/ProgramEnrollments/data/test/linkProgramEnrollment.js b/src/ProgramEnrollments/data/test/linkProgramEnrollment.js new file mode 100644 index 000000000..0616ed3a2 --- /dev/null +++ b/src/ProgramEnrollments/data/test/linkProgramEnrollment.js @@ -0,0 +1,49 @@ +export const lpeSuccessResponse = { + successes: ["('testuser', 'verified')"], + errors: [], + program_uuid: '8bee627e-d85e-4a76-be41-d58921da666e', + username_pair_text: 'testuser,verified', +}; + +export const lpeErrorResponseAlreadyLinked = { + successes: [], + errors: [ + 'Program enrollment with external_student_key=testuser1 is already linked to target account username=verified', + ], + program_uuid: '8bee627e-d85e-4a76-be41-d58921da666e', + username_pair_text: 'testuser,verified', +}; + +export const lpeErrorResponseInvalidUUID = { + successes: [], + errors: [ + "Supplied program UUID '8bee627e-d85e-4a76-be41-d58921da666e' is not a valid UUID.", + ], + program_uuid: '8bee627e-d85e-4a76-be41-d58921da666e', + username_pair_text: 'testuser,verified', +}; + +export const lpeErrorResponseInvalidUsername = { + successes: [], + errors: ['No user found with username verified'], + program_uuid: '8bee627e-d85e-4a76-be41-d58921da666e', + username_pair_text: 'testuser,verified', +}; + +export const lpeErrorResponseInvalidExternalKey = { + successes: [], + errors: [ + 'No program enrollment found for program uuid=8bee627e-d85e-4a76-be41-d58921da666e and external student key=testuser', + ], + program_uuid: '8bee627e-d85e-4a76-be41-d58921da666e', + username_pair_text: 'testuser,verified', +}; + +export const lpeErrorResponseEmptyValues = { + successes: [], + errors: [ + "You must provide both a program uuid and a series of lines with the format 'external_user_key,lms_username'.", + ], + program_uuid: '', + username_pair_text: '', +}; diff --git a/src/SupportToolsTab/SupportToolsTab.jsx b/src/SupportToolsTab/SupportToolsTab.jsx index 5aec87b4a..2b1e4556e 100644 --- a/src/SupportToolsTab/SupportToolsTab.jsx +++ b/src/SupportToolsTab/SupportToolsTab.jsx @@ -3,14 +3,31 @@ import PropTypes from 'prop-types'; import { Tabs, Tab } from '@edx/paragon'; import { history } from '@edx/frontend-platform'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { FEATURE_BASED_ENROLLMENT_TAB, LEARNER_INFO_TAB, TAB_PATH_MAP } from './constants'; +import { + FEATURE_BASED_ENROLLMENT_TAB, + LEARNER_INFO_TAB, + PROGRAM_ENROLLMENT_TAB, + TAB_PATH_MAP, +} from './constants'; import UserPage from '../users/v2/UserPage'; import FeatureBasedEnrollmentIndexPage from '../FeatureBasedEnrollments/FeatureBasedEnrollmentIndexPage'; +import ProgramEnrollmentsIndexPage from '../ProgramEnrollments/ProgramEnrollmentsIndexPage'; export default function SupportToolsTab({ location }) { let tabKey = LEARNER_INFO_TAB; - if (location.pathname.indexOf(TAB_PATH_MAP[FEATURE_BASED_ENROLLMENT_TAB]) !== -1) { - tabKey = FEATURE_BASED_ENROLLMENT_TAB; + switch (location.pathname) { + case TAB_PATH_MAP[FEATURE_BASED_ENROLLMENT_TAB]: + tabKey = FEATURE_BASED_ENROLLMENT_TAB; + break; + case TAB_PATH_MAP[LEARNER_INFO_TAB]: + tabKey = LEARNER_INFO_TAB; + break; + case TAB_PATH_MAP[PROGRAM_ENROLLMENT_TAB]: + tabKey = PROGRAM_ENROLLMENT_TAB; + break; + default: + tabKey = LEARNER_INFO_TAB; + break; } return ( @@ -40,11 +57,17 @@ export default function SupportToolsTab({ location }) { - +
- + +
+ +
diff --git a/src/SupportToolsTab/SupportToolsTab.test.jsx b/src/SupportToolsTab/SupportToolsTab.test.jsx index 455d1fb9e..5c805324e 100644 --- a/src/SupportToolsTab/SupportToolsTab.test.jsx +++ b/src/SupportToolsTab/SupportToolsTab.test.jsx @@ -32,9 +32,10 @@ describe('Support Tools Main tab', () => { wrapper = mount(); const tabs = wrapper.find('nav.nav-tabs a'); - expect(tabs.length).toEqual(2); + expect(tabs.length).toEqual(3); expect(tabs.at(0).text()).toEqual('Learner Information'); expect(tabs.at(1).text()).toEqual('Feature Based Enrollment'); + expect(tabs.at(2).text()).toEqual('Program Enrollments'); expect(wrapper.find('h2').text()).toEqual('Support Tools'); expect(wrapper.find('p').text()).toEqual( @@ -54,6 +55,7 @@ describe('Support Tools Main tab', () => { expect(history.replace).toHaveBeenCalledWith(TAB_PATH_MAP['feature-based-enrollment']); expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active')); expect(tabs.at(1).html()).toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active')); expect(fbeTab.html()).toEqual(expect.stringContaining('active')); expect(fbeTab.find('label').text()).toEqual('Course ID'); @@ -63,13 +65,21 @@ describe('Support Tools Main tab', () => { expect(history.replace).toHaveBeenCalledWith(TAB_PATH_MAP['learner-information']); expect(tabs.at(0).html()).toEqual(expect.stringContaining('active')); expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active')); expect(learnerTab.html()).toEqual(expect.stringContaining('active')); expect(learnerTab.find('label').text()).toEqual('Username, Email or LMS User ID'); + tabs.at(2).simulate('click'); + tabs = wrapper.find('nav.nav-tabs a'); + expect(history.replace).toHaveBeenCalledWith(TAB_PATH_MAP['program-enrollment']); + expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).toEqual(expect.stringContaining('active')); + history.replace.mockReset(); }); - it('default tab changes based on pathname', () => { + it('default tab changes based on feature-based-enrollment pathname', () => { location = { pathname: TAB_PATH_MAP['feature-based-enrollment'], search: '' }; wrapper = mount(); @@ -77,5 +87,28 @@ describe('Support Tools Main tab', () => { expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active')); expect(tabs.at(1).html()).toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active')); + }); + + it('default tab changes based on learner-information pathname', () => { + location = { pathname: TAB_PATH_MAP['learner-information'], search: '' }; + + wrapper = mount(); + const tabs = wrapper.find('nav.nav-tabs a'); + + expect(tabs.at(0).html()).toEqual(expect.stringContaining('active')); + expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active')); + }); + + it('default tab changes based on program-enrollment pathname', () => { + location = { pathname: TAB_PATH_MAP['program-enrollment'], search: '' }; + + wrapper = mount(); + const tabs = wrapper.find('nav.nav-tabs a'); + + expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).toEqual(expect.stringContaining('active')); }); }); diff --git a/src/SupportToolsTab/constants.js b/src/SupportToolsTab/constants.js index 5009ccc4e..58ec3ad54 100644 --- a/src/SupportToolsTab/constants.js +++ b/src/SupportToolsTab/constants.js @@ -1,7 +1,9 @@ export const LEARNER_INFO_TAB = 'learner-information'; export const FEATURE_BASED_ENROLLMENT_TAB = 'feature-based-enrollment'; +export const PROGRAM_ENROLLMENT_TAB = 'program-enrollment'; export const TAB_PATH_MAP = { [LEARNER_INFO_TAB]: '/v2/learner_information', [FEATURE_BASED_ENROLLMENT_TAB]: '/v2/feature_based_enrollments', + [PROGRAM_ENROLLMENT_TAB]: '/v2/program_enrollment', }; diff --git a/src/index.jsx b/src/index.jsx index 18bb94f57..68bae156d 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -18,6 +18,7 @@ import SupportToolsTab from './SupportToolsTab/SupportToolsTab'; import UserPageV2 from './users/v2/UserPage'; import FBEIndexPage from './FeatureBasedEnrollments/FeatureBasedEnrollmentIndexPage'; import UserMessagesProvider from './userMessages/UserMessagesProvider'; +import ProgramEnrollmentsIndexPage from './ProgramEnrollments/ProgramEnrollmentsIndexPage'; import './index.scss'; @@ -41,6 +42,7 @@ subscribe(APP_READY, () => { + , diff --git a/src/overrides.scss b/src/overrides.scss index 7a5bcaafc..01aeec56a 100644 --- a/src/overrides.scss +++ b/src/overrides.scss @@ -153,3 +153,10 @@ $darkCyan: #00262b; font-weight: normal; } } + +.link-program-enrollments { + border: none; + border-radius: 0; + border-right: 1px solid lightgray; + padding-left: 0; +} diff --git a/src/utils/index.js b/src/utils/index.js index 8541d33ff..88d7064c1 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -55,3 +55,27 @@ export function sortedCompareDates(x, y, asc) { const b = new Date(y); return asc ? a - b : b - a; } + +export function extractMessageTuple(message) { + /* + Support Tools API sends an array of success tuples for + Link Program Enrollments of format + [ + '('external_user_key', 'lms_username')', + '('external_user_key', 'lms_username')', + ] + that are identified as strings by the JS. + This function removes the overhead characters and + splits the string into an array of format + [ + [external_user_key,lms_username], + [external_user_key,lms_username], + ] + */ + return message + .replace('(', '') + .replace(')', '') + .replace(/'/g, '') + .replace(/ /g, '') + .split(','); +} diff --git a/src/utils/index.test.js b/src/utils/index.test.js index 3c24e070c..e375884dc 100644 --- a/src/utils/index.test.js +++ b/src/utils/index.test.js @@ -8,6 +8,7 @@ import { titleCase, sortedCompareDates, isValidCourseID, + extractMessageTuple, } from './index'; describe('Test Utils', () => { @@ -172,4 +173,12 @@ describe('Test Utils', () => { )).toEqual(dscSortedDates); }); }); + + describe('Extract Message Tuples', () => { + const message = "('external_user_key', 'lms_username')"; + expect(extractMessageTuple(message)).toEqual([ + 'external_user_key', + 'lms_username', + ]); + }); });