diff --git a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx index 092bcfe7..05470194 100644 --- a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx +++ b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx @@ -9,6 +9,7 @@ const getLearnerHeaderMenu = ( courseSearchUrl, authenticatedUser, exploreCoursesClick, + programsEnabled = false, ) => ({ mainMenu: [ { @@ -17,11 +18,11 @@ const getLearnerHeaderMenu = ( content: formatMessage(messages.course), isActive: true, }, - { + ...(programsEnabled ? [{ type: 'item', href: `${urls.programsUrl()}`, content: formatMessage(messages.program), - }, + }] : []), { type: 'item', href: `${urls.baseAppUrl(courseSearchUrl)}`, diff --git a/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap index 80d09001..34bcdc1f 100644 --- a/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap +++ b/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap @@ -12,11 +12,6 @@ exports[`LearnerDashboardHeader render 1`] = ` "isActive": true, "type": "item", }, - { - "content": "Programs", - "href": "http://localhost:18000/dashboard/programs", - "type": "item", - }, { "content": "Discover New", "href": "http://localhost:18000/course-search-url", diff --git a/src/containers/LearnerDashboardHeader/hooks.js b/src/containers/LearnerDashboardHeader/hooks.js index 115322c1..e3eecf8a 100644 --- a/src/containers/LearnerDashboardHeader/hooks.js +++ b/src/containers/LearnerDashboardHeader/hooks.js @@ -5,6 +5,7 @@ import track from 'tracking'; import { StrictDict } from 'utils'; import { linkNames } from 'tracking/constants'; +import { apiHooks } from 'hooks'; import getLearnerHeaderMenu from './LearnerDashboardMenu'; import * as module from './hooks'; @@ -30,8 +31,9 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou export const useLearnerDashboardHeaderMenu = ({ courseSearchUrl, authenticatedUser, exploreCoursesClick, }) => { + const { enabled: programsEnabled } = apiHooks.useProgramsConfig(); const { formatMessage } = useIntl(); - return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick); + return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick, programsEnabled); }; export const useLearnerDashboardHeaderData = () => { diff --git a/src/containers/LearnerDashboardHeader/hooks.test.js b/src/containers/LearnerDashboardHeader/hooks.test.js index c9bac080..e724c491 100644 --- a/src/containers/LearnerDashboardHeader/hooks.test.js +++ b/src/containers/LearnerDashboardHeader/hooks.test.js @@ -21,6 +21,11 @@ jest.mock('tracking', () => ({ findCoursesClicked: jest.fn(), }, })); +jest.mock('hooks', () => ({ + apiHooks: { + useProgramsConfig: jest.fn(() => ({})), + }, +})); const url = 'http://example.com'; @@ -56,7 +61,7 @@ describe('LearnerDashboardHeader hooks', () => { username: 'test', }; const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser }); - expect(learnerHomeHeaderMenu.mainMenu.length).toBe(3); + expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2); }); }); diff --git a/src/containers/LearnerDashboardHeader/index.test.jsx b/src/containers/LearnerDashboardHeader/index.test.jsx index 4e552925..4c455eea 100644 --- a/src/containers/LearnerDashboardHeader/index.test.jsx +++ b/src/containers/LearnerDashboardHeader/index.test.jsx @@ -3,6 +3,7 @@ import { shallow } from '@edx/react-unit-test-utils'; import Header from '@edx/frontend-component-header'; import urls from 'data/services/lms/urls'; +import { apiHooks } from 'hooks'; import LearnerDashboardHeader from '.'; import { findCoursesNavClicked } from './hooks'; @@ -12,6 +13,9 @@ jest.mock('hooks', () => ({ courseSearchUrl: '/course-search-url', })), }, + apiHooks: { + useProgramsConfig: jest.fn(() => ({})), + }, })); jest.mock('./hooks', () => ({ ...jest.requireActual('./hooks'), @@ -29,7 +33,7 @@ describe('LearnerDashboardHeader', () => { expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1); expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1); expect(wrapper.instance.findByType(Header)).toHaveLength(1); - wrapper.instance.findByType(Header)[0].props.mainMenuItems[2].onClick(); + wrapper.instance.findByType(Header)[0].props.mainMenuItems[1].onClick(); expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url')); expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(0); }); @@ -38,5 +42,11 @@ describe('LearnerDashboardHeader', () => { mergeConfig({ SUPPORT_URL: 'http://localhost:18000/support' }); const wrapper = shallow(); expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(1); + expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(2); + }); + test('should display Programs link if the service is configured in the backend', () => { + apiHooks.useProgramsConfig.mockReturnValue({ enabled: true }); + const wrapper = shallow(); + expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(3); }); }); diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index 6f9acd56..915b52a9 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -20,6 +20,8 @@ export const initializeList = ({ user } = {}) => get( stringifyUrl(urls.getInitApiUrl(), { [apiKeys.user]: user }), ); +export const getProgramsConfig = () => get(urls.programsConfigUrl()); + export const updateEntitlementEnrollment = ({ uuid, courseId }) => post( urls.entitlementEnrollment(uuid), { [apiKeys.courseRunId]: courseId }, @@ -73,6 +75,7 @@ export const createCreditRequest = ({ providerId, courseId, username }) => post( export default { initializeList, + getProgramsConfig, unenrollFromCourse, updateEmailSettings, updateEntitlementEnrollment, diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index 30c2bbf1..789d614c 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -22,6 +22,7 @@ export const baseAppUrl = (url) => updateUrl(getBaseUrl(), url); export const learningMfeUrl = (url) => updateUrl(getConfig().LEARNING_BASE_URL, url); // static view url +const programsConfigUrl = () => baseAppUrl('/config/programs'); const programsUrl = () => baseAppUrl('/dashboard/programs'); export const creditPurchaseUrl = (courseId) => `${getEcommerceUrl()}/credit/checkout/${courseId}/`; @@ -37,6 +38,7 @@ export default StrictDict({ event, getInitApiUrl, learningMfeUrl, + programsConfigUrl, programsUrl, updateEmailSettings, }); diff --git a/src/hooks/api.js b/src/hooks/api.js index d64a1185..24a844f8 100644 --- a/src/hooks/api.js +++ b/src/hooks/api.js @@ -1,5 +1,6 @@ import React from 'react'; +import { logError } from '@edx/frontend-platform/logging'; import { AppContext } from '@edx/frontend-platform/react'; import { RequestKeys } from 'data/constants/requests'; @@ -31,6 +32,25 @@ export const useInitializeApp = () => { }); }; +export const useProgramsConfig = () => { + const [config, setConfig] = React.useState({}); + + React.useEffect(() => { + const fetchProgramsConfig = async () => { + try { + const { data } = await api.getProgramsConfig(); + setConfig(data); + } catch (error) { + logError(`Error accessing programs configuration ${error}`); + } + }; + + fetchProgramsConfig(); + }, []); + + return config; +}; + export const useNewEntitlementEnrollment = (cardId) => { const { uuid } = reduxHooks.useCardEntitlementData(cardId); const onSuccess = module.useInitializeApp(); diff --git a/src/hooks/api.test.js b/src/hooks/api.test.js index cd1a5b6b..ab18a6df 100644 --- a/src/hooks/api.test.js +++ b/src/hooks/api.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { logError } from '@edx/frontend-platform/logging'; import { AppContext } from '@edx/frontend-platform/react'; import { keyStore } from 'utils'; import { RequestKeys } from 'data/constants/requests'; @@ -10,9 +11,14 @@ import * as apiHooks from './api'; const reduxKeys = keyStore(reduxHooks); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + jest.mock('data/services/lms/utils', () => ({ post: jest.fn((...args) => ({ post: args })), })); + jest.mock('data/services/lms/api', () => ({ initializeList: jest.fn(), updateEntitlementEnrollment: jest.fn(), @@ -20,7 +26,9 @@ jest.mock('data/services/lms/api', () => ({ deleteEntitlementEnrollment: jest.fn(), updateEmailSettings: jest.fn(), createCreditRequest: jest.fn(), + getProgramsConfig: jest.fn(), })); + jest.mock('data/redux/hooks', () => ({ useCardCourseRunData: jest.fn(), useCardCreditData: jest.fn(), @@ -110,6 +118,34 @@ describe('api hooks', () => { }); }); + describe('useProgramsConfig', () => { + let mockState; + const setState = jest.fn((newState) => { Object.assign(mockState, newState); }); + beforeEach(() => { + mockState = {}; + React.useState.mockReturnValue([mockState, setState]); + }); + + it('should return the programs configuration when the API call is successful', async () => { + api.getProgramsConfig.mockResolvedValue({ data: { enabled: true } }); + const config = apiHooks.useProgramsConfig(); + const [cb] = React.useEffect.mock.calls[0]; + await cb(); + expect(setState).toHaveBeenCalled(); + expect(config).toEqual({ enabled: true }); + }); + + it('should return an empty object if the api call fails', async () => { + mockState = {}; + api.getProgramsConfig.mockRejectedValue(new Error('error test')); + const config = apiHooks.useProgramsConfig(); + const [cb] = React.useEffect.mock.calls[0]; + await cb(); + expect(config).toEqual({}); + expect(logError).toHaveBeenCalled(); + }); + }); + describe('entitlement enrollment hooks', () => { beforeEach(() => { jest.spyOn(apiHooks, moduleKeys.useInitializeApp).mockReturnValue(initializeApp);