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:
Awais Ansari
2023-01-04 14:01:56 +05:00
committed by GitHub
parent 9c576ff3dc
commit 58e724d724
11 changed files with 143 additions and 11 deletions

View File

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

View File

@@ -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 && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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