From 5f6f3410e83be74b3d3e4d49bb3499282bf94bd0 Mon Sep 17 00:00:00 2001 From: Syed Ali Abbas Zaidi Date: Fri, 9 Jun 2023 13:19:44 +0500 Subject: [PATCH] feat: upgrade react router to v6 --- package-lock.json | 37 ++--- package.json | 2 +- src/constants.js | 33 ++++ .../discussion-tab/DiscussionTab.jsx | 7 +- src/course-home/outline-tab/OutlineTab.jsx | 8 +- src/courseware/CoursewareContainer.jsx | 146 ++++++++---------- .../CoursewareRedirectLandingPage.jsx | 78 ++++------ src/courseware/RedirectPage.jsx | 45 ++++++ .../course/course-exit/CourseExit.jsx | 11 +- src/courseware/utils.jsx | 22 +++ src/decode-page-route/index.jsx | 44 +++--- src/generic/CourseAccessErrorPage.jsx | 6 +- src/generic/path-fixes/PathFixesProvider.jsx | 42 ++--- src/index.jsx | 137 +++++++++------- src/tab-page/TabContainer.jsx | 17 +- 15 files changed, 380 insertions(+), 255 deletions(-) create mode 100644 src/constants.js create mode 100644 src/courseware/RedirectPage.jsx create mode 100644 src/courseware/utils.jsx 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 ( -
- - )} +const CoursewareRedirectLandingPage = () => ( +
+ + )} + /> - - { - global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`); - }} - /> - { - global.location.assign(`${getConfig().LMS_BASE_URL}/dashboard${location.search}`); - }} - /> - { - const { consentPath } = queryString.parse(location.search); - global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`); - }} - /> - { - global.location.assign(`/course/${match.params.courseId}/home`); - }} - /> - -
- ); -}; + + } + /> + } + /> + } + /> + } + /> + +
+); export default CoursewareRedirectLandingPage; diff --git a/src/courseware/RedirectPage.jsx b/src/courseware/RedirectPage.jsx new file mode 100644 index 0000000000..f1189a47fe --- /dev/null +++ b/src/courseware/RedirectPage.jsx @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import { + generatePath, useParams, useLocation, +} from 'react-router-dom'; +import { getConfig } from '@edx/frontend-platform'; + +import queryString from 'query-string'; +import { REDIRECT_MODES } from '../constants'; + +const RedirectPage = ({ + pattern, mode, +}) => { + const { courseId } = useParams(); + const location = useLocation(); + const { consentPath } = queryString.parse(location?.search); + + const BASE_URL = getConfig().LMS_BASE_URL; + + switch (mode) { + case REDIRECT_MODES.DASHBOARD_REDIRECT: + global.location.assign(`${BASE_URL}${pattern}${location?.search}`); + break; + case REDIRECT_MODES.CONSENT_REDIRECT: + global.location.assign(`${BASE_URL}${consentPath}`); + break; + case REDIRECT_MODES.HOME_REDIRECT: + global.location.assign(generatePath(pattern, { courseId })); + break; + default: + global.location.assign(`${BASE_URL}${generatePath(pattern, { courseId })}`); + } + + return null; +}; + +RedirectPage.propTypes = { + pattern: PropTypes.string, + mode: PropTypes.string.isRequired, +}; + +RedirectPage.defaultProps = { + pattern: null, +}; + +export default RedirectPage; diff --git a/src/courseware/course/course-exit/CourseExit.jsx b/src/courseware/course/course-exit/CourseExit.jsx index d6bff4084a..a54cec45e3 100644 --- a/src/courseware/course/course-exit/CourseExit.jsx +++ b/src/courseware/course/course-exit/CourseExit.jsx @@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; import { useSelector } from 'react-redux'; -import { Redirect } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import CourseCelebration from './CourseCelebration'; import CourseInProgress from './CourseInProgress'; @@ -16,6 +16,7 @@ import { unsubscribeFromGoalReminders } from './data/thunks'; import { useModel } from '../../../generic/model-store'; const CourseExit = ({ intl }) => { + const navigate = useNavigate(); const { courseId } = useSelector(state => state.courseware); const { certificateData, @@ -57,10 +58,14 @@ const CourseExit = ({ intl }) => { body = (); } else if (mode === COURSE_EXIT_MODES.celebration) { body = (); - } else { - return (); } + useEffect(() => { + if ((mode === COURSE_EXIT_MODES.disabled) || (!(mode in COURSE_EXIT_MODES))) { + navigate(`/course/${courseId}`); + } + }, []); + return ( <>
diff --git a/src/courseware/utils.jsx b/src/courseware/utils.jsx new file mode 100644 index 0000000000..2c4c337996 --- /dev/null +++ b/src/courseware/utils.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { useNavigate, useParams } from 'react-router-dom'; + +const withParamsAndNavigation = WrappedComponent => { + const WithParamsNavigationComponent = props => { + const { courseId, sequenceId, unitId } = useParams(); + const navigate = useNavigate(); + return ( + + ); + }; + return WithParamsNavigationComponent; +}; + +export default withParamsAndNavigation; diff --git a/src/decode-page-route/index.jsx b/src/decode-page-route/index.jsx index cc38013b24..15a0d0e102 100644 --- a/src/decode-page-route/index.jsx +++ b/src/decode-page-route/index.jsx @@ -1,7 +1,14 @@ import PropTypes from 'prop-types'; -import { PageRoute } from '@edx/frontend-platform/react'; +import { PageWrap } from '@edx/frontend-platform/react'; import React from 'react'; -import { useHistory, generatePath } from 'react-router'; +import { + generatePath, useMatch, Navigate, +} from 'react-router-dom'; +import { DECODE_ROUTES } from '../constants'; + +const ROUTES = [].concat( + ...Object.values(DECODE_ROUTES).map(value => (Array.isArray(value) ? value : [value])), +); export const decodeUrl = (encodedUrl) => { const decodedUrl = decodeURIComponent(encodedUrl); @@ -11,10 +18,16 @@ export const decodeUrl = (encodedUrl) => { return decodeUrl(decodedUrl); }; -const DecodePageRoute = (props) => { - const history = useHistory(); - if (props.computedMatch) { - const { url, path, params } = props.computedMatch; +const DecodePageRoute = ({ children }) => { + let computedMatch = null; + + ROUTES.forEach((route) => { + const matchedRoute = useMatch(route); + if (matchedRoute) { computedMatch = matchedRoute; } + }); + + if (computedMatch) { + const { pathname, pattern, params } = computedMatch; Object.keys(params).forEach((param) => { // only decode params not the entire url. @@ -22,28 +35,19 @@ const DecodePageRoute = (props) => { params[param] = decodeUrl(params[param]); }); - const newUrl = generatePath(path, params); + const newUrl = generatePath(pattern.path, params); // if the url get decoded, reroute to the decoded url - if (newUrl !== url) { - history.replace(newUrl); + if (newUrl !== pathname) { + return ; } } - return ; + return {children} ; }; DecodePageRoute.propTypes = { - computedMatch: PropTypes.shape({ - url: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - // eslint-disable-next-line react/forbid-prop-types - params: PropTypes.any, - }), -}; - -DecodePageRoute.defaultProps = { - computedMatch: null, + children: PropTypes.node.isRequired, }; export default DecodePageRoute; diff --git a/src/generic/CourseAccessErrorPage.jsx b/src/generic/CourseAccessErrorPage.jsx index 3440382ff4..969d6e2976 100644 --- a/src/generic/CourseAccessErrorPage.jsx +++ b/src/generic/CourseAccessErrorPage.jsx @@ -1,9 +1,8 @@ import React, { useEffect } from 'react'; import { LearningHeader as Header } from '@edx/frontend-component-header'; import Footer from '@edx/frontend-component-footer'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import { Redirect } from 'react-router'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert'; import { AlertList } from './user-messages'; @@ -14,6 +13,7 @@ import messages from '../tab-page/messages'; const CourseAccessErrorPage = ({ intl }) => { const { courseId } = useParams(); + const navigate = useNavigate(); const dispatch = useDispatch(); const activeEnterpriseAlert = useActiveEnterpriseAlert(courseId); @@ -38,7 +38,7 @@ const CourseAccessErrorPage = ({ intl }) => { ); } if (courseStatus === LOADED) { - return (); + navigate(`/redirect/home/${courseId}`); } return ( <> diff --git a/src/generic/path-fixes/PathFixesProvider.jsx b/src/generic/path-fixes/PathFixesProvider.jsx index 3215660927..02bf11a0fc 100644 --- a/src/generic/path-fixes/PathFixesProvider.jsx +++ b/src/generic/path-fixes/PathFixesProvider.jsx @@ -1,7 +1,8 @@ -import { Redirect, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import PropTypes from 'prop-types'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { useEffect } from 'react'; /** * We have seen evidence of learners hitting MFE pages with spaces instead of plus signs (which are used commonly @@ -13,24 +14,27 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; */ const PathFixesProvider = ({ children }) => { const location = useLocation(); - - // We only check for spaces. That's not the only kind of character that is escaped in URLs, but it would always be - // present for our cases, and I believe it's the only one we use normally. - if (location.pathname.includes(' ')) { - const newLocation = { - ...location, - pathname: location.pathname.replaceAll(' ', '+'), - }; - - sendTrackEvent('edx.ui.lms.path_fixed', { - new_path: newLocation.pathname, - old_path: location.pathname, - referrer: document.referrer, - search: location.search, - }); - - return (); - } + const navigate = useNavigate(); + + useEffect(() => { + // We only check for spaces. That's not the only kind of character that is escaped in URLs, but it would always be + // present for our cases, and I believe it's the only one we use normally. + if (location.pathname.includes('%20')) { + const newLocation = { + ...location, + pathname: location.pathname.replaceAll('%20', '+'), + }; + + sendTrackEvent('edx.ui.lms.path_fixed', { + new_path: newLocation.pathname, + old_path: location.pathname, + referrer: document.referrer, + search: location.search, + }); + + navigate(newLocation); + } + }, [location.pathname]); return children; // pass through }; diff --git a/src/index.jsx b/src/index.jsx index a509e1f6a8..cdba49f1d4 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -6,10 +6,10 @@ import { mergeConfig, getConfig, } from '@edx/frontend-platform'; -import { AppProvider, ErrorPage, PageRoute } from '@edx/frontend-platform/react'; +import { AppProvider, ErrorPage, PageWrap } from '@edx/frontend-platform/react'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Switch } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { fetchDiscussionTab, fetchLiveTab } from './course-home/data/thunks'; @@ -36,6 +36,7 @@ import PathFixesProvider from './generic/path-fixes'; import LiveTab from './course-home/live-tab/LiveTab'; import CourseAccessErrorPage from './generic/CourseAccessErrorPage'; import DecodePageRoute from './decode-page-route'; +import { DECODE_ROUTES, ROUTES } from './constants'; subscribe(APP_READY, () => { ReactDOM.render( @@ -46,59 +47,91 @@ subscribe(APP_READY, () => { - - - - - - - - - - - - - - - - - - - - - - - - - ( - fetchProgressTab(courseId, match.params.targetUserId)} - slice="courseHome" - > - - + + } /> + } /> + } + /> + + + + + + )} + /> + + + + + + )} + /> + + + + + )} /> - - - - - - + + + + + )} + /> + {DECODE_ROUTES.PROGRESS.map((route) => ( + + + + + + )} + /> + ))} + + + + + + )} /> - + {DECODE_ROUTES.COURSEWARE.map((route) => ( + + + + )} + /> + ))} + diff --git a/src/tab-page/TabContainer.jsx b/src/tab-page/TabContainer.jsx index c5331f9fe8..d69e62f40f 100644 --- a/src/tab-page/TabContainer.jsx +++ b/src/tab-page/TabContainer.jsx @@ -12,15 +12,21 @@ const TabContainer = (props) => { fetch, slice, tab, + isProgressTab, } = props; - const { courseId: courseIdFromUrl } = useParams(); + const { courseId: courseIdFromUrl, targetUserId } = useParams(); const dispatch = useDispatch(); + useEffect(() => { // The courseId from the URL is the course we WANT to load. - dispatch(fetch(courseIdFromUrl)); + if (isProgressTab) { + dispatch(fetch(courseIdFromUrl, targetUserId)); + } else { + dispatch(fetch(courseIdFromUrl)); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [courseIdFromUrl]); + }, [courseIdFromUrl, targetUserId]); // The courseId from the store is the course we HAVE loaded. If the URL changes, // we don't want the application to adjust to it until it has actually loaded the new data. @@ -47,6 +53,11 @@ TabContainer.propTypes = { fetch: PropTypes.func.isRequired, slice: PropTypes.string.isRequired, tab: PropTypes.string.isRequired, + isProgressTab: PropTypes.bool, +}; + +TabContainer.defaultProps = { + isProgressTab: false, }; export default TabContainer;