diff --git a/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx b/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx
index a4ace026..2a2a6b0a 100644
--- a/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx
+++ b/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx
@@ -1,66 +1,91 @@
import React from 'react';
-import { useSelector } from 'react-redux';
-import { generatePath, useHistory, useRouteMatch } from 'react-router';
+import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Breadcrumb } from '@edx/paragon';
+import { Dropdown, DropdownButton } from '@edx/paragon';
-import { Routes } from '../../../data/constants';
-import { selectTopic } from '../../topics/data/selectors';
+import { selectBlocks, selectChapters, selectCurrentSelection } from '../../../data/selectors';
+import { setCurrentChapter, setCurrentSequential, setCurrentVertical } from '../../../data/slices';
import messages from './messages';
function BreadcrumbMenu({ intl }) {
+ const dispatch = useDispatch();
+ const blocks = useSelector(selectBlocks);
+ const chapters = useSelector(selectChapters);
const {
- params: {
- courseId,
- category,
- topicId,
- },
- } = useRouteMatch([Routes.TOPICS.CATEGORY, Routes.TOPICS.TOPIC]);
- const topic = useSelector(selectTopic(topicId));
- const history = useHistory();
+ currentChapter,
+ currentVertical,
+ currentSequential,
+ } = useSelector(selectCurrentSelection);
- const crumbs = [
- {
- url: () => generatePath(Routes.TOPICS.ALL, { courseId }),
- label: intl.formatMessage(messages.allTopics),
- },
- {
- url: () => generatePath(Routes.TOPICS.CATEGORY, {
- courseId,
- category: category || topic?.categoryId,
- }),
- label: category || topic?.categoryId,
- },
- {
- url: () => generatePath(Routes.TOPICS.TOPIC, {
- courseId,
- topicId,
- }),
- label: topic?.name,
- },
- ].filter(crumb => Boolean(crumb.label))
- .map(({
- url,
- label,
- }) => ({
- url: url(),
- label,
- }));
-
- const activeLabel = crumbs.pop().label;
+ const showAllMsg = intl.formatMessage(messages.showAll);
return (
-
{
- e.preventDefault();
- history.push(e.target.pathname);
- }}
- />
+ dispatch(setCurrentChapter(cId))}
+ >
+
+ {showAllMsg}
+
+ {chapters.map(chapter => (
+ chapter.topics.length > 0
+ && (
+
+ {chapter.displayName}
+
+ )
+ ))}
+
+ {currentChapter
+ && (
+ <>
+ /
+ dispatch(setCurrentSequential(sId))}
+ >
+
+ {showAllMsg}
+
+ {blocks[currentChapter].children.map(seqId => (
+ blocks[seqId].topics.length > 0
+ && (
+
+ {blocks[seqId].displayName}
+
+ )
+ ))}
+
+ >
+ )}
+ {currentSequential
+ && (
+ <>
+ /
+ dispatch(setCurrentVertical(vId))}
+ >
+
+ {showAllMsg}
+
+ {blocks[currentSequential]?.children?.map(vertId => (
+ blocks[vertId].topics.length > 0
+ && (
+
+ {blocks[vertId].displayName}
+
+ )
+ ))}
+
+ >
+ )}
);
}
diff --git a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx
new file mode 100644
index 00000000..7399b846
--- /dev/null
+++ b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.jsx
@@ -0,0 +1,120 @@
+import React from 'react';
+
+import { useSelector } from 'react-redux';
+import { generatePath, useHistory, useRouteMatch } from 'react-router';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Dropdown, DropdownButton } from '@edx/paragon';
+
+import { Routes } from '../../../data/constants';
+import {
+ selectCategories,
+ selectNonCoursewareTopics,
+ selectTopic,
+ selectTopicsInCategory,
+} from '../../topics/data/selectors';
+import messages from './messages';
+
+function LegacyBreadcrumbMenu({ intl }) {
+ const history = useHistory();
+ const {
+ params: {
+ courseId,
+ category,
+ topicId: currentTopicId,
+ },
+ } = useRouteMatch([Routes.TOPICS.CATEGORY, Routes.TOPICS.TOPIC]);
+
+ const currentTopic = useSelector(selectTopic(currentTopicId));
+ const currentCategory = category || currentTopic?.categoryId;
+ const topicsInCategory = useSelector(selectTopicsInCategory(currentCategory));
+ const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
+ const categories = useSelector(selectCategories);
+ const showAllMsg = intl.formatMessage(messages.showAll);
+ const isNonCoursewareTopic = currentTopic && !currentCategory;
+
+ const navigateToCategory = (categoryId) => {
+ if (!categoryId) {
+ history.push(generatePath(Routes.TOPICS.ALL, {
+ courseId,
+ category: categoryId,
+ }));
+ } else {
+ history.push(generatePath(Routes.TOPICS.CATEGORY, {
+ courseId,
+ category: categoryId,
+ }));
+ }
+ };
+ const navigateToTopic = (topicId) => {
+ if (!topicId) {
+ navigateToCategory(currentCategory);
+ } else {
+ history.push(generatePath(Routes.TOPICS.TOPIC, {
+ courseId,
+ topicId,
+ }));
+ }
+ };
+
+ return (
+
+ {isNonCoursewareTopic ? (
+
+
+ {showAllMsg}
+
+ {nonCoursewareTopics.map(topic => (
+
+ {topic.name}
+
+ ))}
+
+ ) : (
+
+
+ {showAllMsg}
+
+ {categories.map(categoryId => (
+
+ {categoryId}
+
+ ))}
+
+ )}
+ {currentCategory && (
+ <>
+
/
+
+
+ {showAllMsg}
+
+ {topicsInCategory?.map(topic => (
+
+ {topic.name}
+
+ ))}
+
+ >
+ )}
+
+ );
+}
+
+LegacyBreadcrumbMenu.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(LegacyBreadcrumbMenu);
diff --git a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx
new file mode 100644
index 00000000..51b100e3
--- /dev/null
+++ b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx
@@ -0,0 +1,112 @@
+import React from 'react';
+
+import {
+ act, fireEvent, render, screen,
+} from '@testing-library/react';
+import MockAdapter from 'axios-mock-adapter';
+import { IntlProvider } from 'react-intl';
+import { MemoryRouter, Route } from 'react-router';
+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 { API_BASE_URL, Routes } from '../../../data/constants';
+import { initializeStore } from '../../../store';
+import { executeThunk } from '../../../test-utils';
+import { fetchCourseTopics } from '../../topics/data/thunks';
+import { LegacyBreadcrumbMenu } from '../index';
+
+import '../../topics/data/__factories__';
+
+const courseId = 'course-v1:edX+TestX+Test_Course';
+const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
+let store;
+let axiosMock;
+
+function renderComponent(path) {
+ render(
+
+
+
+
+
+
+ ,
+ );
+}
+
+describe('LegacyBreadcrumbMenu', () => {
+ beforeEach(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore({
+ blocks: {
+ topics: {},
+ },
+ });
+ Factory.resetAll();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ const data = {
+ courseware_topics: Factory.buildList('category', 3),
+ non_courseware_topics: Factory.buildList('topic', 3),
+ };
+ axiosMock
+ .onGet(topicsApiUrl)
+ .reply(200, data);
+ await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState);
+ });
+
+ it('shows the category dropdown with a category selected', async () => {
+ renderComponent(`/discussions/${courseId}/category/category-1`);
+
+ // The current category should be visible on the page
+ const categoryDropdown = await screen.findByText('category-1');
+ // Since a category is selected a subcategory dropdown should also be visible with "show all" selected by default
+ const topicsDropdown = screen.queryByText('Show all');
+ // A show all button should show up that lists topics in the current category
+ expect(topicsDropdown).toBeInTheDocument();
+ // Other categories should not be visible.
+ expect(screen.queryByText('category-2')).not.toBeInTheDocument();
+
+ // Click on the category dropdown.
+ act(() => {
+ fireEvent.click(categoryDropdown);
+ });
+ // Now other categories should be visible in the dropdown.
+ expect(screen.queryByText('category-2')).toBeInTheDocument();
+ // There are three categories but this has a length of 4 since the selected category name appears twice.
+ expect(screen.queryAllByText('category-', { exact: false })).toHaveLength(4);
+
+ // Now click on the topics dropdown
+ act(() => {
+ fireEvent.click(topicsDropdown);
+ });
+ // Topics in the category should be visible.
+ expect(screen.queryAllByText('category-1-topic', { exact: false })).toHaveLength(4);
+ });
+
+ it('shows the category correct dropdown labels with a topic selected', async () => {
+ renderComponent(`/discussions/${courseId}/topics/category-2-topic-1`);
+
+ // Since a topic is selected, we have both a category and topic, so "show all shouldn't be visible"
+ expect(screen.queryByText('Show all')).not.toBeInTheDocument();
+ // The name of the category and topic should be visible.
+ expect(screen.queryByText('category-2')).toBeInTheDocument();
+ expect(screen.queryByText('category-2-topic 1')).toBeInTheDocument();
+ });
+});
diff --git a/src/discussions/navigation/breadcrumb-menu/messages.js b/src/discussions/navigation/breadcrumb-menu/messages.js
index ecfc254f..71da8eed 100644
--- a/src/discussions/navigation/breadcrumb-menu/messages.js
+++ b/src/discussions/navigation/breadcrumb-menu/messages.js
@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'Topics',
description: 'Topics from Breadcrumb Menu item',
},
+ showAll: {
+ id: 'discussions.navigation.breadcrumbMenu.showAll',
+ defaultMessage: 'Show all',
+ description: 'Option to show all items in a section of the breadcrumb',
+ },
});
export default messages;
diff --git a/src/discussions/navigation/index.js b/src/discussions/navigation/index.js
index 1532211d..9ea64da0 100644
--- a/src/discussions/navigation/index.js
+++ b/src/discussions/navigation/index.js
@@ -1,3 +1,4 @@
/* eslint-disable import/prefer-default-export */
export { default as BreadcrumbMenu } from './breadcrumb-menu/BreadcrumbMenu';
+export { default as LegacyBreadcrumbMenu } from './breadcrumb-menu/LegacyBreadcrumbMenu';
export { default as NavigationBar } from './navigation-bar/NavigationBar';
diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx
index 549784f5..b682f4c6 100644
--- a/src/discussions/posts/PostsView.jsx
+++ b/src/discussions/posts/PostsView.jsx
@@ -74,10 +74,10 @@ function PostsView({ showOwnPosts }) {
lastPinnedIdx = false;
// Add a spacing after the group of pinned posts
return (
- <>
+
- >
+
);
}
return (
);
diff --git a/src/discussions/topics/data/__factories__/topics.factory.js b/src/discussions/topics/data/__factories__/topics.factory.js
index 68a8eaaa..cfcfe40b 100644
--- a/src/discussions/topics/data/__factories__/topics.factory.js
+++ b/src/discussions/topics/data/__factories__/topics.factory.js
@@ -1,6 +1,23 @@
import { Factory } from 'rosie';
Factory.define('topic')
- .sequence('id', (idx) => `topic-${idx}`)
- .sequence('name', (idx) => `topic ${idx}`)
+ .option('topicPrefix', null, '')
+ .sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}topic-${idx}`)
+ .sequence('name', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}topic ${idx}`)
+ .attr('thread_counts', [], {
+ discussion: 0,
+ question: 0,
+ })
.attr('children', []);
+
+Factory.define('category')
+ .attr('id', [], null)
+ .sequence('name', (idx) => `category-${idx}`)
+ .attr('thread_counts', [], {
+ discussion: 0,
+ question: 0,
+ })
+ .attr('children', ['name'], (name) => {
+ Factory.reset('topic');
+ return Factory.buildList('topic', 4, null, { topicPrefix: `${name}-` });
+ });
diff --git a/src/discussions/topics/data/selectors.js b/src/discussions/topics/data/selectors.js
index 09047238..99c30ebd 100644
--- a/src/discussions/topics/data/selectors.js
+++ b/src/discussions/topics/data/selectors.js
@@ -5,7 +5,7 @@ export const selectTopicFilter = state => state.topics.filter.trim().toLowerCase
export const selectCategories = state => state.topics.categoryIds;
export const selectTopicsInCategory = (categoryId) => state => (
- state.topics.topicsInCategory[categoryId].map(id => state.topics.topics[id])
+ state.topics.topicsInCategory[categoryId]?.map(id => state.topics.topics[id]) || []
);
export const selectTopics = state => state.topics.topics;