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:
committed by
GitHub
parent
b36c0266fd
commit
b35632df64
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ const LearnerCard = ({ learner }) => {
|
||||
0: enableInContextSidebar ? 'in-context' : undefined,
|
||||
learnerUsername: learner.username,
|
||||
courseId,
|
||||
});
|
||||
})();
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', '#');
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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('/'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user