[feat] Add notices redirect to learning MFE (#667)

* [feat] MICROBA-1523 Add notices redirect to learning MFE

To support the notices plugin on platform, this adds a redirect to the
course home page. If a user lands on that page and has not acknowledged
a notice, the user will be redirected to notice instead of the course
home.
This commit is contained in:
Thomas Tracy
2021-10-06 14:25:05 -04:00
committed by GitHub
parent 0bc7faaa56
commit 3fcc0d87c9
8 changed files with 172 additions and 43 deletions

1
.env
View File

@@ -38,3 +38,4 @@ TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
SESSION_COOKIE_DOMAIN=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -38,3 +38,4 @@ TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
SESSION_COOKIE_DOMAIN='localhost'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -37,3 +37,4 @@ TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { getNotices } from './api';
/**
* This component uses the platform-plugin-notices plugin to function.
* If the user has an unacknowledged notice, they will be rerouted off
* course home and onto a full-screen notice page. If the plugin is not
* installed, or there are no notices, we just passthrough this component.
*/
const NoticesProvider = ({ children }) => {
const [isRedirected, setIsRedirected] = useState();
useEffect(async () => {
if (getConfig().ENABLE_NOTICES) {
const data = await getNotices();
if (data && data.results) {
const { results } = data;
setIsRedirected(true);
window.location.replace(`${results[0]}?next=${window.location.href}`);
}
}
}, []);
return (
<div>
{isRedirected === true ? null : children}
</div>
);
};
NoticesProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default NoticesProvider;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
initializeMockApp,
render,
act,
} from '../../setupTest';
import NoticesProvider from './NoticesProvider';
import { getNotices } from './api';
jest.mock('./api', () => ({
getNotices: jest.fn(),
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('NoticesProvider', () => {
beforeAll(async () => {
jest.resetModules();
await initializeMockApp();
});
function buildAndRender() {
render(
<NoticesProvider>
<div />
</NoticesProvider>,
);
}
it('does not call api if ENABLE_NOTICES is false', () => {
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: false }));
buildAndRender();
expect(getNotices).toHaveBeenCalledTimes(0);
});
it('redirects user on notice returned from API', async () => {
const redirectUrl = 'http://example.com/test_route';
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: true }));
getNotices.mockImplementation(() => ({ results: [redirectUrl] }));
delete window.location;
window.location = { replace: jest.fn() };
process.env.ENABLE_NOTICES = true;
await act(() => buildAndRender());
expect(window.location.replace).toHaveBeenCalledWith(`${redirectUrl}?next=${window.location.href}`);
});
it('does not redirect on no data', async () => {
getNotices.mockImplementation(() => ({}));
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: true }));
delete window.location;
window.location = { replace: jest.fn() };
process.env.ENABLE_NOTICES = true;
await act(() => buildAndRender());
expect(window.location.replace).toHaveBeenCalledTimes(0);
expect(window.location.toString() === 'http://localhost/');
});
});

View File

@@ -0,0 +1,25 @@
/* eslint-disable import/prefer-default-export */
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';
export const getNotices = async () => {
const authenticatedUser = getAuthenticatedUser();
const url = new URL(`${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`);
if (authenticatedUser) {
try {
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
return data;
} catch (e) {
// we will just swallow error, as that probably means the notices app is not installed.
// Notices are not necessary for the rest of courseware to function.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. This probably happened because the notices plugin is not installed on platform.`);
} else {
logError(e);
}
}
}
return null;
};

View File

@@ -0,0 +1 @@
export { default } from './NoticesProvider';

View File

@@ -28,54 +28,57 @@ import { TabContainer } from './tab-page';
import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
import { fetchCourse } from './courseware/data';
import initializeStore from './store';
import NoticesProvider from './generic/notices';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={initializeStore()}>
<UserMessagesProvider>
<Switch>
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
]}
render={({ match }) => (
<TabContainer
tab="progress"
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
slice="courseHome"
>
<ProgressTab />
<NoticesProvider>
<UserMessagesProvider>
<Switch>
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
)}
/>
<PageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
</UserMessagesProvider>
</PageRoute>
<PageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
]}
render={({ match }) => (
<TabContainer
tab="progress"
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
slice="courseHome"
>
<ProgressTab />
</TabContainer>
)}
/>
<PageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
</UserMessagesProvider>
</NoticesProvider>
</AppProvider>,
document.getElementById('root'),
);
@@ -93,6 +96,7 @@ initialize({
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
SOCIAL_UTM_MILESTONE_CAMPAIGN: process.env.SOCIAL_UTM_MILESTONE_CAMPAIGN || null,