diff --git a/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx b/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx index 9709e45c..1c897e4c 100644 --- a/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx +++ b/src/discussions/navigation/breadcrumb-menu/BreadcrumbMenu.jsx @@ -44,7 +44,7 @@ function BreadcrumbMenu() { }); return ( -
+
+
{isNonCoursewareTopic ? ( - { - searchString && dispatch(setSearchQuery(''))} /> - } + {searchString && ( + dispatch(setSearchQuery(''))} + /> + )}
handleKeyDown(e)}> diff --git a/src/discussions/posts/post/PostLink.jsx b/src/discussions/posts/post/PostLink.jsx index d83f4553..472b152b 100644 --- a/src/discussions/posts/post/PostLink.jsx +++ b/src/discussions/posts/post/PostLink.jsx @@ -63,7 +63,7 @@ function PostLink({ style={{ lineHeight: '21px' }} aria-current={isSelected(post.id) ? 'page' : undefined} role="option" - tabindex={(isSelected(post.id) || idx === 0) ? 0 : -1} + tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1} >
(filter - ? item.name.toLowerCase() - .includes(filter) - : true - ), - ) - .map(topic => ( - + const filteredNonCoursewareTopics = nonCoursewareTopics.filter(item => (filter + ? item.name.toLowerCase().includes(filter) + : true + )); + + return (nonCoursewareTopics && category === undefined) + && filteredNonCoursewareTopics.map((topic, index) => ( + )); } @@ -76,6 +80,20 @@ function TopicsView() { const { courseId } = useContext(DiscussionContext); const dispatch = useDispatch(); + const handleKeyDown = (event) => { + const { key } = event; + if (key !== 'ArrowDown' && key !== 'ArrowUp') { return; } + const option = event.target; + + let selectedOption; + if (key === 'ArrowDown') { selectedOption = option.nextElementSibling; } + if (key === 'ArrowUp') { selectedOption = option.previousElementSibling; } + + if (selectedOption) { + selectedOption.focus(); + } + }; + useEffect(() => { // Don't load till the provider information is available if (provider) { @@ -89,19 +107,19 @@ function TopicsView() { }, [topicFilter]); return ( -
-
- { - topicFilter && dispatch(setFilter(''))} /> - } -
- - {provider === DiscussionProvider.OPEN_EDX && } - {provider === DiscussionProvider.LEGACY && } -
+
+ {topicFilter && ( + dispatch(setFilter(''))} + /> + )} +
handleKeyDown(e)}> + + {provider === DiscussionProvider.OPEN_EDX && } + {provider === DiscussionProvider.LEGACY && }
{ filteredTopicsCount === 0 diff --git a/src/discussions/topics/TopicsView.test.jsx b/src/discussions/topics/TopicsView.test.jsx index 09c573c2..430d795d 100644 --- a/src/discussions/topics/TopicsView.test.jsx +++ b/src/discussions/topics/TopicsView.test.jsx @@ -159,7 +159,7 @@ describe('TopicsView', () => { renderComponent(); const archivedTopicGroup = screen.queryAllByTestId('topic-group').pop(); expect(archivedTopicGroup).toHaveTextContent(/archived/i); - const archivedTopicLinks = within(archivedTopicGroup).queryAllByRole('link'); + const archivedTopicLinks = within(archivedTopicGroup).queryAllByRole('option'); expect(archivedTopicLinks).toHaveLength(2); }); } diff --git a/src/discussions/topics/messages.js b/src/discussions/topics/messages.js index 6645e818..6586cc60 100644 --- a/src/discussions/topics/messages.js +++ b/src/discussions/topics/messages.js @@ -1,6 +1,34 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + discussions: { + id: 'discussions.topics.discussions', + defaultMessage: `{count, plural, + =0 {Discussion} + one {# Discussion} + other {# Discussions} + }`, + description: 'Display tooltip text used to indicate how many posts type are discussion', + }, + questions: { + id: 'discussions.topics.questions', + defaultMessage: `{count, plural, + =0 {Question} + one {# Question} + other {# Questions} + }`, + description: 'Display tooltip text used to indicate how many posts type are questions', + }, + reported: { + id: 'discussions.topics.reported', + defaultMessage: '{reported} reported', + description: 'Display tooltip text used to indicate how many posts are reported', + }, + previouslyReported: { + id: 'discussions.topics.previouslyReported', + defaultMessage: '{previouslyReported} previously reported', + description: 'Display tooltip text used to indicate how many posts are previously reported', + }, sortedBy: { id: 'discussions.topics.sort.message', defaultMessage: 'Sorted by {sortBy}', diff --git a/src/discussions/topics/topic-group/TopicGroupBase.jsx b/src/discussions/topics/topic-group/TopicGroupBase.jsx index 9fae7223..337dcad6 100644 --- a/src/discussions/topics/topic-group/TopicGroupBase.jsx +++ b/src/discussions/topics/topic-group/TopicGroupBase.jsx @@ -24,45 +24,50 @@ function TopicGroupBase({ const filter = useSelector(selectTopicFilter); const hasTopics = topics.length > 0; const matchesFilter = filter - ? groupTitle?.toLowerCase() - .includes(filter) + ? groupTitle?.toLowerCase().includes(filter) : true; - const topicElements = topics.filter( - topic => ( - filter - ? (topic.name.toLowerCase() - .includes(filter) || matchesFilter) - : true + + const filteredTopicElements = topics.filter( + topic => (filter + ? (topic.name.toLowerCase().includes(filter) || matchesFilter) + : true ), - ) - .map(topic => ()); - const hasFilteredSubtopics = (topicElements.length > 0); + ); + + const hasFilteredSubtopics = (filteredTopicElements.length > 0); if (!hasTopics || (!matchesFilter && !hasFilteredSubtopics)) { return null; } return (
- {linkToGroup && groupId - ? ( - - {groupTitle} - - ) : ( - - {groupTitle || intl.formatMessage(messages.unnamedTopicCategories)} - - )} - {topicElements} +
+ {linkToGroup && groupId + ? ( + + {groupTitle} + + ) : ( + groupTitle || intl.formatMessage(messages.unnamedTopicCategories) + )} +
+ {filteredTopicElements.map((topic, index) => ( + + ))}
); } diff --git a/src/discussions/topics/topic-group/topic/Topic.jsx b/src/discussions/topics/topic-group/topic/Topic.jsx index 08c0b4a2..57ab1ce8 100644 --- a/src/discussions/topics/topic-group/topic/Topic.jsx +++ b/src/discussions/topics/topic-group/topic/Topic.jsx @@ -2,65 +2,120 @@ import React 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 { Icon } from '@edx/paragon'; -import { Error as ErrorIcon, Help, PostOutline } from '@edx/paragon/icons'; +import { injectIntl, intlShape } 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 { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../../data/selectors'; import { discussionsPath } from '../../../utils'; +import messages from '../../messages'; -function Topic({ topic }) { +function Topic({ + topic, + showDivider, + index, + intl, +}) { const { courseId } = useParams(); const topicUrl = discussionsPath(Routes.TOPICS.TOPIC, { courseId, topicId: topic.id, }); - const icons = [ - { - key: 'discussions', - icon: PostOutline, - count: topic.threadCounts?.discussion || 0, - }, - { - key: 'questions', - icon: Help, - count: topic.threadCounts?.question || 0, - }, - ]; + const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); + const userIsGroupTa = useSelector(selectUserIsGroupTa); + const { inactiveFlags, activeFlags } = topic; + const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa); + const isSelected = (id) => window.location.pathname.includes(id); + return ( isSelected(topic.id)} + aria-current={isSelected(topic.id) ? 'page' : undefined} + role="option" + tabIndex={(isSelected(topic.id) || index === 0) ? 0 : -1} > -
- {topic.name} -
-
- { - icons.map(({ - key, - icon, - count, - }) => ( -
- - {/* Reserve some space for larger counts */} - - {count} - +
+
+
+
+ {topic.name}
- )) - } - {topic?.flags && ( -
- - {topic.flags}
- )} +
+ +
+ {intl.formatMessage(messages.discussions, { + count: topic.threadCounts?.discussion || 0, + })} +
+ + )} + > +
+ + {topic.threadCounts?.discussion || 0} +
+
+ +
+ {intl.formatMessage(messages.questions, { + count: topic.threadCounts?.question || 0, + })} +
+ + )} + > +
+ + {topic.threadCounts?.question || 0} +
+
+ {Boolean(canSeeReportedStats) && ( + +
+ {Boolean(activeFlags) && ( + + {intl.formatMessage(messages.reported, { reported: activeFlags })} + + )} + {Boolean(inactiveFlags) && ( + + {intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })} + + )} +
+ + )} + > +
+ + {activeFlags}{Boolean(inactiveFlags) && `/${inactiveFlags}`} +
+
+ )} +
+
+ {!showDivider &&
} ); } @@ -73,7 +128,15 @@ export const topicShape = PropTypes.shape({ flags: PropTypes.number, }); Topic.propTypes = { + intl: intlShape.isRequired, topic: topicShape.isRequired, + showDivider: PropTypes.bool, + index: PropTypes.number, }; -export default Topic; +Topic.defaultProps = { + showDivider: true, + index: -1, +}; + +export default injectIntl(Topic); diff --git a/src/index.scss b/src/index.scss index 485297d4..308eb0ae 100755 --- a/src/index.scss +++ b/src/index.scss @@ -232,3 +232,11 @@ header { position: sticky; top: 0; } + +.breadcrumb-menu { + z-index: 1; +} + +.discussion-topic-group:last-of-type .divider{ + display: none; +}