Compare commits
12 Commits
abutterwor
...
kdmccormic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e11b6f27 | ||
|
|
a6522f5983 | ||
|
|
a21be5d83c | ||
|
|
521483b836 | ||
|
|
6a5e906cbe | ||
|
|
9b76cc4d97 | ||
|
|
a10e6c2826 | ||
|
|
b4fbd1cf83 | ||
|
|
37610ab181 | ||
|
|
70428228a5 | ||
|
|
1dc069dbbf | ||
|
|
9b72380dea |
@@ -1,16 +1,16 @@
|
||||
NODE_ENV='development'
|
||||
PORT=2000
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:2000'
|
||||
BASE_URL='localhost:19000/learning'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
ORDER_HISTORY_URL='localhost:19000/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
|
||||
8294
package-lock.json
generated
8294
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"dev-build": "fedx-scripts webpack-dev",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
@@ -37,7 +38,7 @@
|
||||
"@edx/frontend-component-footer": "^10.0.6",
|
||||
"@edx/frontend-component-header": "^2.0.3",
|
||||
"@edx/frontend-platform": "^1.3.1",
|
||||
"@edx/paragon": "^7.2.0",
|
||||
"@edx/paragon": "^7.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.12.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^3.0.0",
|
||||
"@edx/frontend-build": "github:edx/frontend-build#kdmccormick/devstack-frontends",
|
||||
"codecov": "^3.6.1",
|
||||
"es-check": "^5.1.0",
|
||||
"glob": "^7.1.6",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Route, Switch, useParams, useRouteMatch,
|
||||
} from 'react-router-dom';
|
||||
import SequenceContainer from './SequenceContainer';
|
||||
|
||||
export default (props) => {
|
||||
const { path } = useRouteMatch();
|
||||
const { courseId } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>CourseContainer</div>
|
||||
<Switch>
|
||||
<Route exact path={path}>
|
||||
<h3>{path} find the sequence and redirect</h3>
|
||||
</Route>
|
||||
|
||||
<Route exact path={`${path}/home`}>
|
||||
<h3>Course Home</h3>
|
||||
</Route>
|
||||
{/* CoursewareContainer ???? */}
|
||||
<Route
|
||||
path={`${path}/sequence/:sequenceId`}
|
||||
component={SequenceContainer}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
};
|
||||
30
src/CoursewareRedirect.jsx
Normal file
30
src/CoursewareRedirect.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import PageLoading from './PageLoading';
|
||||
|
||||
export default () => {
|
||||
const { path } = useRouteMatch();
|
||||
return (
|
||||
<div className="flex-grow-1">
|
||||
<PageLoading srMessage={(
|
||||
<FormattedMessage
|
||||
id="learn.redirect.interstitial.message"
|
||||
description="The screen-reader message when a page is about to redirect"
|
||||
defaultMessage="Redirecting..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${path}/course-home/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/course/`);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -33,5 +33,5 @@ export default class PageLoading extends Component {
|
||||
}
|
||||
|
||||
PageLoading.propTypes = {
|
||||
srMessage: PropTypes.string.isRequired,
|
||||
srMessage: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Route, Switch, useParams, useRouteMatch,
|
||||
} from 'react-router-dom';
|
||||
|
||||
export default (props) => {
|
||||
const { path } = useRouteMatch();
|
||||
const { courseId, sequenceId } = useParams();
|
||||
// const { courseId } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>SequenceContainer</div>
|
||||
|
||||
{courseId} <br />
|
||||
{sequenceId}
|
||||
<Switch>
|
||||
<Route exact path={path}>
|
||||
<h3>{path} find the unit and redirect</h3>
|
||||
</Route>
|
||||
<Route exact path={`${path}/home`}>
|
||||
<h3>Course Home</h3>
|
||||
</Route>
|
||||
<Route
|
||||
path={`${path}/sequence/:sequenceId`}
|
||||
render={(routeProps) => (
|
||||
<SequenceContainer {...routeProps} courseId={courseId} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,11 @@ import CourseDates from './CourseDates';
|
||||
import Section from './Section';
|
||||
import { useModel } from '../model-store';
|
||||
|
||||
const EnrollmentAlert = React.lazy(() => import('../enrollment-alert'));
|
||||
// Note that we import from the component files themselves in the enrollment-alert package.
|
||||
// This is because Reacy.lazy() requires that we import() from a file with a Component as it's
|
||||
// default export.
|
||||
// See React.lazy docs here: https://reactjs.org/docs/code-splitting.html#reactlazy
|
||||
const { EnrollmentAlert, StaffEnrollmentAlert } = React.lazy(() => import('../enrollment-alert'));
|
||||
const LogistrationAlert = React.lazy(() => import('../logistration-alert'));
|
||||
|
||||
export default function CourseHome({
|
||||
@@ -49,6 +53,7 @@ export default function CourseHome({
|
||||
className="mb-3"
|
||||
customAlerts={{
|
||||
clientEnrollmentAlert: EnrollmentAlert,
|
||||
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
|
||||
clientLogistrationAlert: LogistrationAlert,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function SequenceLink({ id, courseId }) {
|
||||
const sequence = useModel('sequences', id);
|
||||
return (
|
||||
<div className="ml-4">
|
||||
<Link to={`/course/${courseId}/${id}`}>{sequence.title}</Link>
|
||||
<Link to={`/learning/course/${courseId}/${id}`}>{sequence.title}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { history, getConfig } from '@edx/frontend-platform';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import { useRouteMatch, Redirect } from 'react-router';
|
||||
import {
|
||||
fetchCourse,
|
||||
fetchSequence,
|
||||
@@ -22,7 +22,7 @@ function useUnitNavigationHandler(courseId, sequenceId, unitId) {
|
||||
const dispatch = useDispatch();
|
||||
return useCallback((nextUnitId) => {
|
||||
dispatch(checkBlockCompletion(courseId, sequenceId, unitId));
|
||||
history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
||||
history.push(`/learning/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
||||
}, [courseId, sequenceId]);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ function useNextSequenceHandler(courseId, sequenceId) {
|
||||
return useCallback(() => {
|
||||
if (nextSequence !== null) {
|
||||
const nextUnitId = nextSequence.unitIds[0];
|
||||
history.push(`/course/${courseId}/${nextSequence.id}/${nextUnitId}`);
|
||||
history.push(`/learning/course/${courseId}/${nextSequence.id}/${nextUnitId}`);
|
||||
}
|
||||
}, [courseStatus, sequenceStatus, sequenceId]);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ function usePreviousSequenceHandler(courseId, sequenceId) {
|
||||
return useCallback(() => {
|
||||
if (previousSequence !== null) {
|
||||
const previousUnitId = previousSequence.unitIds[previousSequence.unitIds.length - 1];
|
||||
history.push(`/course/${courseId}/${previousSequence.id}/${previousUnitId}`);
|
||||
history.push(`/learning/course/${courseId}/${previousSequence.id}/${previousUnitId}`);
|
||||
}
|
||||
}, [courseStatus, sequenceStatus, sequenceId]);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ function useContentRedirect(courseStatus, sequenceStatus) {
|
||||
useEffect(() => {
|
||||
if (courseStatus === 'loaded' && !sequenceId) {
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
history.replace(`/course/${courseId}/${firstSequenceId}`);
|
||||
history.replace(`/learning/course/${courseId}/${firstSequenceId}`);
|
||||
}
|
||||
}, [courseStatus, sequenceId]);
|
||||
|
||||
@@ -102,7 +102,7 @@ function useContentRedirect(courseStatus, sequenceStatus) {
|
||||
const unitIndex = sequence.position || 0;
|
||||
const nextUnitId = sequence.unitIds[unitIndex];
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
history.replace(`/course/${courseId}/${sequence.id}/${nextUnitId}`);
|
||||
history.replace(`/learning/course/${courseId}/${sequence.id}/${nextUnitId}`);
|
||||
}
|
||||
}
|
||||
}, [sequenceStatus, sequenceId, unitId]);
|
||||
@@ -120,21 +120,6 @@ function useSavedSequencePosition(courseId, sequenceId, unitId) {
|
||||
}, [unitId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user away from the app if they don't have access to view this course.
|
||||
*
|
||||
* @param {*} courseStatus
|
||||
* @param {*} course
|
||||
*/
|
||||
function useAccessDeniedRedirect(courseStatus, courseId) {
|
||||
const course = useModel('courses', courseId);
|
||||
useEffect(() => {
|
||||
if (courseStatus === 'loaded' && !course.userHasAccess && !course.isStaff) {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${course.id}/course/`);
|
||||
}
|
||||
}, [courseStatus, course]);
|
||||
}
|
||||
|
||||
export default function CoursewareContainer() {
|
||||
const { params } = useRouteMatch();
|
||||
const {
|
||||
@@ -173,11 +158,14 @@ export default function CoursewareContainer() {
|
||||
const previousSequenceHandler = usePreviousSequenceHandler(courseId, sequenceId);
|
||||
const unitNavigationHandler = useUnitNavigationHandler(courseId, sequenceId, routeUnitId);
|
||||
|
||||
useAccessDeniedRedirect(courseStatus, courseId);
|
||||
useContentRedirect(courseStatus, sequenceStatus);
|
||||
useExamRedirect(sequenceId);
|
||||
useSavedSequencePosition(courseId, sequenceId, routeUnitId);
|
||||
|
||||
if (courseStatus === 'denied') {
|
||||
return <Redirect to={`/learning/redirect/course-home/${courseId}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-grow-1 d-flex flex-column">
|
||||
<Course
|
||||
|
||||
@@ -18,7 +18,12 @@ import Calculator from './calculator';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../model-store';
|
||||
|
||||
const EnrollmentAlert = React.lazy(() => import('../../enrollment-alert'));
|
||||
// Note that we import from the component files themselves in the enrollment-alert package.
|
||||
// This is because Reacy.lazy() requires that we import() from a file with a Component as it's
|
||||
// default export.
|
||||
// See React.lazy docs here: https://reactjs.org/docs/code-splitting.html#reactlazy
|
||||
const EnrollmentAlert = React.lazy(() => import('../../enrollment-alert/EnrollmentAlert'));
|
||||
const StaffEnrollmentAlert = React.lazy(() => import('../../enrollment-alert/StaffEnrollmentAlert'));
|
||||
const LogistrationAlert = React.lazy(() => import('../../logistration-alert'));
|
||||
|
||||
function Course({
|
||||
@@ -70,6 +75,7 @@ function Course({
|
||||
topic="course"
|
||||
customAlerts={{
|
||||
clientEnrollmentAlert: EnrollmentAlert,
|
||||
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
|
||||
clientLogistrationAlert: LogistrationAlert,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,14 +14,14 @@ function InstructorToolbar(props) {
|
||||
<div className="container-fluid py-3 d-md-flex justify-content-end align-items-center">
|
||||
<div className="flex-grow-1">
|
||||
<Collapsible.Advanced className="mr-5 mb-md-0">
|
||||
You are currently previewing the new learning sequence experience.
|
||||
You are currently previewing the new learning sequence experience.
|
||||
<Collapsible.Trigger className="d-inline-block ml-2" style={{ cursor: 'pointer' }}>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<span style={{ borderBottom: 'solid 1px white' }}>More info</span> →
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body>
|
||||
This preview is to allow for early content testing, especially for custom content blocks, with the goal of ensuring it renders as expected in the next experience. You can learn more through the following <a className="text-white" style={{ textDecoration: 'underline' }} href="https://partners.edx.org/announcements/author-preview-learning-sequence-experience-update" target="blank" rel="noopener">Partner Portal post</a>. Please report any issues or provide <a className="text-white" style={{ textDecoration: 'underline' }} target="blank" rel="noopener" href="https://forms.gle/R6jMYJNTCj1vgC1D6">feedback using the linked form</a>.
|
||||
This preview is to allow for early content testing, especially for custom content blocks, with the goal of ensuring it renders as expected in the next experience. You can learn more through the following <a className="text-white" style={{ textDecoration: 'underline' }} href="https://partners.edx.org/announcements/author-preview-learning-sequence-experience-update" target="blank" rel="noopener">Partner Portal post</a>. Please report any issues or provide <a className="text-white" style={{ textDecoration: 'underline' }} target="blank" rel="noopener" href="https://forms.gle/R6jMYJNTCj1vgC1D6">feedback using the linked form</a>.
|
||||
<Collapsible.Trigger className="d-inline-block ml-2" style={{ cursor: 'pointer' }}>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<span style={{ borderBottom: 'solid 1px white' }}>Close</span> ×
|
||||
|
||||
@@ -345,7 +345,7 @@ class Calculator extends Component {
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type"
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type1"
|
||||
defaultMessage="{exponentSyntax} and the exponent"
|
||||
values={{
|
||||
exponentSyntax: '10^',
|
||||
@@ -357,7 +357,7 @@ class Calculator extends Component {
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type"
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type2"
|
||||
defaultMessage="{notationSyntax} notation"
|
||||
values={{
|
||||
notationSyntax: 'e',
|
||||
@@ -366,7 +366,7 @@ class Calculator extends Component {
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type"
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type3"
|
||||
defaultMessage="{notationSyntax} and the exponent"
|
||||
values={{
|
||||
notationSyntax: '1e',
|
||||
|
||||
@@ -12,7 +12,7 @@ function ContentLock({
|
||||
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
|
||||
}) {
|
||||
const handleClick = useCallback(() => {
|
||||
history.push(`/course/${courseId}/${prereqId}`);
|
||||
history.push(`/learning/course/${courseId}/${prereqId}`);
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,11 @@ export function sequenceIdsSelector(state) {
|
||||
if (state.courseware.courseStatus !== 'loaded') {
|
||||
return [];
|
||||
}
|
||||
const { sectionIds } = state.models.courses[state.courseware.courseId];
|
||||
let sequenceIds = [];
|
||||
sectionIds.forEach(sectionId => {
|
||||
sequenceIds = [...sequenceIds, ...state.models.sections[sectionId].sequenceIds];
|
||||
});
|
||||
const { sectionIds = [] } = state.models.courses[state.courseware.courseId];
|
||||
|
||||
const sequenceIds = sectionIds
|
||||
.flatMap(sectionId => state.models.sections[sectionId].sequenceIds);
|
||||
|
||||
return sequenceIds;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ export function firstSequenceIdSelector(state) {
|
||||
if (state.courseware.courseStatus !== 'loaded') {
|
||||
return null;
|
||||
}
|
||||
const sectionId = state.models.courses[state.courseware.courseId].sectionIds[0];
|
||||
return state.models.sections[sectionId].sequenceIds[0];
|
||||
const { sectionIds = [] } = state.models.courses[state.courseware.courseId];
|
||||
|
||||
if (sectionIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.models.sections[sectionIds[0]].sequenceIds[0];
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ function normalizeMetadata(metadata) {
|
||||
start: metadata.start,
|
||||
enrollmentMode: metadata.enrollment.mode,
|
||||
isEnrolled: metadata.enrollment.is_active,
|
||||
userHasAccess: metadata.user_has_access,
|
||||
isStaff: metadata.user_has_staff_access,
|
||||
canLoadCourseware: metadata.can_load_courseware,
|
||||
isStaff: metadata.is_staff,
|
||||
verifiedMode: camelCaseObject(metadata.verified_mode),
|
||||
tabs: camelCaseObject(metadata.tabs),
|
||||
showCalculator: metadata.show_calculator,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
export const LOADING = 'loading';
|
||||
export const LOADED = 'loaded';
|
||||
export const FAILED = 'failed';
|
||||
export const DENIED = 'denied';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'courseware',
|
||||
@@ -26,6 +27,10 @@ const slice = createSlice({
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = FAILED;
|
||||
},
|
||||
fetchCourseDenied: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = DENIED;
|
||||
},
|
||||
fetchSequenceRequest: (state, { payload }) => {
|
||||
state.sequenceId = payload.sequenceId;
|
||||
state.sequenceStatus = LOADING;
|
||||
@@ -45,6 +50,7 @@ export const {
|
||||
fetchCourseRequest,
|
||||
fetchCourseSuccess,
|
||||
fetchCourseFailure,
|
||||
fetchCourseDenied,
|
||||
fetchSequenceRequest,
|
||||
fetchSequenceSuccess,
|
||||
fetchSequenceFailure,
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
getSequenceMetadata,
|
||||
} from './api';
|
||||
import {
|
||||
addModelsMap, updateModel, updateModels, updateModelsMap,
|
||||
addModelsMap, updateModel, updateModels, updateModelsMap, addModel,
|
||||
} from '../model-store';
|
||||
import {
|
||||
fetchCourseRequest,
|
||||
fetchCourseSuccess,
|
||||
fetchCourseFailure,
|
||||
fetchCourseDenied,
|
||||
fetchSequenceRequest,
|
||||
fetchSequenceSuccess,
|
||||
fetchSequenceFailure,
|
||||
@@ -19,39 +20,66 @@ import {
|
||||
export function fetchCourse(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchCourseRequest({ courseId }));
|
||||
Promise.all([
|
||||
getCourseBlocks(courseId),
|
||||
Promise.allSettled([
|
||||
getCourseMetadata(courseId),
|
||||
]).then(([
|
||||
{
|
||||
courses, sections, sequences, units,
|
||||
},
|
||||
course,
|
||||
]) => {
|
||||
dispatch(addModelsMap({
|
||||
modelType: 'courses',
|
||||
modelsMap: courses,
|
||||
}));
|
||||
dispatch(updateModel({
|
||||
modelType: 'courses',
|
||||
model: course,
|
||||
}));
|
||||
dispatch(addModelsMap({
|
||||
modelType: 'sections',
|
||||
modelsMap: sections,
|
||||
}));
|
||||
// We update for sequences and units because the sequence metadata may have come back first.
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'sequences',
|
||||
modelsMap: sequences,
|
||||
}));
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'units',
|
||||
modelsMap: units,
|
||||
}));
|
||||
dispatch(fetchCourseSuccess({ courseId }));
|
||||
}).catch((error) => {
|
||||
logError(error);
|
||||
getCourseBlocks(courseId),
|
||||
]).then(([courseMetadataResult, courseBlocksResult]) => {
|
||||
if (courseMetadataResult.status === 'fulfilled') {
|
||||
dispatch(addModel({
|
||||
modelType: 'courses',
|
||||
model: courseMetadataResult.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (courseBlocksResult.status === 'fulfilled') {
|
||||
const {
|
||||
courses, sections, sequences, units,
|
||||
} = courseBlocksResult.value;
|
||||
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'courses',
|
||||
modelsMap: courses,
|
||||
}));
|
||||
dispatch(addModelsMap({
|
||||
modelType: 'sections',
|
||||
modelsMap: sections,
|
||||
}));
|
||||
// We update for sequences and units because the sequence metadata may have come back first.
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'sequences',
|
||||
modelsMap: sequences,
|
||||
}));
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'units',
|
||||
modelsMap: units,
|
||||
}));
|
||||
}
|
||||
|
||||
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
|
||||
const fetchedBlocks = courseBlocksResult.status === 'fulfilled';
|
||||
|
||||
// Log errors for each request if needed. Course block failures may occur
|
||||
// even if the course metadata request is successful
|
||||
if (!fetchedBlocks) {
|
||||
logError(courseBlocksResult.reason);
|
||||
}
|
||||
if (!fetchedMetadata) {
|
||||
logError(courseMetadataResult.reason);
|
||||
}
|
||||
|
||||
if (fetchedMetadata) {
|
||||
if (courseMetadataResult.value.canLoadCourseware && fetchedBlocks) {
|
||||
// User has access
|
||||
dispatch(fetchCourseSuccess({ courseId }));
|
||||
return;
|
||||
}
|
||||
// User either doesn't have access or only has partial access
|
||||
// (can't access course blocks)
|
||||
dispatch(fetchCourseDenied({ courseId }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Definitely an error happening
|
||||
dispatch(fetchCourseFailure({ courseId }));
|
||||
});
|
||||
};
|
||||
|
||||
24
src/enrollment-alert/StaffEnrollmentAlert.jsx
Normal file
24
src/enrollment-alert/StaffEnrollmentAlert.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Alert from '../user-messages/Alert';
|
||||
import messages from './messages';
|
||||
|
||||
function StaffEnrollmentAlert({ intl }) {
|
||||
return (
|
||||
<Alert type="info" dismissible>
|
||||
{intl.formatMessage(messages['learning.staff.enrollment.alert'])}
|
||||
{' '}
|
||||
<a href={`${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`}>
|
||||
{intl.formatMessage(messages['learning.enrollment.enroll.now'])}
|
||||
</a>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
StaffEnrollmentAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StaffEnrollmentAlert);
|
||||
@@ -10,14 +10,19 @@ export function useEnrollmentAlert(courseId) {
|
||||
const isEnrolled = course && course.isEnrolled;
|
||||
useEffect(() => {
|
||||
if (course && course.isEnrolled !== undefined) {
|
||||
if (!course.isEnrolled) {
|
||||
setAlertId(add({
|
||||
code: 'clientEnrollmentAlert',
|
||||
dismissible: false,
|
||||
type: 'error',
|
||||
topic: 'course',
|
||||
}));
|
||||
} else if (alertId !== null) {
|
||||
if (!course.isEnrolled && alertId === null) {
|
||||
if (course.isStaff) {
|
||||
setAlertId(add({
|
||||
code: 'clientStaffEnrollmentAlert',
|
||||
topic: 'course',
|
||||
}));
|
||||
} else {
|
||||
setAlertId(add({
|
||||
code: 'clientEnrollmentAlert',
|
||||
topic: 'course',
|
||||
}));
|
||||
}
|
||||
} else if (course.isEnrolled && alertId !== null) {
|
||||
remove(alertId);
|
||||
setAlertId(null);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default } from './EnrollmentAlert';
|
||||
export { default as EnrollmentAlert } from './EnrollmentAlert';
|
||||
export { default as StaffEnrollmentAlert } from './StaffEnrollmentAlert';
|
||||
export { useEnrollmentAlert } from './hooks';
|
||||
|
||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'You must be enrolled in the course to see course content.',
|
||||
description: 'Message shown to indicate that a user needs to enroll in a course prior to viewing the course content. Shown as part of an alert, along with a link to enroll.',
|
||||
},
|
||||
'learning.staff.enrollment.alert': {
|
||||
id: 'learning.staff.enrollment.alert',
|
||||
defaultMessage: 'You are viewing this course as staff, and are not enrolled.',
|
||||
description: 'Message shown to indicate that a user is not enrolled, but is able to view a course anyway because they are staff. Shown as part of an alert, along with a link to enroll.',
|
||||
},
|
||||
'learning.enrollment.enroll.now': {
|
||||
id: 'learning.enrollment.enroll.now',
|
||||
defaultMessage: 'Enroll Now',
|
||||
|
||||
@@ -19,7 +19,7 @@ import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
import CoursewareContainer from './courseware';
|
||||
import CourseHomeContainer from './course-home';
|
||||
import CourseContainer from './CourseContainer';
|
||||
import CoursewareRedirect from './CoursewareRedirect';
|
||||
|
||||
import store from './store';
|
||||
|
||||
@@ -28,7 +28,16 @@ subscribe(APP_READY, () => {
|
||||
<AppProvider store={store}>
|
||||
<UserMessagesProvider>
|
||||
<Switch>
|
||||
<Route path="/course/:courseId" component={CourseContainer} />
|
||||
<Route path="/learning/redirect" component={CoursewareRedirect} />
|
||||
<Route path="/learning/course/:courseId/home" component={CourseHomeContainer} />
|
||||
<Route
|
||||
path={[
|
||||
'/learning/course/:courseId/:sequenceId/:unitId',
|
||||
'/learning/course/:courseId/:sequenceId',
|
||||
'/learning/course/:courseId',
|
||||
]}
|
||||
component={CoursewareContainer}
|
||||
/>
|
||||
</Switch>
|
||||
<Footer />
|
||||
</UserMessagesProvider>
|
||||
@@ -42,6 +51,9 @@ subscribe(APP_INIT_ERROR, (error) => {
|
||||
});
|
||||
|
||||
initialize({
|
||||
// TODO: Remove this once the course blocks api supports unauthenticated
|
||||
// access and we are prepared to support public courses in this app.
|
||||
requireAuthenticatedUser: true,
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
|
||||
@@ -8,14 +8,14 @@ export function useLogistrationAlert() {
|
||||
const { add, remove } = useContext(UserMessagesContext);
|
||||
const [alertId, setAlertId] = useState(null);
|
||||
useEffect(() => {
|
||||
if (authenticatedUser === null) {
|
||||
if (authenticatedUser === null && alertId === null) {
|
||||
setAlertId(add({
|
||||
code: 'clientLogistrationAlert',
|
||||
dismissible: false,
|
||||
type: 'error',
|
||||
topic: 'course',
|
||||
}));
|
||||
} else if (alertId !== null) {
|
||||
} else if (authenticatedUser !== null && alertId !== null) {
|
||||
remove(alertId);
|
||||
setAlertId(null);
|
||||
}
|
||||
|
||||
@@ -4,32 +4,40 @@ import PropTypes from 'prop-types';
|
||||
import UserMessagesContext from './UserMessagesContext';
|
||||
|
||||
export default function UserMessagesProvider({ children }) {
|
||||
// Note: The callbacks (add, remove, clear) below interact with useState in very subtle ways.
|
||||
// When we call setMessages, we always do so with the function-based form of the handler, making
|
||||
// use of the "current" state and not relying on lexical scoping to access the state exposed
|
||||
// above with useState. This is very important and allows us to call multiple "add", "remove",
|
||||
// or "clear" functions in a single render. Without it, each call to one of the callbacks
|
||||
// references back to the -original- value of messages instead of the most recent, causing them
|
||||
// all to override each other. Last one in would win.
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [nextId, setNextId] = useState(1);
|
||||
|
||||
const refMessages = useRef(messages);
|
||||
// Because the add, remove, and clear handlers also need to access nextId, we have to do
|
||||
// something a bit different. There's no way to wait for the "currentNextId" in a setMessages
|
||||
// handler. The alternative is to update a ref, which will always point to the current value by
|
||||
// its very nature.
|
||||
const refId = useRef(nextId);
|
||||
|
||||
const add = ({
|
||||
code, dismissible, text, type, topic, ...others
|
||||
}) => {
|
||||
const id = nextId;
|
||||
refMessages.current = [...refMessages.current, {
|
||||
const id = refId.current;
|
||||
setMessages(currentMessages => [...currentMessages, {
|
||||
code, dismissible, text, type, topic, ...others, id,
|
||||
}];
|
||||
setMessages(refMessages.current);
|
||||
setNextId(nextId + 1);
|
||||
return id;
|
||||
}]);
|
||||
refId.current += 1;
|
||||
setNextId(refId.current);
|
||||
return refId.current;
|
||||
};
|
||||
|
||||
const remove = id => {
|
||||
refMessages.current = refMessages.current.filter(message => message.id !== id);
|
||||
setMessages(refMessages.current);
|
||||
setMessages(currentMessages => currentMessages.filter(message => message.id !== id));
|
||||
};
|
||||
|
||||
const clear = (topic = null) => {
|
||||
refMessages.current = topic === null ? [] : refMessages.current.filter(message => message.topic !== topic);
|
||||
|
||||
setMessages(refMessages.current);
|
||||
setMessages(currentMessages => (topic === null ? [] : currentMessages.filter(message => message.topic !== topic)));
|
||||
};
|
||||
|
||||
const value = {
|
||||
|
||||
Reference in New Issue
Block a user