feat: upgrade react router to v6 (#542)

* feat: upgrade react router to v6

* fix: routing issues

* fix: category route should redirect to all posts

* fix: path error on routes
This commit is contained in:
Syed Ali Abbas Zaidi
2023-12-07 18:10:48 +05:00
committed by GitHub
parent b36c0266fd
commit b35632df64
45 changed files with 490 additions and 412 deletions

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider
value={{ courseId, postId }}
value={{ courseId, postId, page: 'posts' }}
>
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
<DiscussionContent />
<Route
path="*"
/>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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]);
}

View File

@@ -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 = () => {
<div className="d-flex bg-light-400 flex-column w-75 w-xs-100 w-xl-75 align-items-center">
<div className="d-flex flex-column w-100">
<Suspense fallback={(<Spinner />)}>
{postEditorVisible ? (
<Route path={Routes.POSTS.NEW_POST}>
<PostEditor />
</Route>
) : (
<Switch>
<Route path={Routes.POSTS.EDIT_POST}>
<PostEditor editExisting />
</Route>
<Route path={Routes.COMMENTS.PATH}>
<PostCommentsView />
</Route>
</Switch>
)}
<Routes>
{postEditorVisible ? (
<Route path={ROUTES.POSTS.NEW_POST} element={<PostEditor />} />
) : (
<>
{ROUTES.POSTS.EDIT_POST.map(route => (
<Route key={route} path={route} element={<PostEditor editExisting />} />
))}
{ROUTES.COMMENTS.PATH.map(route => (
<Route key={route} path={route} element={<PostCommentsView />} />
))}
</>
)}
</Routes>
</Suspense>
</div>
</div>

View File

@@ -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"
>
<Suspense fallback={(<Spinner />)}>
<Switch>
<Routes>
{enableInContext && !enableInContextSidebar && (
<Route
path={Routes.TOPICS.ALL}
component={InContextTopicsView}
exact
/>
<Route
path={ROUTES.TOPICS.ALL}
element={<InContextTopicsView />}
/>
)}
{enableInContext && !enableInContextSidebar && (
<Route
path={[
Routes.TOPICS.TOPIC,
Routes.TOPICS.CATEGORY,
Routes.TOPICS.TOPIC_POST,
Routes.TOPICS.TOPIC_POST_EDIT,
]}
component={TopicPostsView}
exact
/>
[
ROUTES.TOPICS.TOPIC,
ROUTES.TOPICS.CATEGORY,
ROUTES.TOPICS.TOPIC_POST,
ROUTES.TOPICS.TOPIC_POST_EDIT,
].map((route) => (
<Route
key={route}
path={route}
element={<TopicPostsView />}
/>
))
)}
<Route
path={[Routes.POSTS.ALL_POSTS, Routes.POSTS.MY_POSTS, Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
component={PostsView}
/>
<Route path={Routes.TOPICS.PATH} component={LegacyTopicsView} />
{[
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) => (
<Route
key={route}
path={route}
element={<PostsView />}
/>
))}
{ROUTES.TOPICS.PATH.map(path => (
<Route key={path} path={path} element={<LegacyTopicsView />} />
))}
{redirectToLearnersTab && (
<Route path={Routes.LEARNERS.POSTS} component={LearnerPostsView} />
[ROUTES.LEARNERS.POSTS, ROUTES.LEARNERS.POSTS_EDIT].map((route) => (
<Route key={route} path={route} element={<LearnerPostsView />} />
))
)}
{redirectToLearnersTab && (
<Route path={Routes.LEARNERS.PATH} component={LearnersView} />
<Route path={ROUTES.LEARNERS.PATH} element={<LearnersView />} />
)}
{configStatus === RequestStatus.SUCCESSFUL && (
<Redirect
from={Routes.DISCUSSIONS.PATH}
to={{
...location,
pathname: Routes.POSTS.ALL_POSTS,
}}
/>
<Route path={`${ROUTES.DISCUSSIONS.PATH}/*`} element={<Navigate to="posts" />} />
)}
</Switch>
</Routes>
</Suspense>
</div>
);

View File

@@ -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(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<DiscussionContext.Provider value={{ courseId }}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider value={{ courseId, page: 'posts' }}>
<MemoryRouter initialEntries={[location]}>
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={null} />
</MemoryRouter>

View File

@@ -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 = () => {
<DiscussionsRestrictionBanner />
</div>
{provider === DiscussionProvider.LEGACY && (
<Suspense fallback={(<Spinner />)}>
<Route
path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
component={LegacyBreadcrumbMenu}
/>
</Suspense>
<Suspense fallback={(<Spinner />)}>
<Routes>
{[
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) => (
<Route
key={route}
path={route}
element={<LegacyBreadcrumbMenu />}
/>
))}
</Routes>
</Suspense>
)}
<div className="d-flex flex-row position-relative">
<Suspense fallback={(<Spinner />)}>
@@ -112,21 +126,29 @@ const DiscussionsHome = () => {
</Suspense>
)}
{!displayContentArea && (
<Switch>
<Route
path={Routes.TOPICS.PATH}
component={(enableInContext || enableInContextSidebar) ? InContextEmptyTopics : EmptyTopics}
/>
<Route
path={Routes.POSTS.MY_POSTS}
render={routeProps => <EmptyPosts {...routeProps} subTitleMessage={messages.emptyMyPosts} />}
/>
<Route
path={[Routes.POSTS.PATH, Routes.POSTS.ALL_POSTS, Routes.LEARNERS.POSTS]}
render={routeProps => <EmptyPosts {...routeProps} subTitleMessage={messages.emptyAllPosts} />}
/>
{isRedirectToLearners && <Route path={Routes.LEARNERS.PATH} component={EmptyLearners} />}
</Switch>
<Routes>
<>
{ROUTES.TOPICS.PATH.map(route => (
<Route
key={route}
path={`${route}/*`}
element={(enableInContext || enableInContextSidebar) ? <InContextEmptyTopics /> : <EmptyTopics />}
/>
))}
<Route
path={ROUTES.POSTS.MY_POSTS}
element={<EmptyPosts subTitleMessage={messages.emptyMyPosts} />}
/>
{[`${ROUTES.POSTS.PATH}/*`, ROUTES.POSTS.ALL_POSTS, ROUTES.LEARNERS.POSTS].map((route) => (
<Route
key={route}
path={route}
element={<EmptyPosts subTitleMessage={messages.emptyAllPosts} />}
/>
))}
{isRedirectToLearners && <Route path={ROUTES.LEARNERS.PATH} element={<EmptyLearners />} />}
</>
</Routes>
)}
</div>
{!enableInContextSidebar && (

View File

@@ -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(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[location]}>
<DiscussionsHome />
</MemoryRouter>

View File

@@ -26,7 +26,7 @@ function renderComponent(location = `/${courseId}/`) {
return render(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[location]}>
<EmptyPosts subTitleMessage={messages.emptyMyPosts} />
</MemoryRouter>

View File

@@ -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 {

View File

@@ -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(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[location]}>
<EmptyTopics />
<Routes>
<Route path={ROUTES.TOPICS.ALL} element={<EmptyTopics />} />
<Route path={ROUTES.TOPICS.TOPIC} element={<EmptyTopics />} />
</Routes>
</MemoryRouter>
</AppProvider>
</ResponsiveContext.Provider>

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider value={{
courseId,
topicId,
@@ -53,19 +60,35 @@ async function renderComponent({ topicId, category } = { }) {
}}
>
<MemoryRouter initialEntries={[path]}>
<Route exact path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}>
<TopicPostsView />
</Route>
<Route exact path={[Routes.TOPICS.ALL]}>
<PostActionsBar />
<TopicsView />
</Route>
<Route
render={({ location }) => {
lastLocation = location;
return null;
}}
/>
<Routes>
{
[
ROUTES.POSTS.PATH,
ROUTES.TOPICS.CATEGORY,
].map((route) => (
<Route
key={route}
path={route}
element={(
<>
<TopicPostsView />
<LocationComponent />
</>
)}
/>
))
}
<Route
path={ROUTES.TOPICS.ALL}
element={(
<>
<PostActionsBar />
<TopicsView />
<LocationComponent />
</>
)}
/>
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider value={{ courseId, category }}>
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
<Route path="/:courseId/topics/">
<TopicsView />
</Route>
<Route path="/:courseId/category/:category">
<TopicPostsView />
</Route>
<Route
render={({ location }) => {
lastLocation = location;
return null;
}}
/>
<Routes>
<Route path="/:courseId/topics/*" element={<><TopicsView /><LocationComponent /></>} />
<Route path="/:courseId/category/:category" element={<><TopicPostsView /><LocationComponent /></>} />
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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)}
/>
<div className="d-flex flex-fill justify-content-center align-items-center mr-4.5">

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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"

View File

@@ -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)}
/>
<div className="text-primary-500 font-style font-weight-bold py-2.5">

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider
value={{
learnerUsername: username,
courseId,
page: 'learners',
}}
>
<MemoryRouter initialEntries={[`/${courseId}/learners/${username}/posts`]}>
<Route path="/:courseId/learners/:learnerUsername/posts">
<LearnerPostsView />
</Route>
<Route
render={({ location }) => {
lastLocation = location;
return null;
}}
/>
<Routes>
<Route path="/:courseId/learners/:learnerUsername?/posts?" element={<><LearnerPostsView /><LocationComponent /></>} />
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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 = () => {
/>
)}
<div className="list-group list-group-flush learner" role="list">
{courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && !learnersTabEnabled && (
<Redirect
to={{
...location,
pathname: Routes.DISCUSSIONS.PATH,
}}
/>
)}
{renderLearnersList}
{loadingStatus === RequestStatus.IN_PROGRESS ? (
<div className="d-flex justify-content-center p-4">

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider value={{
page: 'learners',
learnerUsername: 'learner-1',
@@ -42,10 +42,17 @@ function renderComponent() {
}}
>
<MemoryRouter initialEntries={[`/${courseId}/`]}>
<Route path="/:courseId/">
<PostActionsBar />
<LearnersView />
</Route>
<Routes>
<Route
path="/:courseId/"
element={(
<>
<PostActionsBar />
<LearnersView />
</>
)}
/>
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider
value={{
learnerUsername: username,
courseId,
}}
>
<MemoryRouter initialEntries={[path]}>
<Route
path={Routes.LEARNERS.POSTS}
component={LearnerPostFilterBar}
/>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,
);
}
const renderComponent = () => render(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider
value={{
learnerUsername: username,
courseId,
}}
>
<LearnerPostFilterBar />
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,
);
describe('LearnerPostFilterBar', () => {
beforeEach(async () => {

View File

@@ -18,7 +18,7 @@ const LearnerCard = ({ learner }) => {
0: enableInContextSidebar ? 'in-context' : undefined,
learnerUsername: learner.username,
courseId,
});
})();
return (
<Link

View File

@@ -29,7 +29,7 @@ const BreadcrumbDropdown = ({
key="null"
active={!currentItem}
as={Link}
to={showAllPath}
to={showAllPath()}
>
{showAllMsg}
</Dropdown.Item>
@@ -39,7 +39,7 @@ const BreadcrumbDropdown = ({
key={itemLabelFunc(item)}
active={itemActiveFunc(item)}
as={Link}
to={itemPathFunc(item)}
to={itemPathFunc(item)()}
>
{itemLabelFunc(item)}
</Dropdown.Item>

View File

@@ -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', '#');

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[path]}>
<Route
path={[
Routes.POSTS.PATH,
Routes.TOPICS.CATEGORY,
]}
component={LegacyBreadcrumbMenu}
/>
<Routes>
{
[
ROUTES.POSTS.PATH,
ROUTES.TOPICS.CATEGORY,
].map((route) => (
<Route
key={route}
path={route}
element={<LegacyBreadcrumbMenu />}
/>
))
}
</Routes>
</MemoryRouter>
</AppProvider>
</IntlProvider>,

View File

@@ -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 = () => {
<Nav.Link
key={link.route}
as={NavLink}
to={discussionsPath(link.route, { courseId })}
isActive={link.isActive}
to={discussionsPath(link.route, { courseId })()}
className={isTopicsNavActive && link.route === Routes.TOPICS.ALL && 'active'}
>
{intl.formatMessage(link.labelMessage)}
</Nav.Link>

View File

@@ -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)}
/>
)

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider
value={{ courseId, postId, isClosed }}
value={{
courseId, postId, page, isClosed, topicId: 'topic-id',
}}
>
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
<MemoryRouter initialEntries={[path]}>
<DiscussionContent />
<Route
path="*"
render={({ location }) => {
testLocation = location;
return null;
}}
/>
<Routes>
<Route
path="*"
element={<LocationComponent />}
/>
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>
@@ -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');

View File

@@ -21,7 +21,7 @@ function renderComponent(location = `/${courseId}/`) {
return render(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[location]}>
<NoResults />
</MemoryRouter>

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[path]}>
<DiscussionContext.Provider value={{
courseId,
@@ -67,15 +67,18 @@ async function renderComponent({
enableInContextSidebar,
}}
>
<Switch>
<Route path={Routes.POSTS.MY_POSTS}>
<PostsView />
</Route>
<Route
path={[Routes.POSTS.PATH, Routes.POSTS.ALL_POSTS, Routes.TOPICS.CATEGORY]}
component={PostsView}
/>
</Switch>
<Routes>
{
[
ROUTES.POSTS.PATH,
ROUTES.POSTS.MY_POSTS,
ROUTES.POSTS.ALL_POSTS,
ROUTES.TOPICS.CATEGORY,
].map((route) => (
<Route key={route} path={route} element={<PostsView />} />
))
}
</Routes>
</DiscussionContext.Provider>
</MemoryRouter>
</AppProvider>

View File

@@ -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]);

View File

@@ -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(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider
value={{ courseId, category: null }}
>
<MemoryRouter initialEntries={[location]}>
<Route path={path}>
<PostEditor editExisting={editExisting} />
</Route>
<Routes>
{paths.map((path) => (
<Route path={path} element={<PostEditor editExisting={editExisting} />} />
))}
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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]);

View File

@@ -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}

View File

@@ -49,7 +49,7 @@ function renderComponent(id) {
return render(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider value={{ courseId }}>
<DiscussionContext.Provider value={{ courseId, page: 'posts' }}>
<PostLink
key={id}
postId={id}

View File

@@ -3,7 +3,7 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { useParams } from 'react-router-dom';
import SearchInfo from '../../components/SearchInfo';
import { RequestStatus } from '../../data/constants';

View File

@@ -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 { initializeMockApp } from '@edx/frontend-platform';
@@ -28,24 +30,21 @@ let axiosMock;
let lastLocation;
let container;
const LocationComponent = () => {
lastLocation = useLocation();
return null;
};
function renderComponent() {
const wrapper = render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<DiscussionContext.Provider value={{ courseId }}>
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
<Route path="/:courseId/topics/">
<TopicsView />
</Route>
<Route path="/:courseId/category/:category">
<TopicsView />
</Route>
<Route
render={({ location }) => {
lastLocation = location;
return null;
}}
/>
<Routes>
<Route path="/:courseId/topics/*" element={<><TopicsView /><LocationComponent /></>} />
<Route path="/:courseId/category/:category" element={<><TopicsView /><LocationComponent /></>} />
</Routes>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -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 ? (
<Link
className="text-decoration-none text-primary-500"
to={discussionsPath(Routes.TOPICS.CATEGORY, {
courseId,
category: groupId,
})}
to={`${pathname}${enableInContextSidebar ? search : ''}`}
>
{groupTitle}
</Link>

View File

@@ -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"

View File

@@ -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('/'));
}