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 <adeel.tajamul@arbisoft.com>
Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
This commit is contained in:
Muhammad Adeel Tajamul
2022-07-07 14:31:20 +05:00
committed by GitHub
parent ca1783a2b6
commit bf953354a1
11 changed files with 185 additions and 28 deletions

50
src/components/Search.jsx Normal file
View File

@@ -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 (
<SearchField
onClear={onClear}
onSubmit={onSubmit}
value={isPostSearch ? postSearch : topicSearch}
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
inputProps={{ className: 'small-font' }}
/>
);
}
Search.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Search);

View File

@@ -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 (
<div className="d-flex flex-row">
<Icon src={Search} className="justify-content-start ml-3.5 mr-2 mb-2 mt-2.5" />
<Button variant="" size="inline">
{intl.formatMessage(messages.searchInfo, { count, text })}
</Button>
<Button variant="link" size="inline" className="ml-auto mr-4" onClick={onClear}>
{intl.formatMessage(messages.clearSearch)}
</Button>
</div>
);
}
SearchInfo.propTypes = {
intl: intlShape.isRequired,
count: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
onClear: PropTypes.func,
};
SearchInfo.defaultProps = {
onClear: () => {},
};
export default injectIntl(SearchInfo);

View File

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

View File

@@ -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 (
<div className="discussion-posts d-flex flex-column">
{
searchString && <SearchInfo count={resultsFound} text={searchString} onClear={() => dispatch(setSearchQuery(''))} />
}
<PostFilterBar filterSelfPosts={showOwnPosts} />
<div className="list-group list-group-flush" role="list">
{postsListComponent}

View File

@@ -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({
<div className="d-flex justify-content-end py-1 flex-grow-1">
{!inContext && (
<>
<SearchField
onClear={() => 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' }}
/>
<div className="border-right mr-3 ml-4" />
<Search />
</>
)}
{inContext && (

View File

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

View File

@@ -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 (
<div
className="discussion-topics d-flex flex-column card"
data-testid="topics-view"
>
<TopicSearchBar />
{
topicFilter && <SearchInfo text={topicFilter} count={filteredTopicsCount} onClear={() => dispatch(setFilter(''))} />
}
<div className="list-group list-group-flush">
<CourseWideTopics />
{provider === DiscussionProvider.OPEN_EDX && <CoursewareTopics />}

View File

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

View File

@@ -11,7 +11,6 @@ function LegacyTopicGroup({
category,
}) {
const topics = useSelector(selectTopicsInCategory(category));
return (
<TopicGroupBase groupId={id} groupTitle={category} topics={topics} />
);

View File

@@ -26,8 +26,8 @@ function TopicGroupBase({
const topicElements = topics.filter(
topic => (
filter
? topic.name.toLowerCase()
.includes(filter)
? (topic.name.toLowerCase()
.includes(filter) || matchesFilter)
: true
),
)

View File

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