From bf953354a1b9f2c66aa600108c1d4ce6021e70f8 Mon Sep 17 00:00:00 2001 From: Muhammad Adeel Tajamul <77053848+muhammadadeeltajamul@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:31:20 +0500 Subject: [PATCH] feat: update search in discussions mfe (#207) * feat: update search in discussions mfe * fix: updated code design * fix: searchbar clear and code refactor * fix: replaced existing search box * feat: added search info bar * fix: resolved failing topics test * fix: resolved intl issue * fix: resolved il8n extract check * fix: labels moved to il8n message files Co-authored-by: adeel.tajamul Co-authored-by: Awais Ansari --- src/components/Search.jsx | 50 +++++++++++++++++++ src/components/SearchInfo.jsx | 40 +++++++++++++++ src/components/index.js | 1 + src/discussions/posts/PostsView.jsx | 10 +++- .../posts/post-actions-bar/PostActionsBar.jsx | 27 +++------- .../posts/post-actions-bar/messages.js | 22 +++++++- src/discussions/topics/TopicsView.jsx | 17 ++++++- src/discussions/topics/data/slices.js | 7 +++ .../topics/topic-group/LegacyTopicGroup.jsx | 1 - .../topics/topic-group/TopicGroupBase.jsx | 4 +- src/discussions/topics/utils.js | 34 +++++++++++++ 11 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 src/components/Search.jsx create mode 100644 src/components/SearchInfo.jsx create mode 100644 src/discussions/topics/utils.js diff --git a/src/components/Search.jsx b/src/components/Search.jsx new file mode 100644 index 00000000..308f4bd4 --- /dev/null +++ b/src/components/Search.jsx @@ -0,0 +1,50 @@ +import React, { useContext, useEffect } from 'react'; + +import camelCase from 'lodash/camelCase'; +import { useDispatch, useSelector } from 'react-redux'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { SearchField } from '@edx/paragon'; + +import { DiscussionContext } from '../discussions/common/context'; +import { setSearchQuery } from '../discussions/posts/data'; +import postsMessages from '../discussions/posts/post-actions-bar/messages'; +import { setFilter as setTopicFilter } from '../discussions/topics/data/slices'; + +function Search({ intl }) { + const dispatch = useDispatch(); + const { page } = useContext(DiscussionContext); + const postSearch = useSelector(({ threads }) => threads.filters.search); + const topicSearch = useSelector(({ topics }) => topics.filter); + const isPostSearch = ['posts', 'my-posts', 'learners'].includes(page); + + const onClear = () => { + dispatch(setSearchQuery('')); + dispatch(setTopicFilter('')); + }; + + const onSubmit = (query) => { + if (isPostSearch) { + dispatch(setSearchQuery(query)); + } else if (page === 'topics') { + dispatch(setTopicFilter(query)); + } + }; + + useEffect(() => onClear(), [page]); + return ( + + ); +} + +Search.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(Search); diff --git a/src/components/SearchInfo.jsx b/src/components/SearchInfo.jsx new file mode 100644 index 00000000..064e8c1b --- /dev/null +++ b/src/components/SearchInfo.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Button, Icon } from '@edx/paragon'; +import { Search } from '@edx/paragon/icons'; + +import messages from '../discussions/posts/post-actions-bar/messages'; + +function SearchInfo({ + intl, + count, + text, + onClear, +}) { + return ( +
+ + + +
+ ); +} + +SearchInfo.propTypes = { + intl: intlShape.isRequired, + count: PropTypes.number.isRequired, + text: PropTypes.string.isRequired, + onClear: PropTypes.func, +}; + +SearchInfo.defaultProps = { + onClear: () => {}, +}; + +export default injectIntl(SearchInfo); diff --git a/src/components/index.js b/src/components/index.js index 7e2e4030..182f3620 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,2 +1,3 @@ export { default as PostActionsBar } from '../discussions/posts/post-actions-bar/PostActionsBar'; +export { default as Search } from './Search'; export { default as TinyMCEEditor } from './TinyMCEEditor'; diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx index 7224ec1f..8213543a 100644 --- a/src/discussions/posts/PostsView.jsx +++ b/src/discussions/posts/PostsView.jsx @@ -1,14 +1,16 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import SearchInfo from '../../components/SearchInfo'; import { selectTopicsUnderCategory } from '../../data/selectors'; import { DiscussionContext } from '../common/context'; import { selectAllThreads, selectTopicThreads, } from './data/selectors'; +import { setSearchQuery } from './data/slices'; import PostFilterBar from './post-filter-bar/PostFilterBar'; import PostsList from './PostsList'; @@ -42,6 +44,9 @@ function PostsView() { category, page, } = useContext(DiscussionContext); + const dispatch = useDispatch(); + const searchString = useSelector(({ threads }) => threads.filters.search); + const resultsFound = useSelector(({ threads }) => threads.totalThreads); let postsListComponent; const showOwnPosts = page === 'my-posts'; @@ -56,6 +61,9 @@ function PostsView() { return (
+ { + searchString && dispatch(setSearchQuery(''))} /> + }
{postsListComponent} diff --git a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx index d6787698..3d40ce0e 100644 --- a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx +++ b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx @@ -1,20 +1,17 @@ -import React, { useContext } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router'; -import { useLocation } from 'react-router-dom'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { - Button, Icon, IconButton, SearchField, + Button, Icon, IconButton, } from '@edx/paragon'; import { Close } from '@edx/paragon/icons'; -import { Routes } from '../../../data/constants'; -import { DiscussionContext } from '../../common/context'; -import { discussionsPath, postMessageToParent } from '../../utils'; -import { setSearchQuery, showPostEditor } from '../data'; +import Search from '../../../components/Search'; +import { postMessageToParent } from '../../utils'; +import { showPostEditor } from '../data'; import messages from './messages'; import './actionBar.scss'; @@ -24,9 +21,6 @@ function PostActionsBar({ inContext, }) { const dispatch = useDispatch(); - const { courseId } = useContext(DiscussionContext); - const location = useLocation(); - const history = useHistory(); const handleCloseInContext = () => { postMessageToParent('learning.events.sidebar.close'); }; @@ -34,16 +28,7 @@ function PostActionsBar({
{!inContext && ( <> - dispatch(setSearchQuery(''))} - onSubmit={(value) => { - dispatch(setSearchQuery(value)); - history.push(discussionsPath(Routes.POSTS.ALL_POSTS, { courseId })(location)); - }} - placeholder={intl.formatMessage(messages.searchAllPosts)} - inputProps={{ className: 'small-font' }} - /> -
+ )} {inContext && ( diff --git a/src/discussions/posts/post-actions-bar/messages.js b/src/discussions/posts/post-actions-bar/messages.js index 070e374b..20c59412 100644 --- a/src/discussions/posts/post-actions-bar/messages.js +++ b/src/discussions/posts/post-actions-bar/messages.js @@ -6,10 +6,30 @@ const messages = defineMessages({ defaultMessage: 'Discussions', }, searchAllPosts: { - id: 'discussions.posts.actionBar.search', + id: 'discussions.posts.actionBar.searchAllPosts', defaultMessage: 'Search all posts', description: 'Placeholder text in search box', }, + search: { + id: 'discussions.posts.actionBar.search', + defaultMessage: `{page, select, + topics {Find a topic} + posts {Search all posts} + learners {Search learner} + myPosts {Search all posts} + }`, + description: 'Placeholder text in search box', + }, + searchInfo: { + id: 'discussions.actionBar.searchInfo', + defaultMessage: 'Showing {count} results for "{text}"', + description: 'Message displayed when user performs a search', + }, + clearSearch: { + id: 'discussions.actionBar.clearSearch', + defaultMessage: 'Clear', + description: 'Button to clear search', + }, addAPost: { id: 'discussion.posts.actionBar.add', defaultMessage: 'Add a post', diff --git a/src/discussions/topics/TopicsView.jsx b/src/discussions/topics/TopicsView.jsx index 7c68a064..44a8b24e 100644 --- a/src/discussions/topics/TopicsView.jsx +++ b/src/discussions/topics/TopicsView.jsx @@ -3,17 +3,19 @@ import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router'; +import SearchInfo from '../../components/SearchInfo'; import { DiscussionProvider } from '../../data/constants'; import { selectSequences } from '../../data/selectors'; import { DiscussionContext } from '../common/context'; import { selectDiscussionProvider } from '../data/selectors'; import { selectCategories, selectNonCoursewareTopics, selectTopicFilter } from './data/selectors'; +import { setFilter, setTopicsCount } from './data/slices'; 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 TopicSearchBar from './topic-search-bar/TopicSearchBar'; +import countFilteredTopics from './utils'; function CourseWideTopics() { const { category } = useParams(); @@ -66,8 +68,12 @@ function LegacyCoursewareTopics() { function TopicsView() { const provider = useSelector(selectDiscussionProvider); + const topicFilter = useSelector(selectTopicFilter); + const topicsSelector = useSelector(({ topics }) => topics); + const filteredTopicsCount = useSelector(({ topics }) => topics.results.count); const { courseId } = useContext(DiscussionContext); const dispatch = useDispatch(); + useEffect(() => { // Don't load till the provider information is available if (provider) { @@ -75,12 +81,19 @@ function TopicsView() { } }, [provider]); + useEffect(() => { + const count = countFilteredTopics(topicsSelector, provider); + dispatch(setTopicsCount(count)); + }, [topicFilter]); + return (
- + { + topicFilter && dispatch(setFilter(''))} /> + }
{provider === DiscussionProvider.OPEN_EDX && } diff --git a/src/discussions/topics/data/slices.js b/src/discussions/topics/data/slices.js index 97707f88..4456a0c0 100644 --- a/src/discussions/topics/data/slices.js +++ b/src/discussions/topics/data/slices.js @@ -19,6 +19,9 @@ const topicsSlice = createSlice({ topics: {}, filter: '', sortBy: TopicOrdering.BY_COURSE_STRUCTURE, + results: { + count: 0, + }, }, reducers: { fetchCourseTopicsRequest: (state) => { @@ -44,6 +47,9 @@ const topicsSlice = createSlice({ setSortBy: (state, { payload }) => { state.sortBy = payload; }, + setTopicsCount: (state, { payload }) => { + state.results.count = payload; + }, }, }); @@ -53,6 +59,7 @@ export const { fetchCourseTopicsFailed, setFilter, setSortBy, + setTopicsCount, } = topicsSlice.actions; export const topicsReducer = topicsSlice.reducer; diff --git a/src/discussions/topics/topic-group/LegacyTopicGroup.jsx b/src/discussions/topics/topic-group/LegacyTopicGroup.jsx index ca2a5067..bab0774d 100644 --- a/src/discussions/topics/topic-group/LegacyTopicGroup.jsx +++ b/src/discussions/topics/topic-group/LegacyTopicGroup.jsx @@ -11,7 +11,6 @@ function LegacyTopicGroup({ category, }) { const topics = useSelector(selectTopicsInCategory(category)); - return ( ); diff --git a/src/discussions/topics/topic-group/TopicGroupBase.jsx b/src/discussions/topics/topic-group/TopicGroupBase.jsx index 61eda60a..9a52cb4e 100644 --- a/src/discussions/topics/topic-group/TopicGroupBase.jsx +++ b/src/discussions/topics/topic-group/TopicGroupBase.jsx @@ -26,8 +26,8 @@ function TopicGroupBase({ const topicElements = topics.filter( topic => ( filter - ? topic.name.toLowerCase() - .includes(filter) + ? (topic.name.toLowerCase() + .includes(filter) || matchesFilter) : true ), ) diff --git a/src/discussions/topics/utils.js b/src/discussions/topics/utils.js new file mode 100644 index 00000000..6499cb6e --- /dev/null +++ b/src/discussions/topics/utils.js @@ -0,0 +1,34 @@ +import { DiscussionProvider } from '../../data/constants'; + +export default function countFilteredTopics(topicsSelector, provider) { + let count = 0; + const query = topicsSelector?.filter?.trim().toLowerCase(); + // Counting non-courseware topics + const nonCoursewareTopicsList = topicsSelector.nonCoursewareIds?.map( + id => topicsSelector.topics[id], + ).filter(item => (query + ? item.name.toLowerCase().includes(query) + : true + )); + count += nonCoursewareTopicsList.length; + // Counting legacy topics + if (provider === DiscussionProvider.LEGACY) { + const categories = topicsSelector?.categoryIds; + const filteredTopics = categories?.map(categoryId => { + const topics = topicsSelector.topicsInCategory[categoryId]?.map( + id => topicsSelector.topics[id], + ) || []; + const matchesFilter = query ? categoryId?.toLowerCase().includes(query) : true; + return topics.filter( + topic => ( + query + ? (topic.name.toLowerCase() + .includes(query) || matchesFilter) + : true + ), + ); + }); + count += [].concat(...filteredTopics).length; + } + return count; +}