feat: display archived topics is post editor and topics list (#397)
* fix: display subsection name when there is no units in subsection * feat: display archived topics is post editor and topics list * fix: posts filter are affecting topic posts result
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import first from 'lodash/first';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -13,7 +12,8 @@ import { selectTopicThreads } from '../posts/data/selectors';
|
||||
import PostsList from '../posts/PostsList';
|
||||
import { discussionsPath, handleKeyDown } from '../utils';
|
||||
import {
|
||||
selectLoadingStatus, selectNonCoursewareTopics, selectSubsectionUnits, selectUnits,
|
||||
selectArchivedTopic, selectLoadingStatus, selectNonCoursewareTopics,
|
||||
selectSubsection, selectSubsectionUnits, selectUnits,
|
||||
} from './data/selectors';
|
||||
import { BackButton, NoResults } from './components';
|
||||
import messages from './messages';
|
||||
@@ -25,8 +25,10 @@ function TopicPostsView({ intl }) {
|
||||
const topicsLoadingStatus = useSelector(selectLoadingStatus);
|
||||
const posts = useSelector(selectTopicThreads([topicId]));
|
||||
const selectedSubsectionUnits = useSelector(selectSubsectionUnits(category));
|
||||
const selectedSubsection = useSelector(selectSubsection(category));
|
||||
const selectedUnit = useSelector(selectUnits)?.find(unit => unit.id === topicId);
|
||||
const selectedNonCoursewareTopic = useSelector(selectNonCoursewareTopics)?.find(topic => topic.id === topicId);
|
||||
const selectedArchivedTopic = useSelector(selectArchivedTopic(topicId));
|
||||
|
||||
const backButtonPath = () => {
|
||||
const path = selectedUnit ? Routes.TOPICS.CATEGORY : Routes.TOPICS.ALL;
|
||||
@@ -39,12 +41,13 @@ function TopicPostsView({ intl }) {
|
||||
{topicId ? (
|
||||
<BackButton
|
||||
path={backButtonPath()}
|
||||
title={selectedUnit?.name || selectedNonCoursewareTopic?.name || intl.formatMessage(messages.unnamedTopic)}
|
||||
title={selectedUnit?.name || selectedNonCoursewareTopic?.name || selectedArchivedTopic?.name
|
||||
|| intl.formatMessage(messages.unnamedTopic)}
|
||||
/>
|
||||
) : (
|
||||
<BackButton
|
||||
path={discussionsPath(Routes.TOPICS.ALL, { courseId })(location)}
|
||||
title={first(selectedSubsectionUnits)?.parentTitle || intl.formatMessage(messages.unnamedSubsection)}
|
||||
title={selectedSubsection?.displayName || intl.formatMessage(messages.unnamedSubsection)}
|
||||
/>
|
||||
)}
|
||||
<div className="border-bottom border-light-400" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Spinner } from '@edx/paragon';
|
||||
@@ -8,22 +9,23 @@ import { Spinner } from '@edx/paragon';
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectDiscussionProvider } from '../data/selectors';
|
||||
import { selectAreThreadsFiltered, selectDiscussionProvider } from '../data/selectors';
|
||||
import { clearFilter, clearSort } from '../posts/data/slices';
|
||||
import NoResults from '../posts/NoResults';
|
||||
import { handleKeyDown } from '../utils';
|
||||
import {
|
||||
selectCoursewareTopics,
|
||||
selectFilteredTopics, selectLoadingStatus,
|
||||
selectArchivedTopics, selectCoursewareTopics, selectFilteredTopics, selectLoadingStatus,
|
||||
selectNonCoursewareTopics, selectTopicFilter,
|
||||
} from './data/selectors';
|
||||
import { setFilter } from './data/slices';
|
||||
import { fetchCourseTopicsV3 } from './data/thunks';
|
||||
import { SectionBaseGroup, Topic } from './topic';
|
||||
import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic';
|
||||
|
||||
function TopicsList() {
|
||||
const loadingStatus = useSelector(selectLoadingStatus);
|
||||
const coursewareTopics = useSelector(selectCoursewareTopics);
|
||||
const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
|
||||
const archivedTopics = useSelector(selectArchivedTopics);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,6 +45,12 @@ function TopicsList() {
|
||||
showDivider={(coursewareTopics.length - 1) !== index}
|
||||
/>
|
||||
))}
|
||||
{!isEmpty(archivedTopics) && (
|
||||
<ArchivedBaseGroup
|
||||
archivedTopics={archivedTopics}
|
||||
showDivider={(!isEmpty(nonCoursewareTopics) || !isEmpty(coursewareTopics))}
|
||||
/>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS && (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
@@ -59,6 +67,7 @@ function TopicsView() {
|
||||
const topicFilter = useSelector(selectTopicFilter);
|
||||
const filteredTopics = useSelector(selectFilteredTopics);
|
||||
const loadingStatus = useSelector(selectLoadingStatus);
|
||||
const isPostsFiltered = useSelector(selectAreThreadsFiltered);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider) {
|
||||
@@ -66,6 +75,13 @@ function TopicsView() {
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPostsFiltered) {
|
||||
dispatch(clearFilter());
|
||||
dispatch(clearSort());
|
||||
}
|
||||
}, [isPostsFiltered]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column h-100" data-testid="inContext-topics-view">
|
||||
{topicFilter && (
|
||||
|
||||
@@ -18,6 +18,22 @@ export const selectSubsectionUnits = subsectionId => state => state.inContextTop
|
||||
unit => unit.parentId === subsectionId,
|
||||
);
|
||||
|
||||
export const selectSubsection = category => createSelector(
|
||||
selectCoursewareTopics,
|
||||
(coursewareTopics) => (
|
||||
coursewareTopics?.map((topic) => topic?.children)?.flat()?.find((topic) => topic.id === category)
|
||||
),
|
||||
);
|
||||
|
||||
export const selectArchivedTopics = state => state.inContextTopics.archivedTopics;
|
||||
|
||||
export const selectArchivedTopic = topic => createSelector(
|
||||
selectArchivedTopics,
|
||||
(archivedTopics) => (
|
||||
archivedTopics?.find((archivedTopic) => archivedTopic.id === topic)
|
||||
),
|
||||
);
|
||||
|
||||
export const selectLoadingStatus = state => state.inContextTopics.status;
|
||||
|
||||
export const selectFilteredTopics = createSelector(
|
||||
|
||||
@@ -12,6 +12,7 @@ const topicsSlice = createSlice({
|
||||
nonCoursewareTopics: [],
|
||||
nonCoursewareIds: [],
|
||||
units: [],
|
||||
archivedTopics: [],
|
||||
filter: '',
|
||||
},
|
||||
reducers: {
|
||||
@@ -25,6 +26,7 @@ const topicsSlice = createSlice({
|
||||
state.nonCoursewareTopics = payload.nonCoursewareTopics;
|
||||
state.nonCoursewareIds = payload.nonCoursewareIds;
|
||||
state.units = payload.units;
|
||||
state.archivedTopics = payload.archivedTopics;
|
||||
},
|
||||
fetchCourseTopicsFailed: (state) => {
|
||||
state.status = RequestStatus.FAILED;
|
||||
|
||||
@@ -29,8 +29,15 @@ function normalizeTopicsV3(topics) {
|
||||
return arrayOfUnits;
|
||||
}, []);
|
||||
|
||||
const archivedTopics = reduce(topics, (arrayOfArchivedTopics, topic) => {
|
||||
if (topic.id === 'archived') {
|
||||
return topic.children;
|
||||
}
|
||||
return arrayOfArchivedTopics;
|
||||
}, []);
|
||||
|
||||
const coursewareTopics = topics.filter((topic) => topic.courseware);
|
||||
const nonCoursewareTopics = topics.filter((topic) => !topic.courseware);
|
||||
const nonCoursewareTopics = topics.filter((topic) => !topic.courseware && topic.enabledInContext);
|
||||
const nonCoursewareIds = nonCoursewareTopics?.map((topic) => topic.id);
|
||||
|
||||
return {
|
||||
@@ -39,6 +46,7 @@ function normalizeTopicsV3(topics) {
|
||||
coursewareTopics,
|
||||
nonCoursewareTopics,
|
||||
nonCoursewareIds,
|
||||
archivedTopics,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Nothing here yet',
|
||||
description: 'Helping Text to display if nothing here yet',
|
||||
},
|
||||
archivedTopics: {
|
||||
id: 'discussions.topics.archived.label',
|
||||
defaultMessage: 'Archived',
|
||||
description: 'Heading for displaying topics that are archived.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import Topic, { topicShape } from './Topic';
|
||||
|
||||
function ArchivedBaseGroup({
|
||||
archivedTopics,
|
||||
showDivider,
|
||||
intl,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{showDivider && (
|
||||
<>
|
||||
<div className="divider border-top border-light-500" />
|
||||
<div className="divider pt-1 bg-light-300" />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="discussion-topic-group d-flex flex-column text-primary-500"
|
||||
data-testid="archived-group"
|
||||
>
|
||||
<div className="pt-3 px-4 font-weight-bold">{intl.formatMessage(messages.archivedTopics)}</div>
|
||||
{archivedTopics?.map((topic, index) => (
|
||||
<Topic
|
||||
key={topic.id}
|
||||
topic={topic}
|
||||
showDivider={(archivedTopics.length - 1) !== index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ArchivedBaseGroup.propTypes = {
|
||||
archivedTopics: PropTypes.arrayOf(topicShape).isRequired,
|
||||
showDivider: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
ArchivedBaseGroup.defaultProps = {
|
||||
showDivider: false,
|
||||
};
|
||||
export default injectIntl(ArchivedBaseGroup);
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as ArchivedBaseGroup } from './ArchivedBaseGroup';
|
||||
export { default as SectionBaseGroup } from './SectionBaseGroup';
|
||||
export { default as Topic } from './Topic';
|
||||
|
||||
@@ -217,6 +217,19 @@ const threadsSlice = createSlice({
|
||||
clearRedirect: (state) => {
|
||||
state.redirectToThread = null;
|
||||
},
|
||||
clearFilter: (state) => {
|
||||
state.filters = {
|
||||
status: PostsStatusFilter.ALL,
|
||||
postType: ThreadType.ALL,
|
||||
cohort: '',
|
||||
search: '',
|
||||
};
|
||||
state.pages = [];
|
||||
},
|
||||
clearSort: (state) => {
|
||||
state.sortedBy = ThreadOrdering.BY_LAST_ACTIVITY;
|
||||
state.pages = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -253,6 +266,8 @@ export const {
|
||||
hidePostEditor,
|
||||
clearRedirect,
|
||||
clearPostsPages,
|
||||
clearFilter,
|
||||
clearSort,
|
||||
} = threadsSlice.actions;
|
||||
|
||||
export const threadsReducer = threadsSlice.reducer;
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from '../../data/selectors';
|
||||
import { EmptyPage } from '../../empty-posts';
|
||||
import {
|
||||
selectArchivedTopics,
|
||||
selectCoursewareTopics as inContextCourseware,
|
||||
selectNonCoursewareIds as inContextCoursewareIds,
|
||||
selectNonCoursewareTopics as inContextNonCourseware,
|
||||
@@ -114,6 +115,7 @@ function PostEditor({
|
||||
const { allowAnonymous, allowAnonymousToPeers } = useSelector(selectAnonymousPostingConfig);
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const archivedTopics = useSelector(selectArchivedTopics);
|
||||
|
||||
const canDisplayEditReason = (reasonCodesEnabled && editExisting
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
@@ -317,7 +319,8 @@ function PostEditor({
|
||||
</option>
|
||||
))}
|
||||
{enableInContext ? (
|
||||
coursewareTopics?.map(section => (
|
||||
<>
|
||||
{coursewareTopics?.map(section => (
|
||||
section?.children?.map(subsection => (
|
||||
<optgroup
|
||||
label={handleInContextSelectLabel(section, subsection)}
|
||||
@@ -330,7 +333,17 @@ function PostEditor({
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
))
|
||||
))}
|
||||
{(userIsStaff || userIsGroupTa || userHasModerationPrivileges) && (
|
||||
<optgroup label={intl.formatMessage(messages.archivedTopics)}>
|
||||
{archivedTopics.map(topic => (
|
||||
<option key={topic.id} value={topic.id}>
|
||||
{topic.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
coursewareTopics.map(categoryObj => (
|
||||
<optgroup
|
||||
|
||||
@@ -136,6 +136,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Thread not found',
|
||||
description: 'message to show on screen if the request thread is not found in course',
|
||||
},
|
||||
archivedTopics: {
|
||||
id: 'discussions.topics.archived.label',
|
||||
defaultMessage: 'Archived',
|
||||
description: 'Heading for displaying topics that are archived.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user