[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:
1
.env
1
.env
@@ -38,3 +38,4 @@ TWITTER_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SESSION_COOKIE_DOMAIN=''
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
|
||||
@@ -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=''
|
||||
|
||||
@@ -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=''
|
||||
|
||||
35
src/generic/notices/NoticesProvider.jsx
Normal file
35
src/generic/notices/NoticesProvider.jsx
Normal 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;
|
||||
61
src/generic/notices/NoticesProvider.test.jsx
Normal file
61
src/generic/notices/NoticesProvider.test.jsx
Normal 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/');
|
||||
});
|
||||
});
|
||||
25
src/generic/notices/api.js
Normal file
25
src/generic/notices/api.js
Normal 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;
|
||||
};
|
||||
1
src/generic/notices/index.js
Normal file
1
src/generic/notices/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './NoticesProvider';
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user