diff --git a/package-lock.json b/package-lock.json index a423120e5..f0d9d4580 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-footer": "12.6.0", "@edx/frontend-component-header": "4.10.1", - "@edx/frontend-platform": "4.6.3", + "@edx/frontend-platform": "5.6.1", "@edx/paragon": "20.46.3", "@reduxjs/toolkit": "1.8.0", "@tinymce/tinymce-react": "3.13.1", @@ -27,8 +27,8 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-redux": "7.2.9", - "react-router": "5.2.1", - "react-router-dom": "5.3.0", + "react-router": "6.18.0", + "react-router-dom": "6.18.0", "redux": "4.2.1", "regenerator-runtime": "0.14.0", "timeago.js": "4.0.2", @@ -3529,9 +3529,9 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.3.tgz", - "integrity": "sha512-vvmg2rWfjdOD9BKcHiFlV3n4kVGqMGUYS0UrIk8Dx7BYbb7It03q/twe5b2D3PHQwvNCTei9EgX8+Tn1QhkXBA==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.6.1.tgz", + "integrity": "sha512-7MOIjGGYplVY7yHrSea90EkQ24UxKxRKU9FaihB41yUSL/Vin1txDuIn3059Xr+60QfIKRsym+LogXe9IZ47Dw==", "dependencies": { "@cospired/i18n-iso-languages": "4.1.0", "@formatjs/intl-pluralrules": "4.3.3", @@ -3564,7 +3564,7 @@ "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0", "react-redux": "^7.1.1", - "react-router-dom": "^5.0.1", + "react-router-dom": "^6.0.0", "redux": "^4.0.4" } }, @@ -6034,6 +6034,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@restart/context": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", @@ -17430,9 +17438,9 @@ } }, "node_modules/jquery": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", - "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "peer": true }, "node_modules/js-tokens": { @@ -18085,19 +18093,6 @@ "node": ">=4" } }, - "node_modules/mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - }, - "peerDependencies": { - "prop-types": "^15.0.0", - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/mini-css-extract-plugin": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", @@ -19044,14 +19039,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -20786,40 +20773,33 @@ } }, "node_modules/react-router": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", - "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "dependencies": { + "@remix-run/router": "1.11.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "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.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", "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" + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-style-singleton": { diff --git a/package.json b/package.json index 73044d636..a734a8266 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-footer": "12.6.0", "@edx/frontend-component-header": "4.10.1", - "@edx/frontend-platform": "4.6.3", + "@edx/frontend-platform": "5.6.1", "@edx/paragon": "20.46.3", "@reduxjs/toolkit": "1.8.0", "@tinymce/tinymce-react": "3.13.1", @@ -51,8 +51,8 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-redux": "7.2.9", - "react-router": "5.2.1", - "react-router-dom": "5.3.0", + "react-router": "6.18.0", + "react-router-dom": "6.18.0", "redux": "4.2.1", "regenerator-runtime": "0.14.0", "timeago.js": "4.0.2", diff --git a/src/components/TinyMCEEditor.jsx b/src/components/TinyMCEEditor.jsx index 93cc4fcc4..a304c34c3 100644 --- a/src/components/TinyMCEEditor.jsx +++ b/src/components/TinyMCEEditor.jsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Editor } from '@tinymce/tinymce-react'; -import { useLocation, useParams } from 'react-router'; +import { useLocation, useParams } from 'react-router-dom'; // TinyMCE so the global var exists // eslint-disable-next-line no-unused-vars,import/no-extraneous-dependencies import tinymce from 'tinymce/tinymce'; diff --git a/src/data/constants.js b/src/data/constants.js index dc786b439..3fa9496e6 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -147,18 +147,17 @@ export const Routes = { PATH: BASE_PATH, }, LEARNERS: { - PATH: `${BASE_PATH}/learners`, - POSTS: `${BASE_PATH}/learners/:learnerUsername/posts(/:postId)?`, + PATH: `${BASE_PATH}/learners/:learnerUsername?`, + POSTS: `${BASE_PATH}/learners/:learnerUsername/posts/:postId?`, + POSTS_EDIT: `${BASE_PATH}/learners/:learnerUsername/posts/:postId/edit`, }, POSTS: { PATH: `${BASE_PATH}/topics/:topicId`, - MY_POSTS: `${BASE_PATH}/my-posts(/:postId)?`, - ALL_POSTS: `${BASE_PATH}/posts(/:postId)?`, - NEW_POST: [ - `${BASE_PATH}/topics/:topicId/posts/:postId`, - `${BASE_PATH}/topics/:topicId`, - `${BASE_PATH}`, - ], + MY_POSTS: `${BASE_PATH}/my-posts/:postId?`, + ALL_POSTS: `${BASE_PATH}/posts/:postId?`, + EDIT_MY_POSTS: `${BASE_PATH}/my-posts/:postId/edit`, + EDIT_ALL_POSTS: `${BASE_PATH}/posts/:postId/edit`, + NEW_POST: `${BASE_PATH}/*`, EDIT_POST: [ `${BASE_PATH}/category/:category/posts/:postId/edit`, `${BASE_PATH}/topics/:topicId/posts/:postId/edit`, @@ -169,19 +168,19 @@ export const Routes = { }, COMMENTS: { PATH: [ - `${BASE_PATH}/category/:category/posts/:postId`, - `${BASE_PATH}/topics/:topicId/posts/:postId`, + `${BASE_PATH}/category/:category/posts/:postId?`, + `${BASE_PATH}/topics/:topicId/posts/:postId?`, `${BASE_PATH}/posts/:postId`, `${BASE_PATH}/my-posts/:postId`, - `${BASE_PATH}/learners/:learnerUsername/posts/:postId`, + `${BASE_PATH}/learners/:learnerUsername/posts/:postId?`, ], - PAGE: `${BASE_PATH}/:page`, + PAGE: `${BASE_PATH}/:page/*`, PAGES: { - category: `${BASE_PATH}/category/:category/posts/:postId`, - topics: `${BASE_PATH}/topics/:topicId/posts/:postId`, + category: `${BASE_PATH}/category/:category/posts/:postId?`, + topics: `${BASE_PATH}/topics/:topicId/posts/:postId?`, posts: `${BASE_PATH}/posts/:postId`, 'my-posts': `${BASE_PATH}/my-posts/:postId`, - learners: `${BASE_PATH}/learners/:learnerUsername/posts/:postId`, + learners: `${BASE_PATH}/learners/:learnerUsername/posts/:postId?`, }, }, TOPICS: { @@ -192,9 +191,10 @@ export const Routes = { ], ALL: `${BASE_PATH}/topics`, CATEGORY: `${BASE_PATH}/category/:category`, - CATEGORY_POST: `${BASE_PATH}/category/:category/posts/:postId`, + CATEGORY_POST: `${BASE_PATH}/category/:category/posts/:postId?`, + CATEGORY_POST_EDIT: `${BASE_PATH}/category/:category/posts/:postId/edit`, TOPIC: `${BASE_PATH}/topics/:topicId`, - TOPIC_POST: `${BASE_PATH}/topics/:topicId/posts/:postId`, + TOPIC_POST: `${BASE_PATH}/topics/:topicId/posts/:postId?`, TOPIC_POST_EDIT: `${BASE_PATH}/topics/:topicId/posts/:postId/edit`, }, }; @@ -208,11 +208,12 @@ export const PostsPages = { }; export const ALL_ROUTES = [] - .concat([Routes.TOPICS.CATEGORY_POST, Routes.TOPICS.CATEGORY]) + .concat([Routes.TOPICS.CATEGORY_POST, `${Routes.TOPICS.CATEGORY}?`]) .concat(Routes.COMMENTS.PATH) .concat(Routes.TOPICS.PATH) + .concat(Routes.POSTS.EDIT_POST) .concat([Routes.POSTS.ALL_POSTS, Routes.POSTS.MY_POSTS]) .concat([Routes.LEARNERS.POSTS, Routes.LEARNERS.PATH]) - .concat([Routes.DISCUSSIONS.PATH]); + .concat([`${Routes.DISCUSSIONS.PATH}/*`]); export const MAX_UPLOAD_FILE_SIZE = 1024; diff --git a/src/discussions/common/AuthorLabel.jsx b/src/discussions/common/AuthorLabel.jsx index de673bf99..2730b8296 100644 --- a/src/discussions/common/AuthorLabel.jsx +++ b/src/discussions/common/AuthorLabel.jsx @@ -2,8 +2,7 @@ import React, { useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { generatePath } from 'react-router'; -import { Link } from 'react-router-dom'; +import { generatePath, Link } from 'react-router-dom'; import * as timeago from 'timeago.js'; import { useIntl } from '@edx/frontend-platform/i18n'; diff --git a/src/discussions/common/HoverCard.test.jsx b/src/discussions/common/HoverCard.test.jsx index 1871c7ffb..437d80883 100644 --- a/src/discussions/common/HoverCard.test.jsx +++ b/src/discussions/common/HoverCard.test.jsx @@ -3,7 +3,7 @@ import { } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -60,15 +60,12 @@ async function mockAxiosReturnPagedCommentsResponses() { function renderComponent(postId) { const wrapper = render( - + - diff --git a/src/discussions/data/hooks.js b/src/discussions/data/hooks.js index 3389acad7..07d869991 100644 --- a/src/discussions/data/hooks.js +++ b/src/discussions/data/hooks.js @@ -4,7 +4,9 @@ import { } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation, useRouteMatch } from 'react-router'; +import { + matchPath, useLocation, useMatch, useNavigate, +} from 'react-router-dom'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -52,16 +54,18 @@ export function useTotalTopicThreadCount() { } export const useSidebarVisible = () => { + const location = useLocation(); const enableInContext = useSelector(selectEnableInContext); - const isViewingTopics = useRouteMatch(Routes.TOPICS.ALL); - const isViewingLearners = useRouteMatch(Routes.LEARNERS.PATH); + const isViewingTopics = useMatch(Routes.TOPICS.ALL); + const isViewingLearners = useMatch(`${Routes.LEARNERS.PATH}/*`); const isFiltered = useSelector(selectAreThreadsFiltered); const totalThreads = useSelector(selectPostThreadCount); const isThreadsEmpty = Boolean(useSelector(threadsLoadingStatus()) === RequestStatus.SUCCESSFUL && !totalThreads); - const isIncontextTopicsView = Boolean(useRouteMatch(Routes.TOPICS.PATH) && enableInContext); - const hideSidebar = Boolean(isThreadsEmpty && !isFiltered && !(isViewingTopics?.isExact || isViewingLearners)); + const matchInContextTopicView = Routes.TOPICS.PATH.find((route) => matchPath({ path: `${route}/*` }, location.pathname)); + const isInContextTopicsView = Boolean(matchInContextTopicView && enableInContext); + const hideSidebar = Boolean(isThreadsEmpty && !isFiltered && !(isViewingTopics || isViewingLearners)); - if (isIncontextTopicsView) { + if (isInContextTopicsView) { return true; } @@ -84,7 +88,7 @@ export function useCourseDiscussionData(courseId) { export function useRedirectToThread(courseId, enableInContextSidebar) { const dispatch = useDispatch(); - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const redirectToThread = useSelector( @@ -101,7 +105,7 @@ export function useRedirectToThread(courseId, enableInContextSidebar) { postId: redirectToThread.threadId, topicId: redirectToThread.topicId, })(location); - history.push(newLocation); + navigate({ ...newLocation }); } }, [redirectToThread]); } diff --git a/src/discussions/discussions-home/DiscussionContent.jsx b/src/discussions/discussions-home/DiscussionContent.jsx index ca3e668c1..420c4c61b 100644 --- a/src/discussions/discussions-home/DiscussionContent.jsx +++ b/src/discussions/discussions-home/DiscussionContent.jsx @@ -1,10 +1,10 @@ import React, { lazy, Suspense } from 'react'; import { useSelector } from 'react-redux'; -import { Route, Switch } from 'react-router'; +import { Route, Routes } from 'react-router-dom'; import Spinner from '../../components/Spinner'; -import { Routes } from '../../data/constants'; +import { Routes as ROUTES } from '../../data/constants'; const PostEditor = lazy(() => import('../posts/post-editor/PostEditor')); const PostCommentsView = lazy(() => import('../post-comments/PostCommentsView')); @@ -16,20 +16,20 @@ const DiscussionContent = () => {
)}> - {postEditorVisible ? ( - - - - ) : ( - - - - - - - - - )} + + {postEditorVisible ? ( + } /> + ) : ( + <> + {ROUTES.POSTS.EDIT_POST.map(route => ( + } /> + ))} + {ROUTES.COMMENTS.PATH.map(route => ( + } /> + ))} + + )} +
diff --git a/src/discussions/discussions-home/DiscussionSidebar.jsx b/src/discussions/discussions-home/DiscussionSidebar.jsx index 66c81633c..f1ef4182a 100644 --- a/src/discussions/discussions-home/DiscussionSidebar.jsx +++ b/src/discussions/discussions-home/DiscussionSidebar.jsx @@ -6,13 +6,13 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; import { - Redirect, Route, Switch, useLocation, -} from 'react-router'; + Navigate, Route, Routes, +} from 'react-router-dom'; import { useWindowSize } from '@edx/paragon'; import Spinner from '../../components/Spinner'; -import { RequestStatus, Routes } from '../../data/constants'; +import { RequestStatus, Routes as ROUTES } from '../../data/constants'; import { DiscussionContext } from '../common/context'; import { useContainerSize, useIsOnDesktop, useIsOnXLDesktop, useShowLearnersTab, @@ -27,7 +27,6 @@ const PostsView = lazy(() => import('../posts/PostsView')); const LegacyTopicsView = lazy(() => import('../topics/TopicsView')); const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => { - const location = useLocation(); const isOnDesktop = useIsOnDesktop(); const isOnXLDesktop = useIsOnXLDesktop(); const { enableInContextSidebar } = useContext(DiscussionContext); @@ -62,47 +61,60 @@ const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => { data-testid="sidebar" > )}> - + {enableInContext && !enableInContextSidebar && ( - + } + /> )} {enableInContext && !enableInContextSidebar && ( - + [ + ROUTES.TOPICS.TOPIC, + ROUTES.TOPICS.CATEGORY, + ROUTES.TOPICS.TOPIC_POST, + ROUTES.TOPICS.TOPIC_POST_EDIT, + ].map((route) => ( + } + /> + )) )} - - + {[ + ROUTES.POSTS.ALL_POSTS, + ROUTES.POSTS.EDIT_ALL_POSTS, + ROUTES.POSTS.MY_POSTS, + ROUTES.POSTS.EDIT_MY_POSTS, + ROUTES.TOPICS.CATEGORY, + ROUTES.TOPICS.CATEGORY_POST, + ROUTES.TOPICS.CATEGORY_POST_EDIT, + ROUTES.TOPICS.TOPIC, + ROUTES.TOPICS.TOPIC_POST, + ROUTES.TOPICS.TOPIC_POST_EDIT, + ].map((route) => ( + } + /> + ))} + {ROUTES.TOPICS.PATH.map(path => ( + } /> + ))} {redirectToLearnersTab && ( - + [ROUTES.LEARNERS.POSTS, ROUTES.LEARNERS.POSTS_EDIT].map((route) => ( + } /> + )) )} {redirectToLearnersTab && ( - + } /> )} {configStatus === RequestStatus.SUCCESSFUL && ( - + } /> )} - + ); diff --git a/src/discussions/discussions-home/DiscussionSidebar.test.jsx b/src/discussions/discussions-home/DiscussionSidebar.test.jsx index 653f1f492..dda145c82 100644 --- a/src/discussions/discussions-home/DiscussionSidebar.test.jsx +++ b/src/discussions/discussions-home/DiscussionSidebar.test.jsx @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; import { Context as ResponsiveContext } from 'react-responsive'; -import { MemoryRouter } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -28,8 +28,8 @@ function renderComponent(displaySidebar = true, location = `/${courseId}/`) { const wrapper = render( - - + + diff --git a/src/discussions/discussions-home/DiscussionsHome.jsx b/src/discussions/discussions-home/DiscussionsHome.jsx index a1616722a..83ad198ea 100644 --- a/src/discussions/discussions-home/DiscussionsHome.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.jsx @@ -4,14 +4,14 @@ import React, { lazy, Suspense, useRef } from 'react'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; import { - Route, Switch, useLocation, useRouteMatch, -} from 'react-router'; + matchPath, Route, Routes, useLocation, useMatch, +} from 'react-router-dom'; import { LearningHeader as Header } from '@edx/frontend-component-header'; import { Spinner } from '../../components'; import { selectCourseTabs } from '../../components/NavigationBar/data/selectors'; -import { ALL_ROUTES, DiscussionProvider, Routes } from '../../data/constants'; +import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants'; import { DiscussionContext } from '../common/context'; import { useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useShowLearnersTab, useSidebarVisible, @@ -40,8 +40,10 @@ const DiscussionsHome = () => { const provider = useSelector(selectDiscussionProvider); const enableInContext = useSelector(selectEnableInContext); const { courseNumber, courseTitle, org } = useSelector(selectCourseTabs); - const { params: { page } } = useRouteMatch(`${Routes.COMMENTS.PAGE}?`); - const { params } = useRouteMatch(ALL_ROUTES); + const pageParams = useMatch(ROUTES.COMMENTS.PAGE)?.params; + const page = pageParams?.page || null; + const matchPattern = ALL_ROUTES.find((route) => matchPath({ path: route }, location.pathname)); + const { params } = useMatch(matchPattern); const isRedirectToLearners = useShowLearnersTab(); const isOnDesktop = useIsOnDesktop(); let displaySidebar = useSidebarVisible(); @@ -95,12 +97,24 @@ const DiscussionsHome = () => { {provider === DiscussionProvider.LEGACY && ( - )}> - - + )}> + + {[ + ROUTES.TOPICS.CATEGORY, + ROUTES.TOPICS.CATEGORY_POST, + ROUTES.TOPICS.CATEGORY_POST_EDIT, + ROUTES.TOPICS.TOPIC, + ROUTES.TOPICS.TOPIC_POST, + ROUTES.TOPICS.TOPIC_POST_EDIT, + ].map((route) => ( + } + /> + ))} + + )}
)}> @@ -112,21 +126,29 @@ const DiscussionsHome = () => { )} {!displayContentArea && ( - - - } - /> - } - /> - {isRedirectToLearners && } - + + <> + {ROUTES.TOPICS.PATH.map(route => ( + : } + /> + ))} + } + /> + {[`${ROUTES.POSTS.PATH}/*`, ROUTES.POSTS.ALL_POSTS, ROUTES.LEARNERS.POSTS].map((route) => ( + } + /> + ))} + {isRedirectToLearners && } />} + + )}
{!enableInContextSidebar && ( diff --git a/src/discussions/discussions-home/DiscussionsHome.test.jsx b/src/discussions/discussions-home/DiscussionsHome.test.jsx index c2fd04b67..804f6413f 100644 --- a/src/discussions/discussions-home/DiscussionsHome.test.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.test.jsx @@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; import { Context as ResponsiveContext } from 'react-responsive'; -import { MemoryRouter } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -42,7 +42,7 @@ function renderComponent(location = `/${courseId}/`) { const wrapper = render( - + diff --git a/src/discussions/empty-posts/EmptyPosts.test.jsx b/src/discussions/empty-posts/EmptyPosts.test.jsx index 6d654a809..88f591b07 100644 --- a/src/discussions/empty-posts/EmptyPosts.test.jsx +++ b/src/discussions/empty-posts/EmptyPosts.test.jsx @@ -26,7 +26,7 @@ function renderComponent(location = `/${courseId}/`) { return render( - + diff --git a/src/discussions/empty-posts/EmptyTopics.jsx b/src/discussions/empty-posts/EmptyTopics.jsx index bcf8e5155..b2420803e 100644 --- a/src/discussions/empty-posts/EmptyTopics.jsx +++ b/src/discussions/empty-posts/EmptyTopics.jsx @@ -1,11 +1,10 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useRouteMatch } from 'react-router'; +import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { ALL_ROUTES } from '../../data/constants'; import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks'; import { selectTopicThreadCount } from '../data/selectors'; import messages from '../messages'; @@ -15,11 +14,11 @@ import EmptyPage from './EmptyPage'; const EmptyTopics = () => { const intl = useIntl(); - const match = useRouteMatch(ALL_ROUTES); + const { topicId } = useParams(); const dispatch = useDispatch(); const isOnDesktop = useIsOnDesktop(); const hasGlobalThreads = useTotalTopicThreadCount() > 0; - const topicThreadCount = useSelector(selectTopicThreadCount(match.params.topicId)); + const topicThreadCount = useSelector(selectTopicThreadCount(topicId)); const addPost = useCallback(() => ( dispatch(showPostEditor()) @@ -35,7 +34,7 @@ const EmptyTopics = () => { return null; } - if (match.params.topicId) { + if (topicId) { if (topicThreadCount > 0) { title = messages.noPostSelected; } else { diff --git a/src/discussions/empty-posts/EmptyTopics.test.jsx b/src/discussions/empty-posts/EmptyTopics.test.jsx index 0621c115d..31072a19a 100644 --- a/src/discussions/empty-posts/EmptyTopics.test.jsx +++ b/src/discussions/empty-posts/EmptyTopics.test.jsx @@ -2,14 +2,14 @@ import { render, screen } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; import { Context as ResponsiveContext } from 'react-responsive'; -import { MemoryRouter } from 'react-router'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; -import { getApiBaseUrl } from '../../data/constants'; +import { getApiBaseUrl, Routes as ROUTES } from '../../data/constants'; import { initializeStore } from '../../store'; import { executeThunk } from '../../test-utils'; import messages from '../messages'; @@ -26,9 +26,12 @@ function renderComponent(location = `/${courseId}/topics/`) { return render( - + - + + } /> + } /> + diff --git a/src/discussions/in-context-topics/TopicPostsView.test.jsx b/src/discussions/in-context-topics/TopicPostsView.test.jsx index f8abeb2d5..0679808a8 100644 --- a/src/discussions/in-context-topics/TopicPostsView.test.jsx +++ b/src/discussions/in-context-topics/TopicPostsView.test.jsx @@ -4,7 +4,9 @@ import { import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; -import { generatePath, MemoryRouter, Route } from 'react-router'; +import { + generatePath, MemoryRouter, Route, Routes, useLocation, +} from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -12,7 +14,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { PostActionsBar } from '../../components'; -import { Routes } from '../../data/constants'; +import { Routes as ROUTES } from '../../data/constants'; import { initializeStore } from '../../store'; import { executeThunk } from '../../test-utils'; import { DiscussionContext } from '../common/context'; @@ -35,16 +37,21 @@ let axiosMock; let lastLocation; let container; +const LocationComponent = () => { + lastLocation = useLocation(); + return null; +}; + async function renderComponent({ topicId, category } = { }) { let path = `/${courseId}/topics`; if (topicId) { - path = generatePath(Routes.POSTS.PATH, { courseId, topicId }); + path = generatePath(ROUTES.POSTS.PATH, { courseId, topicId }); } else if (category) { - path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category }); + path = generatePath(ROUTES.TOPICS.CATEGORY, { courseId, category }); } const wrapper = await render( - + - - - - - - - - { - lastLocation = location; - return null; - }} - /> + + { + [ + ROUTES.POSTS.PATH, + ROUTES.TOPICS.CATEGORY, + ].map((route) => ( + + + + + )} + /> + )) + } + + + + + + )} + /> + diff --git a/src/discussions/in-context-topics/TopicsView.test.jsx b/src/discussions/in-context-topics/TopicsView.test.jsx index f2da48c4a..4cc62c2e5 100644 --- a/src/discussions/in-context-topics/TopicsView.test.jsx +++ b/src/discussions/in-context-topics/TopicsView.test.jsx @@ -5,7 +5,9 @@ import { import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { + MemoryRouter, Route, Routes, useLocation, +} from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -32,24 +34,21 @@ let axiosMock; let lastLocation; let container; +const LocationComponent = () => { + lastLocation = useLocation(); + return null; +}; + function renderComponent() { const wrapper = render( - + - - - - - - - { - lastLocation = location; - return null; - }} - /> + + } /> + } /> + diff --git a/src/discussions/in-context-topics/components/BackButton.jsx b/src/discussions/in-context-topics/components/BackButton.jsx index a485013f8..2d28ff3d0 100644 --- a/src/discussions/in-context-topics/components/BackButton.jsx +++ b/src/discussions/in-context-topics/components/BackButton.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Icon, IconButton, Spinner } from '@edx/paragon'; @@ -12,7 +12,7 @@ import messages from '../messages'; const BackButton = ({ intl, path, title, loading, }) => { - const history = useHistory(); + const navigate = useNavigate(); return ( <> @@ -22,7 +22,7 @@ const BackButton = ({ iconAs={Icon} style={{ padding: '18px' }} size="inline" - onClick={() => history.push(path)} + onClick={() => navigate(path)} alt={intl.formatMessage(messages.backAlt)} />
diff --git a/src/discussions/in-context-topics/components/EmptyTopics.jsx b/src/discussions/in-context-topics/components/EmptyTopics.jsx index ef23c80c1..0f66d3489 100644 --- a/src/discussions/in-context-topics/components/EmptyTopics.jsx +++ b/src/discussions/in-context-topics/components/EmptyTopics.jsx @@ -1,11 +1,10 @@ import React, { useCallback, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useRouteMatch } from 'react-router'; +import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { ALL_ROUTES } from '../../../data/constants'; import { DiscussionContext } from '../../common/context'; import { useIsOnDesktop } from '../../data/hooks'; import { selectPostThreadCount } from '../../data/selectors'; @@ -16,11 +15,11 @@ import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../ const EmptyTopics = () => { const intl = useIntl(); - const match = useRouteMatch(ALL_ROUTES); + const { category, topicId } = useParams(); const dispatch = useDispatch(); const isOnDesktop = useIsOnDesktop(); const { enableInContextSidebar } = useContext(DiscussionContext); - const courseWareThreadsCount = useSelector(selectCourseWareThreadsCount(match.params.category)); + const courseWareThreadsCount = useSelector(selectCourseWareThreadsCount(category)); const topicThreadsCount = useSelector(selectPostThreadCount); // hasGlobalThreads is used to determine if there are any post available in courseware and non-courseware topics const hasGlobalThreads = useSelector(selectTotalTopicsThreadsCount) > 0; @@ -39,7 +38,7 @@ const EmptyTopics = () => { return null; } - if (match.params.topicId) { + if (topicId) { if (topicThreadsCount > 0) { title = messages.noPostSelected; } else { @@ -48,7 +47,7 @@ const EmptyTopics = () => { subTitle = messages.emptyTopic; fullWidth = true; } - } else if (match.params.category) { + } else if (category) { if (enableInContextSidebar && topicThreadsCount > 0) { title = messages.noPostSelected; } else if (courseWareThreadsCount > 0) { diff --git a/src/discussions/in-context-topics/topic/SectionBaseGroup.jsx b/src/discussions/in-context-topics/topic/SectionBaseGroup.jsx index 667eeeeea..faca47362 100644 --- a/src/discussions/in-context-topics/topic/SectionBaseGroup.jsx +++ b/src/discussions/in-context-topics/topic/SectionBaseGroup.jsx @@ -2,8 +2,7 @@ import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { useParams } from 'react-router'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -41,7 +40,7 @@ const SectionBaseGroup = ({ role="option" data-subsection-id={subsection.id} data-testid="subsection-group" - to={sectionUrl(subsection.id)} + to={sectionUrl(subsection.id)()} onClick={() => isSelected(subsection.id)} aria-current={isSelected(section.id) ? 'page' : undefined} tabIndex={(isSelected(subsection.id) || index === 0) ? 0 : -1} diff --git a/src/discussions/in-context-topics/topic/Topic.jsx b/src/discussions/in-context-topics/topic/Topic.jsx index 39b160839..b4f955861 100644 --- a/src/discussions/in-context-topics/topic/Topic.jsx +++ b/src/discussions/in-context-topics/topic/Topic.jsx @@ -5,8 +5,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import { useParams } from 'react-router'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon'; @@ -42,7 +41,7 @@ const Topic = ({ 'border-light-400 border-bottom': showDivider, })} data-topic-id={topic.id} - to={topicUrl} + to={topicUrl()} onClick={() => isSelected(topic.id)} aria-current={isSelected(topic.id) ? 'page' : undefined} role="option" diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx index a49890093..48f819360 100644 --- a/src/discussions/learners/LearnerPostsView.jsx +++ b/src/discussions/learners/LearnerPostsView.jsx @@ -4,7 +4,7 @@ import React, { import capitalize from 'lodash/capitalize'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -35,7 +35,7 @@ import messages from './messages'; const LearnerPostsView = () => { const intl = useIntl(); const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useDispatch(); const postsIds = useSelector(selectAllThreadsIds); @@ -83,7 +83,7 @@ const LearnerPostsView = () => { iconAs={Icon} style={{ padding: '18px' }} size="inline" - onClick={() => history.push(discussionsPath(Routes.LEARNERS.PATH, { courseId })(location))} + onClick={() => navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) })} alt={intl.formatMessage(messages.back)} />
diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx index 62d094679..cee5d14df 100644 --- a/src/discussions/learners/LearnerPostsView.test.jsx +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -6,7 +6,9 @@ import { import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { + MemoryRouter, Route, Routes, useLocation, +} from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -33,26 +35,26 @@ const username = 'abc123'; let container; let lastLocation; +const LocationComponent = () => { + lastLocation = useLocation(); + return null; +}; + async function renderComponent() { const wrapper = render( - + - - - - { - lastLocation = location; - return null; - }} - /> + + } /> + diff --git a/src/discussions/learners/LearnersView.jsx b/src/discussions/learners/LearnersView.jsx index 51e58a30e..7483f3b06 100644 --- a/src/discussions/learners/LearnersView.jsx +++ b/src/discussions/learners/LearnersView.jsx @@ -1,15 +1,13 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - Redirect, useLocation, useParams, -} from 'react-router'; +import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, Spinner } from '@edx/paragon'; import SearchInfo from '../../components/SearchInfo'; -import { RequestStatus, Routes } from '../../data/constants'; +import { RequestStatus } from '../../data/constants'; import { selectConfigLoadingStatus, selectLearnersTabEnabled } from '../data/selectors'; import NoResults from '../posts/NoResults'; import { @@ -27,7 +25,6 @@ import messages from './messages'; const LearnersView = () => { const intl = useIntl(); const { courseId } = useParams(); - const location = useLocation(); const dispatch = useDispatch(); const orderBy = useSelector(selectLearnerSorting()); const nextPage = useSelector(selectLearnerNextPage()); @@ -83,14 +80,6 @@ const LearnersView = () => { /> )}
- {courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && !learnersTabEnabled && ( - - )} {renderLearnersList} {loadingStatus === RequestStatus.IN_PROGRESS ? (
diff --git a/src/discussions/learners/LearnersView.test.jsx b/src/discussions/learners/LearnersView.test.jsx index 712f066c6..183ba07e0 100644 --- a/src/discussions/learners/LearnersView.test.jsx +++ b/src/discussions/learners/LearnersView.test.jsx @@ -7,7 +7,7 @@ import { import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; @@ -34,7 +34,7 @@ let container; function renderComponent() { const wrapper = render( - + - - - - + + + + + + )} + /> + diff --git a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx index d4281557f..6dd45c250 100644 --- a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx +++ b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx @@ -5,12 +5,10 @@ import { waitFor, } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; -import { generatePath, MemoryRouter, Route } from 'react-router'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; -import { Routes } from '../../../data/constants'; import { initializeStore } from '../../../store'; import { DiscussionContext } from '../../common/context'; import LearnerPostFilterBar from './LearnerPostFilterBar'; @@ -18,32 +16,21 @@ import LearnerPostFilterBar from './LearnerPostFilterBar'; let store; const username = 'abc123'; const courseId = 'course-v1:edX+DemoX+Demo_Course'; -const path = generatePath( - Routes.LEARNERS.POSTS, - { courseId, learnerUsername: username }, -); -function renderComponent() { - return render( - - - - - - - - - , - ); -} +const renderComponent = () => render( + + + + + + + , +); describe('LearnerPostFilterBar', () => { beforeEach(async () => { diff --git a/src/discussions/learners/learner/LearnerCard.jsx b/src/discussions/learners/learner/LearnerCard.jsx index 97acb808a..8304cd6c4 100644 --- a/src/discussions/learners/learner/LearnerCard.jsx +++ b/src/discussions/learners/learner/LearnerCard.jsx @@ -18,7 +18,7 @@ const LearnerCard = ({ learner }) => { 0: enableInContextSidebar ? 'in-context' : undefined, learnerUsername: learner.username, courseId, - }); + })(); return ( {showAllMsg} @@ -39,7 +39,7 @@ const BreadcrumbDropdown = ({ key={itemLabelFunc(item)} active={itemActiveFunc(item)} as={Link} - to={itemPathFunc(item)} + to={itemPathFunc(item)()} > {itemLabelFunc(item)} diff --git a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx index f9ad455e2..06b53c7a0 100644 --- a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx +++ b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { useRouteMatch } from 'react-router'; +import { useParams } from 'react-router-dom'; import { Routes } from '../../../data/constants'; import { @@ -15,12 +15,10 @@ import BreadcrumbDropdown from './BreadcrumbDropdown'; const LegacyBreadcrumbMenu = () => { const { - params: { - courseId, - category, - topicId: currentTopicId, - }, - } = useRouteMatch([Routes.TOPICS.CATEGORY, Routes.TOPICS.TOPIC]); + courseId, + category, + topicId: currentTopicId, + } = useParams(); const currentTopic = useSelector(selectTopic(currentTopicId)); const currentCategory = category || currentTopic?.categoryId; const decodedCurrentCategory = String(currentCategory).replace('%23', '#'); diff --git a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx index 1e9e9e089..e0eda57d7 100644 --- a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx +++ b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx @@ -5,14 +5,14 @@ import { } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; -import { getApiBaseUrl, Routes } from '../../../data/constants'; +import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants'; import { initializeStore } from '../../../store'; import { executeThunk } from '../../../test-utils'; import { fetchCourseTopics } from '../../topics/data/thunks'; @@ -28,15 +28,22 @@ let axiosMock; function renderComponent(path) { render( - + - + + { + [ + ROUTES.POSTS.PATH, + ROUTES.TOPICS.CATEGORY, + ].map((route) => ( + } + /> + )) + } + , diff --git a/src/discussions/navigation/navigation-bar/NavigationBar.jsx b/src/discussions/navigation/navigation-bar/NavigationBar.jsx index dd7ea886b..dd6a715b1 100644 --- a/src/discussions/navigation/navigation-bar/NavigationBar.jsx +++ b/src/discussions/navigation/navigation-bar/NavigationBar.jsx @@ -1,7 +1,6 @@ import React, { useContext, useMemo } from 'react'; -import { matchPath } from 'react-router'; -import { NavLink } from 'react-router-dom'; +import { matchPath, NavLink, useLocation } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Nav } from '@edx/paragon'; @@ -16,6 +15,8 @@ const NavigationBar = () => { const intl = useIntl(); const { courseId } = useContext(DiscussionContext); const showLearnersTab = useShowLearnersTab(); + const location = useLocation(); + const isTopicsNavActive = Boolean(matchPath({ path: `${Routes.TOPICS.CATEGORY}/*` }, location.pathname)); const navLinks = useMemo(() => ([ { @@ -28,7 +29,6 @@ const NavigationBar = () => { }, { route: Routes.TOPICS.ALL, - isActive: (match, location) => Boolean(matchPath(location.pathname, { path: Routes.TOPICS.PATH })), labelMessage: messages.allTopics, }, ]), []); @@ -49,8 +49,8 @@ const NavigationBar = () => { {intl.formatMessage(link.labelMessage)} diff --git a/src/discussions/post-comments/PostCommentsView.jsx b/src/discussions/post-comments/PostCommentsView.jsx index f44a0bf69..4407b5711 100644 --- a/src/discussions/post-comments/PostCommentsView.jsx +++ b/src/discussions/post-comments/PostCommentsView.jsx @@ -2,14 +2,16 @@ import React, { Suspense, useCallback, useContext, useEffect, useState, } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, Icon, IconButton } from '@edx/paragon'; import { ArrowBack } from '@edx/paragon/icons'; import Spinner from '../../components/Spinner'; -import { EndorsementStatus, PostsPages, ThreadType } from '../../data/constants'; +import { + EndorsementStatus, PostsPages, ThreadType, +} from '../../data/constants'; import { useDispatchWithState } from '../../data/hooks'; import { DiscussionContext } from '../common/context'; import { useIsOnDesktop } from '../data/hooks'; @@ -27,7 +29,7 @@ const CommentsView = React.lazy(() => import('./comments/CommentsView')); const PostCommentsView = () => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const isOnDesktop = useIsOnDesktop(); const [addingResponse, setAddingResponse] = useState(false); @@ -37,6 +39,9 @@ const PostCommentsView = () => { } = useContext(DiscussionContext); const commentsCount = useCommentsCount(postId); const { closed, id: threadId, type } = usePost(postId); + const redirectUrl = discussionsPath(PostsPages[page], { + courseId, learnerUsername, category, topicId, + })(location); useEffect(() => { if (!threadId) { @@ -89,9 +94,7 @@ const PostCommentsView = () => { variant="plain" className="px-0 line-height-24 py-0 my-1.5 border-0 font-weight-normal font-style text-primary-500" iconBefore={ArrowBack} - onClick={() => history.push(discussionsPath(PostsPages[page], { - courseId, learnerUsername, category, topicId, - })(location))} + onClick={() => navigate({ ...redirectUrl })} size="sm" > {intl.formatMessage(messages.backAlt)} @@ -106,9 +109,7 @@ const PostCommentsView = () => { style={{ padding: '18px' }} size="inline" className="ml-4 mt-4" - onClick={() => history.push(discussionsPath(PostsPages[page], { - courseId, learnerUsername, category, topicId, - })(location))} + onClick={() => navigate({ ...redirectUrl })} alt={intl.formatMessage(messages.backAlt)} /> ) diff --git a/src/discussions/post-comments/PostCommentsView.test.jsx b/src/discussions/post-comments/PostCommentsView.test.jsx index 07540f4ae..3bee05c2f 100644 --- a/src/discussions/post-comments/PostCommentsView.test.jsx +++ b/src/discussions/post-comments/PostCommentsView.test.jsx @@ -3,7 +3,9 @@ import { } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { + MemoryRouter, Route, Routes, useLocation, +} from 'react-router-dom'; import { Factory } from 'rosie'; import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform'; @@ -106,22 +108,28 @@ async function setupCourseConfig() { await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); } -function renderComponent(postId, isClosed = false) { +const LocationComponent = () => { + testLocation = useLocation(); + return null; +}; + +function renderComponent(postId, isClosed = false, page = 'posts', path = `/${courseId}/posts/${postId}`) { const wrapper = render( - + - + - { - testLocation = location; - return null; - }} - /> + + } + /> + @@ -427,6 +435,28 @@ describe('ThreadView', () => { expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`); }); + it('should show the editor if the post is edited on topics page', async () => { + await setupCourseConfig(false); + await waitFor(() => renderComponent( + discussionPostId, + false, + 'topics', + `/${courseId}/topics/topic-id/posts/${discussionPostId}`, + )); + + const post = await screen.findByTestId('post-thread-1'); + const hoverCard = within(post).getByTestId('hover-card-thread-1'); + await act(async () => { + fireEvent.click( + within(hoverCard).getByRole('button', { name: /actions menu/i }), + ); + }); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /edit/i })); + }); + expect(testLocation.pathname).toBe(`/${courseId}/topics/topic-id/posts/${discussionPostId}/edit`); + }); + it('should allow pinning the post', async () => { await waitFor(() => renderComponent(discussionPostId)); const post = await screen.findByTestId('post-thread-1'); diff --git a/src/discussions/posts/NoResults.test.jsx b/src/discussions/posts/NoResults.test.jsx index 78dc4eea2..0a92667c7 100644 --- a/src/discussions/posts/NoResults.test.jsx +++ b/src/discussions/posts/NoResults.test.jsx @@ -21,7 +21,7 @@ function renderComponent(location = `/${courseId}/`) { return render( - + diff --git a/src/discussions/posts/PostsView.test.jsx b/src/discussions/posts/PostsView.test.jsx index 7cc1efb15..d420270c7 100644 --- a/src/discussions/posts/PostsView.test.jsx +++ b/src/discussions/posts/PostsView.test.jsx @@ -5,15 +5,15 @@ import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; import { - generatePath, MemoryRouter, Route, Switch, -} from 'react-router'; + generatePath, MemoryRouter, Route, Routes, +} from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; -import { getApiBaseUrl, Routes, ThreadType } from '../../data/constants'; +import { getApiBaseUrl, Routes as ROUTES, ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; import { executeThunk } from '../../test-utils'; import { getCohortsApiUrl } from '../cohorts/data/api'; @@ -39,24 +39,24 @@ const username = 'abc123'; async function renderComponent({ postId, topicId, category, myPosts, enableInContextSidebar = false, } = { myPosts: false }) { - let path = generatePath(Routes.POSTS.ALL_POSTS, { courseId }); - let page; + let path = generatePath(ROUTES.POSTS.ALL_POSTS, { courseId }); + let page = 'posts'; if (postId) { - path = generatePath(Routes.POSTS.ALL_POSTS, { courseId, postId }); + path = generatePath(ROUTES.POSTS.ALL_POSTS, { courseId, postId }); page = 'posts'; } else if (topicId) { - path = generatePath(Routes.POSTS.PATH, { courseId, topicId }); - page = 'posts'; + path = generatePath(ROUTES.POSTS.PATH, { courseId, topicId }); + page = 'topics'; } else if (category) { - path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category }); + path = generatePath(ROUTES.TOPICS.CATEGORY, { courseId, category }); page = 'category'; } else if (myPosts) { - path = generatePath(Routes.POSTS.MY_POSTS, { courseId }); + path = generatePath(ROUTES.POSTS.MY_POSTS, { courseId }); page = 'my-posts'; } await render( - + - - - - - - + + { + [ + ROUTES.POSTS.PATH, + ROUTES.POSTS.MY_POSTS, + ROUTES.POSTS.ALL_POSTS, + ROUTES.TOPICS.CATEGORY, + ].map((route) => ( + } /> + )) + } + diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx index 8a15ca8aa..e626c3581 100644 --- a/src/discussions/posts/post-editor/PostEditor.jsx +++ b/src/discussions/posts/post-editor/PostEditor.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import { Formik } from 'formik'; import { isEmpty } from 'lodash'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import * as Yup from 'yup'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -55,7 +55,7 @@ const PostEditor = ({ editExisting, }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); const editorRef = useRef(null); @@ -126,7 +126,7 @@ const PostEditor = ({ learnerUsername: post?.author, category, })(location); - history.push(newLocation); + navigate({ ...newLocation }); } dispatch(hidePostEditor()); }, [postId, topicId, post?.author, category, editExisting, commentsPagePath, location]); diff --git a/src/discussions/posts/post-editor/PostEditor.test.jsx b/src/discussions/posts/post-editor/PostEditor.test.jsx index 49a15693d..ff75a5bdf 100644 --- a/src/discussions/posts/post-editor/PostEditor.test.jsx +++ b/src/discussions/posts/post-editor/PostEditor.test.jsx @@ -7,14 +7,14 @@ import userEvent from '@testing-library/user-event'; import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; -import { MemoryRouter, Route } from 'react-router'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Factory } from 'rosie'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; -import { getApiBaseUrl, Routes } from '../../../data/constants'; +import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants'; import { initializeStore } from '../../../store'; import { executeThunk } from '../../../test-utils'; import { getCohortsApiUrl } from '../../cohorts/data/api'; @@ -37,17 +37,19 @@ let axiosMock; let container; async function renderComponent(editExisting = false, location = `/${courseId}/posts/`) { - const path = editExisting ? Routes.POSTS.EDIT_POST : Routes.POSTS.NEW_POSTS; + const paths = editExisting ? ROUTES.POSTS.EDIT_POST : [ROUTES.POSTS.NEW_POST]; const wrapper = await render( - + - - - + + {paths.map((path) => ( + } /> + ))} + diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx index 721397bbf..7e5464002 100644 --- a/src/discussions/posts/post/Post.jsx +++ b/src/discussions/posts/post/Post.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { toString } from 'lodash'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -19,6 +19,7 @@ import HoverCard from '../../common/HoverCard'; import { ContentTypes } from '../../data/constants'; import { selectUserHasModerationPrivileges } from '../../data/selectors'; import { selectTopic } from '../../topics/data/selectors'; +import { truncatePath } from '../../utils'; import { selectThread } from '../data/selectors'; import { removeThread, updateExistingThread } from '../data/thunks'; import ClosePostReasonModal from './ClosePostReasonModal'; @@ -35,7 +36,7 @@ const Post = ({ handleAddResponseButton }) => { } = useSelector(selectThread(postId)); const intl = useIntl(); const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useDispatch(); const { courseId } = useContext(DiscussionContext); const topic = useSelector(selectTopic(topicId)); @@ -48,9 +49,11 @@ const Post = ({ handleAddResponseButton }) => { const displayPostFooter = following || voteCount || closed || (groupId && userHasModerationPrivileges); const handleDeleteConfirmation = useCallback(async () => { + const basePath = truncatePath(location.pathname); + await dispatch(removeThread(postId)); - history.push({ - pathname: '.', + navigate({ + pathname: basePath, search: enableInContextSidebar && '?inContextSidebar', }); hideDeleteConfirmation(); @@ -61,7 +64,7 @@ const Post = ({ handleAddResponseButton }) => { hideReportConfirmation(); }, [abuseFlagged, postId, hideReportConfirmation]); - const handlePostContentEdit = useCallback(() => history.push({ + const handlePostContentEdit = useCallback(() => navigate({ ...location, pathname: `${location.pathname}/edit`, }), [location.pathname]); diff --git a/src/discussions/posts/post/PostLink.jsx b/src/discussions/posts/post/PostLink.jsx index 5db61bf9c..5035c35fa 100644 --- a/src/discussions/posts/post/PostLink.jsx +++ b/src/discussions/posts/post/PostLink.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Badge, Icon } from '@edx/paragon'; @@ -25,6 +25,7 @@ const PostLink = ({ showDivider, }) => { const intl = useIntl(); + const { search } = useLocation(); const { courseId, postId: selectedPostId, @@ -37,14 +38,14 @@ const PostLink = ({ topicId, hasEndorsed, type, author, authorLabel, abuseFlagged, abuseFlaggedCount, read, commentCount, unreadCommentCount, id, pinned, previewBody, title, voted, voteCount, following, groupId, groupName, createdAt, } = useSelector(selectThread(postId)); - const linkUrl = discussionsPath(Routes.COMMENTS.PAGES[page], { + const { pathname } = discussionsPath(Routes.COMMENTS.PAGES[page], { 0: enableInContextSidebar ? 'in-context' : undefined, courseId, topicId, postId, category, learnerUsername, - }); + })(); const showAnsweredBadge = hasEndorsed && type === ThreadType.QUESTION; const authorLabelColor = AvatarOutlineAndLabelColors[authorLabel]; const canSeeReportedBadge = abuseFlagged || abuseFlaggedCount; @@ -63,7 +64,7 @@ const PostLink = ({ 'border-bottom border-light-400': showDivider, }) } - to={linkUrl} + to={`${pathname}${enableInContextSidebar ? search : ''}`} aria-current={checkIsSelected ? 'page' : undefined} role="option" tabIndex={(checkIsSelected || idx === 0) ? 0 : -1} diff --git a/src/discussions/posts/post/PostLink.test.jsx b/src/discussions/posts/post/PostLink.test.jsx index d37293673..42c068ec9 100644 --- a/src/discussions/posts/post/PostLink.test.jsx +++ b/src/discussions/posts/post/PostLink.test.jsx @@ -49,7 +49,7 @@ function renderComponent(id) { return render( - + { + lastLocation = useLocation(); + return null; +}; + function renderComponent() { const wrapper = render( - + - - - - - - - { - lastLocation = location; - return null; - }} - /> + + } /> + } /> + diff --git a/src/discussions/topics/topic-group/TopicGroupBase.jsx b/src/discussions/topics/topic-group/TopicGroupBase.jsx index 18eed076f..08861e49f 100644 --- a/src/discussions/topics/topic-group/TopicGroupBase.jsx +++ b/src/discussions/topics/topic-group/TopicGroupBase.jsx @@ -2,7 +2,7 @@ import React, { useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -20,10 +20,15 @@ const TopicGroupBase = ({ topicsIds, }) => { const intl = useIntl(); - const { courseId } = useContext(DiscussionContext); + const { search } = useLocation(); + const { courseId, enableInContextSidebar } = useContext(DiscussionContext); const filter = useSelector(selectTopicFilter); const topics = useSelector(selectTopicsById(topicsIds)); const hasTopics = topics.length > 0; + const { pathname } = discussionsPath(Routes.TOPICS.CATEGORY, { + courseId, + category: groupId, + })(); const matchesFilter = useMemo(() => ( filter ? groupTitle?.toLowerCase().includes(filter) : true @@ -69,10 +74,7 @@ const TopicGroupBase = ({ {linkToGroup && groupId ? ( {groupTitle} diff --git a/src/discussions/topics/topic-group/topic/Topic.jsx b/src/discussions/topics/topic-group/topic/Topic.jsx index 6e4ed220f..838998e9d 100644 --- a/src/discussions/topics/topic-group/topic/Topic.jsx +++ b/src/discussions/topics/topic-group/topic/Topic.jsx @@ -1,18 +1,18 @@ /* eslint-disable react/prop-types */ /* eslint-disable no-unused-vars, react/forbid-prop-types */ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import { useParams } from 'react-router'; -import { Link } from 'react-router-dom'; +import { Link, useLocation, useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon'; import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons'; import { Routes } from '../../../../data/constants'; +import { DiscussionContext } from '../../../common/context'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../../data/selectors'; import { discussionsPath } from '../../../utils'; import { selectTopic } from '../../data/selectors'; @@ -20,6 +20,8 @@ import messages from '../../messages'; const Topic = ({ topicId, showDivider, index }) => { const intl = useIntl(); + const { search } = useLocation(); + const { enableInContextSidebar } = useContext(DiscussionContext); const { courseId } = useParams(); const topic = useSelector(selectTopic(topicId)); const { @@ -28,7 +30,7 @@ const Topic = ({ topicId, showDivider, index }) => { const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsGroupTa = useSelector(selectUserIsGroupTa); const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa); - const topicUrl = discussionsPath(Routes.TOPICS.TOPIC, { courseId, topicId }); + const { pathname } = discussionsPath(Routes.TOPICS.TOPIC, { courseId, topicId })(); const isSelected = useCallback((selectedId) => ( window.location.pathname.includes(selectedId) @@ -42,7 +44,7 @@ const Topic = ({ topicId, showDivider, index }) => { }) } data-topic-id={id} - to={topicUrl} + to={`${pathname}${enableInContextSidebar ? search : ''}`} onClick={() => isSelected(id)} aria-current={isSelected(id) ? 'page' : undefined} role="option" diff --git a/src/discussions/utils.js b/src/discussions/utils.js index b7c45ba4f..64e0eb8cb 100644 --- a/src/discussions/utils.js +++ b/src/discussions/utils.js @@ -3,7 +3,9 @@ import { useCallback, useContext, useMemo } from 'react'; import { getIn } from 'formik'; import { uniqBy } from 'lodash'; import { useSelector } from 'react-redux'; -import { generatePath, useRouteMatch } from 'react-router'; +import { + generatePath, matchPath, useLocation, +} from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { @@ -11,7 +13,9 @@ import { } from '@edx/paragon/icons'; import { InsertLink } from '../components/icons'; -import { ContentActions, Routes, ThreadType } from '../data/constants'; +import { + ContentActions, Routes, ThreadType, +} from '../data/constants'; import { ContentSelectors } from './data/constants'; import { PostCommentsContext } from './post-comments/postCommentsContext'; import messages from './messages'; @@ -42,8 +46,9 @@ export function isFormikFieldInvalid(field, { * @returns {string} */ export function useCommentsPagePath() { - const { params } = useRouteMatch(Routes.COMMENTS.PAGE); - return Routes.COMMENTS.PAGES[params.page]; + const location = useLocation(); + const { params: { page } } = matchPath({ path: Routes.COMMENTS.PAGE }, location.pathname); + return Routes.COMMENTS.PAGES[page]; } /** @@ -284,3 +289,7 @@ export function handleKeyDown(event) { export function isLastElementOfList(list, element) { return list[list.length - 1] === element; } + +export function truncatePath(path) { + return path.substring(0, path.lastIndexOf('/')); +}