diff --git a/package-lock.json b/package-lock.json index e64a3f19c2..b3be2871e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "react-helmet": "6.1.0", "react-redux": "7.2.9", "react-router": "5.2.1", - "react-router-dom": "5.3.0", + "react-router-dom": "^6.0.0", "react-share": "4.4.1", "redux": "4.1.2", "regenerator-runtime": "0.13.11", @@ -23563,33 +23563,26 @@ } }, "node_modules/react-router-dom": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", - "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0.tgz", + "integrity": "sha512-bPXyYipf0zu6K7mHSEmNO5YqLKq2q9N+Dsahw9Xh3oq1IirsI3vbnIYcVWin6A0zWyHmKhMGoV7Gr0j0kcuVFg==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "react-router": "6.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/react-router-dom/node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "node_modules/react-router-dom/node_modules/react-router": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.0.tgz", + "integrity": "sha512-FcTRCihYZvERMNbG54D9+Wkv2cj/OtoxNlA/87D7vxKYlmSmbF9J9XChI9Is44j/behEiOhbovgVZBhKQn+wgA==", "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "history": "^5.0.3" + }, + "peerDependencies": { + "react": ">=16.8" } }, "node_modules/react-router/node_modules/history": { diff --git a/package.json b/package.json index 681f15b8d9..c2f10e65c8 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "react-helmet": "6.1.0", "react-redux": "7.2.9", "react-router": "5.2.1", - "react-router-dom": "5.3.0", + "react-router-dom": "^6.0.0", "react-share": "4.4.1", "redux": "4.1.2", "regenerator-runtime": "0.13.11", diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000000..ac10aa3044 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,33 @@ +export const DECODE_ROUTES = { + ACCESS_DENIED: '/course/:courseId/access-denied', + HOME: '/course/:courseId/home', + LIVE: '/course/:courseId/live', + DATES: '/course/:courseId/dates', + DISCUSSION: '/course/:courseId/discussion/:path/*', + PROGRESS: [ + '/course/:courseId/progress/:targetUserId/', + '/course/:courseId/progress', + ], + COURSE_END: '/course/:courseId/course-end', + COURSEWARE: [ + '/course/:courseId/:sequenceId/:unitId', + '/course/:courseId/:sequenceId', + '/course/:courseId', + ], + REDIRECT_HOME: 'home/:courseId', + REDIRECT_SURVEY: 'survey/:courseId', +}; + +export const ROUTES = { + UNSUBSCRIBE: '/goal-unsubscribe/:token', + REDIRECT: '/redirect/*', + DASHBOARD: 'dashboard', + CONSENT: 'consent', +}; + +export const REDIRECT_MODES = { + DASHBOARD_REDIRECT: 'dashboard-redirect', + CONSENT_REDIRECT: 'consent-redirect', + HOME_REDIRECT: 'home-redirect', + SURVEY_REDIRECT: 'survey-redirect', +}; diff --git a/src/course-home/discussion-tab/DiscussionTab.jsx b/src/course-home/discussion-tab/DiscussionTab.jsx index 0bbae8be3c..06ee97cc7c 100644 --- a/src/course-home/discussion-tab/DiscussionTab.jsx +++ b/src/course-home/discussion-tab/DiscussionTab.jsx @@ -2,21 +2,20 @@ import { getConfig } from '@edx/frontend-platform'; import { injectIntl } from '@edx/frontend-platform/i18n'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; -import { generatePath, useHistory } from 'react-router'; -import { useParams } from 'react-router-dom'; +import { useParams, generatePath, useNavigate } from 'react-router-dom'; import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks'; const DiscussionTab = () => { const { courseId } = useSelector(state => state.courseHome); const { path } = useParams(); const [originalPath] = useState(path); - const history = useHistory(); + const navigate = useNavigate(); const [, iFrameHeight] = useIFrameHeight(); useIFramePluginEvents({ 'discussions.navigate': (payload) => { const basePath = generatePath('/course/:courseId/discussion', { courseId }); - history.push(`${basePath}/${payload.path}`); + navigate(`${basePath}/${payload.path}`); }, }); const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/${originalPath}`; diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 072f0d04c6..2012d99f91 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -1,9 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; -import { history } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; import { AlertList } from '../../generic/user-messages'; @@ -67,6 +66,7 @@ const OutlineTab = ({ intl }) => { } = useModel('coursewareMeta', courseId); const [expandAll, setExpandAll] = useState(false); + const navigate = useNavigate(); const eventProperties = { org_key: org, @@ -115,9 +115,7 @@ const OutlineTab = ({ intl }) => { // Deleting the course_start query param as it only needs to be set once // whenever passed in query params. currentParams.delete('start_course'); - history.replace({ - search: currentParams.toString(), - }); + navigate(`?${currentParams.toString()}`, { replace: true }); } }, [location.search]); diff --git a/src/courseware/CoursewareContainer.jsx b/src/courseware/CoursewareContainer.jsx index 34fcc092de..d43b7dd38e 100644 --- a/src/courseware/CoursewareContainer.jsx +++ b/src/courseware/CoursewareContainer.jsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { history } from '@edx/frontend-platform'; import { createSelector } from '@reduxjs/toolkit'; import { defaultMemoize as memoize } from 'reselect'; @@ -17,45 +16,46 @@ import { TabPage } from '../tab-page'; import Course from './course'; import { handleNextSectionCelebration } from './course/celebration'; +import withParamsAndNavigation from './utils'; // Look at where this is called in componentDidUpdate for more info about its usage -const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => { +const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId, navigate) => { if (courseStatus === 'loaded' && !sequenceId) { // Note that getResumeBlock is just an API call, not a redux thunk. getResumeBlock(courseId).then((data) => { // This is a replace because we don't want this change saved in the browser's history. if (data.sectionId && data.unitId) { - history.replace(`/course/${courseId}/${data.sectionId}/${data.unitId}`); + navigate(`/course/${courseId}/${data.sectionId}/${data.unitId}`, { replace: true }); } else if (firstSequenceId) { - history.replace(`/course/${courseId}/${firstSequenceId}`); + navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true }); } }); } }); // Look at where this is called in componentDidUpdate for more info about its usage -const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => { +const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => { if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) { - history.replace(`/course/${courseId}/${unitId}`); + navigate(`/course/${courseId}/${unitId}`, { replace: true }); } }); // Look at where this is called in componentDidUpdate for more info about its usage -const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => { +const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => { if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) { // If the section is non-empty, redirect to its first sequence. if (section.sequenceIds && section.sequenceIds[0]) { - history.replace(`/course/${courseId}/${section.sequenceIds[0]}`); + navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true }); // Otherwise, just go to the course root, letting the resume redirect take care of things. } else { - history.replace(`/course/${courseId}`); + navigate(`/course/${courseId}`, { replace: true }); } } }); // Look at where this is called in componentDidUpdate for more info about its usage const checkUnitToSequenceUnitRedirect = memoize( - (courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId) => { + (courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId, navigate) => { if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) { if (sequenceMightBeUnit) { // If the sequence failed to load as a sequence, but it is marked as a possible unit, then @@ -64,60 +64,62 @@ const checkUnitToSequenceUnitRedirect = memoize( getSequenceForUnitDeprecated(courseId, unitId).then( parentId => { if (parentId) { - history.replace(`/course/${courseId}/${parentId}/${unitId}`); + navigate(`/course/${courseId}/${parentId}/${unitId}`, { replace: true }); } else { - history.replace(`/course/${courseId}`); + navigate(`/course/${courseId}`, { replace: true }); } }, () => { // error case - history.replace(`/course/${courseId}`); + navigate(`/course/${courseId}`, { replace: true }); }, ); } else { // Invalid sequence that isn't a unit either. Redirect up to main course. - history.replace(`/course/${courseId}`); + navigate(`/course/${courseId}`, { replace: true }); } } }, ); // Look at where this is called in componentDidUpdate for more info about its usage -const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => { +const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId, navigate) => { if (sequenceStatus === 'loaded' && sequence.id && !unitId) { if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) { const nextUnitId = sequence.unitIds[sequence.activeUnitIndex]; // This is a replace because we don't want this change saved in the browser's history. - history.replace(`/course/${courseId}/${sequence.id}/${nextUnitId}`); + navigate(`/course/${courseId}/${sequence.id}/${nextUnitId}`, { replace: true }); } } }); // Look at where this is called in componentDidUpdate for more info about its usage -const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => { - if (sequenceStatus !== 'loaded' || !sequence.id) { - return; - } +const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize( + (courseId, sequenceStatus, sequence, unitId, navigate) => { + if (sequenceStatus !== 'loaded' || !sequence.id) { + return; + } - const hasUnits = sequence.unitIds?.length > 0; + const hasUnits = sequence.unitIds?.length > 0; - if (unitId === 'first') { - if (hasUnits) { - const firstUnitId = sequence.unitIds[0]; - history.replace(`/course/${courseId}/${sequence.id}/${firstUnitId}`); - } else { + if (unitId === 'first') { + if (hasUnits) { + const firstUnitId = sequence.unitIds[0]; + navigate(`/course/${courseId}/${sequence.id}/${firstUnitId}`, { replace: true }); + } else { // No units... go to general sequence page - history.replace(`/course/${courseId}/${sequence.id}`); - } - } else if (unitId === 'last') { - if (hasUnits) { - const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1]; - history.replace(`/course/${courseId}/${sequence.id}/${lastUnitId}`); - } else { + navigate(`/course/${courseId}/${sequence.id}`, { replace: true }); + } + } else if (unitId === 'last') { + if (hasUnits) { + const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1]; + navigate(`/course/${courseId}/${sequence.id}/${lastUnitId}`, { replace: true }); + } else { // No units... go to general sequence page - history.replace(`/course/${courseId}/${sequence.id}`); + navigate(`/course/${courseId}/${sequence.id}`, { replace: true }); + } } - } -}); + }, +); class CoursewareContainer extends Component { checkSaveSequencePosition = memoize((unitId) => { @@ -145,12 +147,8 @@ class CoursewareContainer extends Component { componentDidMount() { const { - match: { - params: { - courseId: routeCourseId, - sequenceId: routeSequenceId, - }, - }, + routeCourseId, + routeSequenceId, } = this.props; // Load data whenever the course or sequence ID changes. this.checkFetchCourse(routeCourseId); @@ -167,13 +165,10 @@ class CoursewareContainer extends Component { sequence, firstSequenceId, sectionViaSequenceId, - match: { - params: { - courseId: routeCourseId, - sequenceId: routeSequenceId, - unitId: routeUnitId, - }, - }, + routeCourseId, + routeSequenceId, + routeUnitId, + navigate, } = this.props; // Load data whenever the course or sequence ID changes. @@ -202,7 +197,7 @@ class CoursewareContainer extends Component { // Check resume redirect: // /course/:courseId -> /course/:courseId/:sequenceId/:unitId // based on sequence/unit where user was last active. - checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId); + checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate); // Check section-unit to unit redirect: // /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId @@ -215,46 +210,45 @@ class CoursewareContainer extends Component { // otherwise, we could get stuck in a redirect loop, since a sequence that failed to load // would endlessly redirect to itself through `checkSectionUnitToUnitRedirect` // and `checkUnitToSequenceUnitRedirect`. - checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId); + checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate); // Check section to sequence redirect: // /course/:courseId/:sectionId -> /course/:courseId/:sequenceId // by redirecting to the first sequence within the section. - checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId); + checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate); // Check unit to sequence-unit redirect: // /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId // by filling in the ID of the parent sequence of :unitId. checkUnitToSequenceUnitRedirect(( - courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId + courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, + sequenceId, sectionViaSequenceId, routeUnitId, navigate )); // Check sequence to sequence-unit redirect: // /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId // by filling in the ID the most-recently-active unit in the sequence, OR // the ID of the first unit the sequence if none is active. - checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId); + checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate); // Check sequence-unit marker to sequence-unit redirect: // /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId // /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId // by filling in the ID the first or last unit in the sequence. // "Sequence unit marker" is an invented term used only in this component. - checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId); + checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate); } handleUnitNavigationClick = (nextUnitId) => { const { - courseId, sequenceId, - match: { - params: { - unitId: routeUnitId, - }, - }, + courseId, + sequenceId, + routeUnitId, + navigate, } = this.props; this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId); - history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`); + navigate(`/course/${courseId}/${sequenceId}/${nextUnitId}`); }; handleNextSequenceClick = () => { @@ -264,10 +258,11 @@ class CoursewareContainer extends Component { nextSequence, sequence, sequenceId, + navigate, } = this.props; if (nextSequence !== null) { - history.push(`/course/${courseId}/${nextSequence.id}/first`); + navigate(`/course/${courseId}/${nextSequence.id}/first`); const celebrateFirstSection = course && course.celebrations && course.celebrations.firstSection; if (celebrateFirstSection && sequence.sectionId !== nextSequence.sectionId) { @@ -277,9 +272,9 @@ class CoursewareContainer extends Component { }; handlePreviousSequenceClick = () => { - const { previousSequence, courseId } = this.props; + const { previousSequence, courseId, navigate } = this.props; if (previousSequence !== null) { - history.push(`/course/${courseId}/${previousSequence.id}/last`); + navigate(`/course/${courseId}/${previousSequence.id}/last`); } }; @@ -288,11 +283,7 @@ class CoursewareContainer extends Component { courseStatus, courseId, sequenceId, - match: { - params: { - unitId: routeUnitId, - }, - }, + routeUnitId, } = this.props; return ( @@ -335,13 +326,9 @@ const courseShape = PropTypes.shape({ }); CoursewareContainer.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - courseId: PropTypes.string.isRequired, - sequenceId: PropTypes.string, - unitId: PropTypes.string, - }).isRequired, - }).isRequired, + routeCourseId: PropTypes.string.isRequired, + routeSequenceId: PropTypes.string, + routeUnitId: PropTypes.string, courseId: PropTypes.string, sequenceId: PropTypes.string, firstSequenceId: PropTypes.string, @@ -357,11 +344,14 @@ CoursewareContainer.propTypes = { checkBlockCompletion: PropTypes.func.isRequired, fetchCourse: PropTypes.func.isRequired, fetchSequence: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired, }; CoursewareContainer.defaultProps = { courseId: null, sequenceId: null, + routeSequenceId: null, + routeUnitId: null, firstSequenceId: null, nextSequence: null, previousSequence: null, @@ -476,4 +466,4 @@ export default connect(mapStateToProps, { saveSequencePosition, fetchCourse, fetchSequence, -})(CoursewareContainer); +})(withParamsAndNavigation(CoursewareContainer)); diff --git a/src/courseware/CoursewareRedirectLandingPage.jsx b/src/courseware/CoursewareRedirectLandingPage.jsx index c3f965c5e8..1d90f50b2e 100644 --- a/src/courseware/CoursewareRedirectLandingPage.jsx +++ b/src/courseware/CoursewareRedirectLandingPage.jsx @@ -1,56 +1,44 @@ import React from 'react'; -import { Switch, useRouteMatch } from 'react-router'; -import { getConfig } from '@edx/frontend-platform'; +import { Routes, Route } from 'react-router-dom'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { PageRoute } from '@edx/frontend-platform/react'; +import { PageWrap } from '@edx/frontend-platform/react'; -import queryString from 'query-string'; import PageLoading from '../generic/PageLoading'; import DecodePageRoute from '../decode-page-route'; +import { DECODE_ROUTES, REDIRECT_MODES, ROUTES } from '../constants'; +import RedirectPage from './RedirectPage'; -const CoursewareRedirectLandingPage = () => { - const { path } = useRouteMatch(); - return ( -