diff --git a/src/components/ScrollThreshold.jsx b/src/components/ScrollThreshold.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/TinyMCEEditor.jsx b/src/components/TinyMCEEditor.jsx
index e4dc653b..f1937eac 100644
--- a/src/components/TinyMCEEditor.jsx
+++ b/src/components/TinyMCEEditor.jsx
@@ -34,6 +34,7 @@ import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.c
// eslint-disable-next-line import/no-unresolved
import contentUiCss from '!!raw-loader!tinymce/skins/ui/oxide/content.min.css';
+/* istanbul ignore next */
const setup = (editor) => {
editor.ui.registry.addButton('openedx_code', {
icon: 'sourcecode',
@@ -49,6 +50,7 @@ const setup = (editor) => {
});
};
+/* istanbul ignore next */
export default function TinyMCEEditor(props) {
// note that skin and content_css is disabled to avoid the normal
// loading process and is instead loaded as a string via content_style
diff --git a/src/data/selectors.js b/src/data/selectors.js
index e0faa3ca..3207deff 100644
--- a/src/data/selectors.js
+++ b/src/data/selectors.js
@@ -32,4 +32,10 @@ export const selectSequences = createSelector(
(chapterIds, blocks) => chapterIds?.flatMap(cId => blocks[cId].children.map(seqId => blocks[seqId])) || [],
);
+export const selectArchivedTopics = createSelector(
+ state => state.topics.topics,
+ state => state.topics.archivedIds || [],
+ (topics, ids) => ids.map(id => topics[id]),
+);
+
export const selectTopicIds = () => (state) => state.blocks.chapters;
diff --git a/src/discussions/topics/TopicsView.jsx b/src/discussions/topics/TopicsView.jsx
index ea382437..7c68a064 100644
--- a/src/discussions/topics/TopicsView.jsx
+++ b/src/discussions/topics/TopicsView.jsx
@@ -9,9 +9,10 @@ import { DiscussionContext } from '../common/context';
import { selectDiscussionProvider } from '../data/selectors';
import { selectCategories, selectNonCoursewareTopics, selectTopicFilter } from './data/selectors';
import { fetchCourseTopics } from './data/thunks';
+import ArchivedTopicGroup from './topic-group/ArchivedTopicGroup';
import LegacyTopicGroup from './topic-group/LegacyTopicGroup';
+import SequenceTopicGroup from './topic-group/SequenceTopicGroup';
import Topic from './topic-group/topic/Topic';
-import TopicGroup from './topic-group/TopicGroup';
import TopicSearchBar from './topic-search-bar/TopicSearchBar';
function CourseWideTopics() {
@@ -32,13 +33,19 @@ function CourseWideTopics() {
function CoursewareTopics() {
const sequences = useSelector(selectSequences);
- return sequences?.map(
- sequence => (
-
- ),
+
+ return (
+ <>
+ { sequences?.map(
+ sequence => (
+
+ ),
+ )}
+
+ >
);
}
diff --git a/src/discussions/topics/TopicsView.test.jsx b/src/discussions/topics/TopicsView.test.jsx
index 57292c1d..09c573c2 100644
--- a/src/discussions/topics/TopicsView.test.jsx
+++ b/src/discussions/topics/TopicsView.test.jsx
@@ -1,4 +1,6 @@
-import { fireEvent, render, screen } from '@testing-library/react';
+import {
+ fireEvent, render, screen, within,
+} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { IntlProvider } from 'react-intl';
import { MemoryRouter, Route } from 'react-router';
@@ -10,11 +12,12 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { getBlocksAPIResponse } from '../../data/__factories__';
import { blocksAPIURL } from '../../data/api';
-import { API_BASE_URL } from '../../data/constants';
+import { API_BASE_URL, DiscussionProvider } from '../../data/constants';
import { selectSequences } from '../../data/selectors';
import { fetchCourseBlocks } from '../../data/thunks';
import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils';
+import { DiscussionContext } from '../common/context';
import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors';
import { fetchCourseTopics } from './data/thunks';
import TopicsView from './TopicsView';
@@ -33,20 +36,22 @@ function renderComponent() {
render(
-
-
-
-
-
-
-
- {
- lastLocation = location;
- return null;
- }}
- />
-
+
+
+
+
+
+
+
+
+ {
+ lastLocation = location;
+ return null;
+ }}
+ />
+
+
,
);
@@ -95,9 +100,14 @@ describe('TopicsView', () => {
const blocksAPIResponse = getBlocksAPIResponse(true);
const ids = Object.values(blocksAPIResponse.blocks).filter(block => block.type === 'vertical')
.map(block => block.block_id);
+ const deletedIds = [
+ 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@deleted-vertical-1',
+ 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@deleted-vertical-2',
+ ];
const data = [
...Factory.buildList('topic.v2', 2, { usage_key: null }, { topicPrefix: 'ncw' }),
...ids.map(id => Factory.build('topic.v2', { id })),
+ ...deletedIds.map(id => Factory.build('topic.v2', { id, enabled_in_context: false }, { topicPrefix: 'archived ' })),
];
axiosMock
@@ -124,7 +134,7 @@ describe('TopicsView', () => {
});
});
- it('displays non-courseware in outside of a topic group', async () => {
+ it('displays non-courseware outside of a topic group', async () => {
await setupMockResponse();
renderComponent();
@@ -135,9 +145,25 @@ describe('TopicsView', () => {
});
const topicGroups = screen.queryAllByTestId('topic-group');
- expect(topicGroups).toHaveLength(categories.length);
+ // For the new provider there should be a section for archived topics
+ expect(topicGroups).toHaveLength(
+ provider === DiscussionProvider.LEGACY
+ ? categories.length
+ : categories.length + 1,
+ );
});
+ if (provider === DiscussionProvider.OPEN_EDX) {
+ it('displays archived topics', async () => {
+ await setupMockResponse();
+ renderComponent();
+ const archivedTopicGroup = screen.queryAllByTestId('topic-group').pop();
+ expect(archivedTopicGroup).toHaveTextContent(/archived/i);
+ const archivedTopicLinks = within(archivedTopicGroup).queryAllByRole('link');
+ expect(archivedTopicLinks).toHaveLength(2);
+ });
+ }
+
it('displays courseware topics', async () => {
await setupMockResponse();
renderComponent();
diff --git a/src/discussions/topics/data/slices.js b/src/discussions/topics/data/slices.js
index 1860577a..97707f88 100644
--- a/src/discussions/topics/data/slices.js
+++ b/src/discussions/topics/data/slices.js
@@ -11,6 +11,8 @@ const topicsSlice = createSlice({
categoryIds: [],
// List of all non-courseware topics
nonCoursewareIds: [],
+ // Topics that have been archived
+ archivedIds: [],
// Mapping of all topics in each category
topicsInCategory: {},
// Map of topics ids to topic data
@@ -27,6 +29,7 @@ const topicsSlice = createSlice({
state.topics = payload.topics;
state.nonCoursewareIds = payload.nonCoursewareIds;
state.categoryIds = payload.categoryIds;
+ state.archivedIds = payload.archivedIds;
state.topicsInCategory = payload.topicsInCategory;
},
fetchCourseTopicsFailed: (state) => {
diff --git a/src/discussions/topics/data/thunks.js b/src/discussions/topics/data/thunks.js
index 3c1e967a..d52568c4 100644
--- a/src/discussions/topics/data/thunks.js
+++ b/src/discussions/topics/data/thunks.js
@@ -29,14 +29,17 @@ function normaliseTopics(data) {
function normaliseTopicsV2(data) {
const nonCoursewareIds = [];
const topics = {};
+ const archivedIds = [];
data.forEach(topic => {
- if (topic.usageKey === null) {
+ if (!topic.enabledInContext) {
+ archivedIds.push(topic.id);
+ } else if (topic.usageKey === null) {
nonCoursewareIds.push(topic.id);
}
topics[topic.id] = topic;
});
return {
- topics, nonCoursewareIds,
+ topics, nonCoursewareIds, archivedIds,
};
}
diff --git a/src/discussions/topics/topic-search-bar/messages.js b/src/discussions/topics/messages.js
similarity index 85%
rename from src/discussions/topics/topic-search-bar/messages.js
rename to src/discussions/topics/messages.js
index 9c7185d2..c0c6d409 100644
--- a/src/discussions/topics/topic-search-bar/messages.js
+++ b/src/discussions/topics/messages.js
@@ -26,6 +26,11 @@ const messages = defineMessages({
defaultMessage: 'Find a topic',
description: 'Placeholder text in search bar',
},
+ archivedTopics: {
+ id: 'discussions.topics.archived.label',
+ defaultMessage: 'Archived',
+ description: 'Heading for displaying topics that are archived.',
+ },
});
export default messages;
diff --git a/src/discussions/topics/topic-group/ArchivedTopicGroup.jsx b/src/discussions/topics/topic-group/ArchivedTopicGroup.jsx
new file mode 100644
index 00000000..21b64a82
--- /dev/null
+++ b/src/discussions/topics/topic-group/ArchivedTopicGroup.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { useSelector } from 'react-redux';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import { selectArchivedTopics } from '../../../data/selectors';
+import messages from '../messages';
+import TopicGroupBase from './TopicGroupBase';
+
+function ArchivedTopicGroup({ intl }) {
+ const topics = useSelector(selectArchivedTopics);
+ return (
+
+ );
+}
+ArchivedTopicGroup.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(ArchivedTopicGroup);
diff --git a/src/discussions/topics/topic-group/LegacyTopicGroup.jsx b/src/discussions/topics/topic-group/LegacyTopicGroup.jsx
index 0e4daf7a..ca2a5067 100644
--- a/src/discussions/topics/topic-group/LegacyTopicGroup.jsx
+++ b/src/discussions/topics/topic-group/LegacyTopicGroup.jsx
@@ -2,56 +2,18 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
-import { useParams } from 'react-router';
-import { Link } from 'react-router-dom';
-import { Routes } from '../../../data/constants';
-import { discussionsPath } from '../../utils';
-import { selectTopicFilter, selectTopicsInCategory } from '../data/selectors';
-import Topic from './topic/Topic';
+import { selectTopicsInCategory } from '../data/selectors';
+import TopicGroupBase from './TopicGroupBase';
function LegacyTopicGroup({
id,
category,
}) {
- const { courseId } = useParams();
const topics = useSelector(selectTopicsInCategory(category));
- const filter = useSelector(selectTopicFilter);
- const matchesFilter = filter
- ? category?.toLowerCase()
- .includes(filter)
- : true;
- const topicElements = topics.filter(
- topic => (
- filter
- ? topic.name.toLowerCase()
- .includes(filter)
- : true
- ),
- )
- .map(topic => ());
- const hasFilteredSubtopics = (topicElements.length > 0);
- if (!matchesFilter && !hasFilteredSubtopics) {
- return null;
- }
return (
-
-
- {category}
-
- {topicElements}
-
+
);
}
diff --git a/src/discussions/topics/topic-group/SequenceTopicGroup.jsx b/src/discussions/topics/topic-group/SequenceTopicGroup.jsx
new file mode 100644
index 00000000..7c9f74a0
--- /dev/null
+++ b/src/discussions/topics/topic-group/SequenceTopicGroup.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { useSelector } from 'react-redux';
+
+import { selectTopicsById } from '../data/selectors';
+import TopicGroupBase from './TopicGroupBase';
+
+function SequenceTopicGroup({
+ sequence,
+}) {
+ const topicsIds = sequence.topics;
+ const topics = useSelector(selectTopicsById(topicsIds));
+
+ return (
+
+ );
+}
+
+SequenceTopicGroup.propTypes = {
+ sequence: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ displayName: PropTypes.string.isRequired,
+ topics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ }).isRequired,
+};
+
+export default SequenceTopicGroup;
diff --git a/src/discussions/topics/topic-group/TopicGroup.jsx b/src/discussions/topics/topic-group/TopicGroup.jsx
deleted file mode 100644
index 2bf8f49c..00000000
--- a/src/discussions/topics/topic-group/TopicGroup.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { useSelector } from 'react-redux';
-import { useParams } from 'react-router';
-import { Link } from 'react-router-dom';
-
-import { Routes } from '../../../data/constants';
-import { discussionsPath } from '../../utils';
-import { selectTopicFilter, selectTopicsById } from '../data/selectors';
-import Topic from './topic/Topic';
-
-function TopicGroup({
- sequence,
-}) {
- const { courseId } = useParams();
- const topicsIds = sequence.topics;
- const topics = useSelector(selectTopicsById(topicsIds));
- const filter = useSelector(selectTopicFilter);
- const hasTopics = topics.length > 0;
- const matchesFilter = filter
- ? sequence.displayName?.toLowerCase()
- .includes(filter)
- : true;
- const topicElements = topics.filter(
- topic => (
- filter
- ? topic.name.toLowerCase()
- .includes(filter)
- : true
- ),
- )
- .map(topic => ());
- const hasFilteredSubtopics = (topicElements.length > 0);
- if (!hasTopics || (!matchesFilter && !hasFilteredSubtopics)) {
- return null;
- }
-
- return (
-
-
- {sequence.displayName}
-
- {topicElements}
-
- );
-}
-
-TopicGroup.propTypes = {
- sequence: PropTypes.shape({
- id: PropTypes.string.isRequired,
- displayName: PropTypes.string.isRequired,
- topics: PropTypes.arrayOf(PropTypes.string).isRequired,
- }).isRequired,
-};
-
-export default TopicGroup;
diff --git a/src/discussions/topics/topic-group/TopicGroupBase.jsx b/src/discussions/topics/topic-group/TopicGroupBase.jsx
new file mode 100644
index 00000000..61eda60a
--- /dev/null
+++ b/src/discussions/topics/topic-group/TopicGroupBase.jsx
@@ -0,0 +1,77 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+
+import { useSelector } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { Routes } from '../../../data/constants';
+import { DiscussionContext } from '../../common/context';
+import { discussionsPath } from '../../utils';
+import { selectTopicFilter } from '../data/selectors';
+import Topic, { topicShape } from './topic/Topic';
+
+function TopicGroupBase({
+ groupId,
+ groupTitle,
+ linkToGroup,
+ topics,
+}) {
+ const { courseId } = useContext(DiscussionContext);
+ const filter = useSelector(selectTopicFilter);
+ const hasTopics = topics.length > 0;
+ const matchesFilter = filter
+ ? groupTitle?.toLowerCase()
+ .includes(filter)
+ : true;
+ const topicElements = topics.filter(
+ topic => (
+ filter
+ ? topic.name.toLowerCase()
+ .includes(filter)
+ : true
+ ),
+ )
+ .map(topic => ());
+ const hasFilteredSubtopics = (topicElements.length > 0);
+ if (!hasTopics || (!matchesFilter && !hasFilteredSubtopics)) {
+ return null;
+ }
+ return (
+
+ {linkToGroup
+ ? (
+
+ {groupTitle}
+
+ ) : (
+
+ {groupTitle}
+
+ )}
+ {topicElements}
+
+ );
+}
+
+TopicGroupBase.propTypes = {
+ groupId: PropTypes.string.isRequired,
+ groupTitle: PropTypes.string.isRequired,
+ topics: PropTypes.arrayOf(topicShape).isRequired,
+ linkToGroup: PropTypes.bool,
+};
+
+TopicGroupBase.defaultProps = {
+ linkToGroup: true,
+};
+
+export default TopicGroupBase;
diff --git a/src/discussions/topics/topic-search-bar/TopicSearchBar.jsx b/src/discussions/topics/topic-search-bar/TopicSearchBar.jsx
index da403c6d..080066ec 100644
--- a/src/discussions/topics/topic-search-bar/TopicSearchBar.jsx
+++ b/src/discussions/topics/topic-search-bar/TopicSearchBar.jsx
@@ -6,7 +6,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { SearchField } from '@edx/paragon';
import { setFilter } from '../data';
-import messages from './messages';
+import messages from '../messages';
function TopicSearchBar({ intl }) {
const dispatch = useDispatch();