diff --git a/app/assets/javascripts/components/article_finder/article_finder.jsx b/app/assets/javascripts/components/article_finder/article_finder.jsx index 9c3476d060..dfc3af8777 100644 --- a/app/assets/javascripts/components/article_finder/article_finder.jsx +++ b/app/assets/javascripts/components/article_finder/article_finder.jsx @@ -1,468 +1,532 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; -import { connect } from 'react-redux'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; import InputRange from 'react-input-range'; import { includes, map, find } from 'lodash-es'; import qs from 'query-string'; import SelectedWikiOption from '../common/selected_wiki_option'; -import { compose } from 'redux'; -import withRouter from '../util/withRouter'; - import TextInput from '../common/text_input.jsx'; import ArticleFinderRow from './article_finder_row.jsx'; import List from '../common/list.jsx'; import Loading from '../common/loading.jsx'; - import { STUDENT_ROLE } from '../../constants'; -import { ORESSupportedWiki, PageAssessmentSupportedWiki } from '../../utils/article_finder_language_mappings.js'; -import { fetchCategoryResults, fetchKeywordResults, updateFields, sortArticleFinder, resetArticleFinder, clearResults } from '../../actions/article_finder_action.js'; -import { fetchAssignments, addAssignment, deleteAssignment } from '../../actions/assignment_actions.js'; -import { getFilteredArticleFinder } from '../../selectors'; - +import { + ORESSupportedWiki, + PageAssessmentSupportedWiki, +} from '../../utils/article_finder_language_mappings.js'; +import { + fetchCategoryResults, + fetchKeywordResults, + updateFields, + sortArticleFinder, + resetArticleFinder, + clearResults, +} from '../../actions/article_finder_action.js'; +import { + fetchAssignments, + addAssignment, + deleteAssignment, +} from '../../actions/assignment_actions.js'; import { trackedWikisMaker } from '../../utils/wiki_utils'; import ArticleUtils from '../../utils/article_utils'; - -const ArticleFinder = createReactClass({ - getDefaultProps() { - return { - course: { - home_wiki: { - language: 'en', - project: 'wikipedia' - } +import { useLocation } from 'react-router-dom'; +import { table_keys } from './constants'; +import useInitialiseArticleFinder from './hooks/useInitialiseArticleFinder'; + +const ArticleFinder = (props) => { + const dispatch = useDispatch(); + const location = useLocation(); + + const { + course_id, + current_user, + course = { + home_wiki: { + language: 'en', + project: 'wikipedia', + }, + }, + } = props; + + const { + articles, + unfilteredArticles, + wikidataLabels, + loading, + search_term, + min_views, + article_quality, + search_type, + continue_results, + offset, + cmcontinue, + assignments, + loadingAssignments, + fetchState, + sort, + home_wiki, + selectedWiki, + buildURL, + } = useInitialiseArticleFinder(); + + const [isSubmitted, setIsSubmitted] = useState(false); + const [showFilters, setShowFilters] = useState(false); + const [_table_keys, setTableKeys] = useState(table_keys); + const searchBoxRef = useRef(''); + + useEffect(() => { + const loadOnMount = () => { + if (window.location.search.substring(1)) { + getParamsURL(); + } + if (course_id && loadingAssignments) { + dispatch(fetchAssignments(course_id)); } + if (location.project) { + return _updateFields('wiki', { + language: location.language, + project: location.project, + }); + } + return _updateFields('home_wiki', home_wiki); }; - }, + loadOnMount(); - getInitialState() { - return { - isSubmitted: false, - showFilters: false, + return () => { + dispatch(clearResults()); + dispatch(resetArticleFinder()); }; - }, + }, []); + + useEffect(() => { + const handleSortKey = () => { + const newTableKeys = { ..._table_keys }; + if (sort.key) { + const order = sort.sortKey ? 'asc' : 'desc'; + Object.entries(newTableKeys).forEach((item) => { + const [key, value] = item; + if (key === sort.key) { + value.order = order; + } else { + delete value.order; + } + }); + console.log('newTableKeys', newTableKeys); + setTableKeys(newTableKeys); + } + if ( + !includes(ORESSupportedWiki.languages, selectedWiki.language) + || !includes(ORESSupportedWiki.projects, selectedWiki.project) + ) { + delete newTableKeys.revScore; + setTableKeys(newTableKeys); + } - componentDidMount() { - if (window.location.search.substring(1)) { - this.getParamsURL(); - } - if (this.props.course_id && this.props.loadingAssignments) { - this.props.fetchAssignments(this.props.course_id); - } - if (this.props.router.location.project) { - return this.updateFields('wiki', { language: this.props.router.location.language, project: this.props.router.location.project }); - } - return this.updateFields('home_wiki', this.props.course.home_wiki); - }, + if ( + !PageAssessmentSupportedWiki[selectedWiki.project] + || !includes( + PageAssessmentSupportedWiki[selectedWiki.project], + selectedWiki.language + ) + ) { + delete newTableKeys.grade; + setTableKeys(newTableKeys); + } - componentWillUnmount() { - return this.props.resetArticleFinder(); - }, + if (!course_id || !current_user.id || current_user.notEnrolled) { + delete newTableKeys.tools; + setTableKeys(newTableKeys); + } + }; - onKeyDown(keyCode, ref) { + handleSortKey(); + }, [ + sort.key, + sort.sortKey, + course_id, + current_user.id, + current_user.notEnrolled, + PageAssessmentSupportedWiki[selectedWiki.project], + ORESSupportedWiki, + ]); + + const onKeyDown = (keyCode, ref) => { if (keyCode === 13) { ref.blur(); - this.searchArticles(); + searchArticles(); } - }, + }; - getParamsURL() { + const getParamsURL = () => { const query = qs.parse(window.location.search); const entries = Object.entries(query); entries.map(([key, val]) => { - val = (key === 'article_quality') ? parseInt(val) : val; - return this.updateFields(key, val); + val = key === 'article_quality' ? parseInt(val) : val; + return _updateFields(key, val); }); - }, - - updateFields(key, value) { - const update_field = this.props.updateFields(key, value); - Promise.resolve(update_field).then(() => { - if (this.props.search_term.length !== 0) { - this.buildURL(); - } - }); - }, - - toggleFilter() { - return this.setState({ - showFilters: !this.state.showFilters - }); - }, - buildURL() { - let queryStringUrl = window.location.href.split('?')[0]; - const params_array = ['search_type', 'article_quality', 'min_views']; - queryStringUrl += `?search_term=${this.props.search_term}`; - params_array.forEach((param) => { - return queryStringUrl += `&${param}=${this.props[param]}`; - }); - history.replaceState(window.location.href, 'query_string', queryStringUrl); - }, - searchArticles() { - this.setState({ - isSubmitted: true, - }); - if (this.props.search_term === '') { - return this.setState({ - isSubmitted: false, - }); - } else if (this.props.search_type === 'keyword') { - this.buildURL(); - return this.props.fetchKeywordResults(this.props.search_term, this.props.selectedWiki); + }; + + const _updateFields = (key, value) => { + dispatch(updateFields(key, value)); + }; + + const toggleFilter = () => { + setShowFilters(isShown => !isShown); + }; + + const searchArticles = () => { + setIsSubmitted(true); + if (search_term === '') { + setIsSubmitted(false); + } else if (search_type === 'keyword') { + buildURL(); + return dispatch(fetchKeywordResults(search_term, selectedWiki)); } - return this.props.fetchCategoryResults(this.props.search_term, this.props.selectedWiki); - }, + return dispatch(fetchCategoryResults(search_term, selectedWiki)); + }; - fetchMoreResults() { - if (this.props.search_type === 'keyword') { - return this.props.fetchKeywordResults(this.props.search_term, this.props.selectedWiki, this.props.offset, true); + const fetchMoreResults = () => { + if (search_type === 'keyword') { + return dispatch( + fetchKeywordResults(search_term, selectedWiki, offset, true) + ); } - return this.props.fetchCategoryResults(this.props.search_term, this.props.selectedWiki, this.props.cmcontinue, true); - }, - - handleChange(e) { - const grade = e.target.value; - return this.props.updateFields('grade', grade); - }, + return dispatch( + fetchCategoryResults(search_term, selectedWiki, cmcontinue, true) + ); + }; - handleWikiChange(wiki) { + const handleWikiChange = (wiki) => { wiki = wiki.value; - this.setState({ isSubmitted: false }); - this.props.clearResults(); - return this.updateFields('wiki', { language: wiki.language, project: wiki.project }); - }, - - sortSelect(e) { - this.props.sortArticleFinder(e.target.value); - }, - - render() { - const searchButton = ; - const searchTerm = ( - {searchButton} - ); - - const searchType = ( -
-
-
- -
-
- -
+ setIsSubmitted(false); + dispatch(clearResults()); + return _updateFields('wiki', { + language: wiki.language, + project: wiki.project, + }); + }; + + const searchTerm = ( + + + + ); + + const searchType = () => ( +
+
+
+ +
+
+
- ); +
+ ); - const minimumViews = ( -
- ( +
+ +
+ ); + + const articleQuality = () => ( +
+
+ + _updateFields('article_quality', value)} + step={1} />
- ); - - const articleQuality = ( -
-
- - this.updateFields('article_quality', value)} - step={1} - /> -
-
- ); - let filters; - if (this.state.showFilters) { - filters = ( -
- {minimumViews} - {articleQuality} - {searchType} -
- ); - } - - let filterButton; - if (!this.state.showFilters) { - filterButton = ( - - ); - } else { - filterButton = ( - - ); +
+ ); + + const filterBtnClassAndText = useMemo(() => { + if (!showFilters) { + return { + className: 'button dark', + text: I18n.t('article_finder.show_options'), + }; } + return { + className: 'button', + text: I18n.t('article_finder.hide_options'), + }; + }, [showFilters]); - let filterBlock; - if (this.state.isSubmitted && !this.props.loading) { - filterBlock = ( + const filterBlock = useMemo(() => { + if (isSubmitted && !loading) { + return (
- {filterButton} -
-
- {filters} +
+ {showFilters ? ( +
+
+ {minimumViews()} + {articleQuality()} + {searchType()} +
+
+ ) : null}
); } - - const keys = { - relevanceIndex: { - label: I18n.t('article_finder.relevanceIndex'), - desktop_only: false - }, - title: { - label: I18n.t('articles.title'), - desktop_only: false - }, - grade: { - label: I18n.t('article_finder.page_assessment_class'), - desktop_only: false, - sortable: true, - }, - revScore: { - label: I18n.t('article_finder.completeness_estimate'), - desktop_only: false, - sortable: true, - }, - pageviews: { - label: I18n.t('article_finder.average_views'), - desktop_only: false, - sortable: true, - }, - tools: { - label: I18n.t('article_finder.tools'), - desktop_only: false, - sortable: false, + }, [ + isSubmitted, + loading, + showFilters, + minimumViews, + articleQuality, + searchType, + ]); + + const _sortArticleFinder = (key) => { + dispatch(sortArticleFinder(key)); + }; + + const renderList = () => { + const modifiedAssignmentsArray = map(assignments, (element) => { + if (!element.language && !element.project) { + return { + ...element, + language: selectedWiki.language, + project: selectedWiki.project, + }; } - }; - if (this.props.sort.key) { - const order = (this.props.sort.sortKey) ? 'asc' : 'desc'; - keys[this.props.sort.key].order = order; - } - if (!includes(ORESSupportedWiki.languages, this.props.selectedWiki.language) || !includes(ORESSupportedWiki.projects, this.props.selectedWiki.project)) { - delete keys.revScore; - } - - if (!PageAssessmentSupportedWiki[this.props.selectedWiki.project] || !includes(PageAssessmentSupportedWiki[this.props.selectedWiki.project], this.props.selectedWiki.language)) { - delete keys.grade; - } - - if (!this.props.course_id || !this.props.current_user.id || this.props.current_user.notEnrolled) { - delete keys.tools; - } + return element; + }); - let list; - if (this.state.isSubmitted && !this.props.loading) { - const modifiedAssignmentsArray = map(this.props.assignments, (element) => { - if (!element.language && !element.project) { - return { - ...element, - language: this.props.selectedWiki.language, - project: this.props.selectedWiki.project - }; - } - return element; - }); - - const elements = map(this.props.articles, (article, title) => { - let assignment; - if (this.props.course_id) { - if (this.props.current_user.isAdvancedRole) { - assignment = find(modifiedAssignmentsArray, { article_title: title, user_id: null, language: this.props.selectedWiki.language, project: this.props.selectedWiki.project }); - } else if (this.props.current_user.role === STUDENT_ROLE) { - assignment = find(modifiedAssignmentsArray, { article_title: title, user_id: this.props.current_user.id, language: this.props.selectedWiki.language, project: this.props.selectedWiki.project }); - } + const elements = map(articles, (article, title) => { + let assignment; + if (course_id) { + if (current_user.isAdvancedRole) { + assignment = find(modifiedAssignmentsArray, { + article_title: title, + user_id: null, + language: selectedWiki.language, + project: selectedWiki.project, + }); + } else if (current_user.role === STUDENT_ROLE) { + assignment = find(modifiedAssignmentsArray, { + article_title: title, + user_id: current_user.id, + language: selectedWiki.language, + project: selectedWiki.project, + }); } + } - return ( - - ); - }); - list = ( - dispatch(addAssignment)} + deleteAssignment={() => dispatch(deleteAssignment)} + current_user={current_user} /> ); - } - - let loader; - if (this.state.isSubmitted && this.props.loading) { - loader = ; - } - - let fetchMoreButton; - if (this.props.continue_results && this.state.isSubmitted) { - fetchMoreButton = ( - - ); - } - - let searchStats; - if (!this.props.loading && this.state.isSubmitted) { - const fetchedCount = Object.keys(this.props.unfilteredArticles).length; - const filteredCount = Object.keys(this.props.articles).length; - searchStats = ( -
-
-
-
{fetchedCount}
- {I18n.t(`article_finder.${ArticleUtils.projectSuffix(this.props.selectedWiki.project, 'fetched_articles')}`)} -
-
-
{filteredCount}
- {I18n.t(`article_finder.${ArticleUtils.projectSuffix(this.props.selectedWiki.project, 'filtered_articles')}`)} -
-
-
- ); - } - - const loaderMessage = { - ARTICLES_LOADING: I18n.t(`article_finder.${ArticleUtils.projectSuffix(this.props.selectedWiki.project, 'searching_articles')}`), - TITLE_RECEIVED: I18n.t('article_finder.fetching_assessments'), - PAGEASSESSMENT_RECEIVED: I18n.t('article_finder.fetching_revisions'), - REVISION_RECEIVED: I18n.t('article_finder.fetching_scores'), - REVISIONSCORE_RECEIVED: I18n.t('article_finder.fetching_pageviews'), - }; - - let fetchingLoader; - if (this.props.fetchState !== 'PAGEVIEWS_RECEIVED' && !this.props.loading) { - fetchingLoader = ( -
-
- {loaderMessage[this.props.fetchState]} -
- ); - } - - const trackedWikis = trackedWikisMaker(this.props.course); + }); - const options = ( - ); + }; + const fetchMoreButton = () => { return ( -
-
-

{I18n.t(`article_finder.${ArticleUtils.projectSuffix(this.props.selectedWiki.project, 'article_finder')}`)}

-
- {I18n.t(`article_finder.${ArticleUtils.projectSuffix(this.props.selectedWiki.project, 'subheading_message')}`)} -
-
-
-
- {searchTerm} -
-
- {options} - {filterBlock} -
- {searchStats} -
- {fetchingLoader} + + ); + }; + + const getSearchStats = () => { + const fetchedCount = Object.keys(unfilteredArticles).length; + const filteredCount = Object.keys(articles).length; + return ( +
+
+
+
{fetchedCount}
+ + {I18n.t( + `article_finder.${ArticleUtils.projectSuffix( + selectedWiki.project, + 'fetched_articles' + )}` + )} +
-
- {fetchMoreButton} +
+
{filteredCount}
+ + {I18n.t( + `article_finder.${ArticleUtils.projectSuffix( + selectedWiki.project, + 'filtered_articles' + )}` + )} +
- {loader} - {list} -
- {fetchMoreButton} -
); - } -}); - -const mapStateToProps = state => ({ - articles: getFilteredArticleFinder(state), - unfilteredArticles: state.articleFinder.articles, - wikidataLabels: state.wikidataLabels.labels, - loading: state.articleFinder.loading, - search_term: state.articleFinder.search_term, - min_views: state.articleFinder.min_views, - article_quality: state.articleFinder.article_quality, - depth: state.articleFinder.depth, - search_type: state.articleFinder.search_type, - continue_results: state.articleFinder.continue_results, - offset: state.articleFinder.offset, - cmcontinue: state.articleFinder.cmcontinue, - assignments: state.assignments.assignments, - loadingAssignments: state.assignments.loading, - fetchState: state.articleFinder.fetchState, - sort: state.articleFinder.sort, - home_wiki: state.articleFinder.home_wiki, - selectedWiki: state.articleFinder.wiki || state.articleFinder.home_wiki -}); - -const mapDispatchToProps = { - fetchCategoryResults: fetchCategoryResults, - updateFields: updateFields, - addAssignment: addAssignment, - fetchAssignments: fetchAssignments, - sortArticleFinder: sortArticleFinder, - fetchKeywordResults: fetchKeywordResults, - deleteAssignment: deleteAssignment, - resetArticleFinder: resetArticleFinder, - clearResults: clearResults, + }; + + const loaderMessage = { + ARTICLES_LOADING: I18n.t( + `article_finder.${ArticleUtils.projectSuffix( + selectedWiki.project, + 'searching_articles' + )}` + ), + TITLE_RECEIVED: I18n.t('article_finder.fetching_assessments'), + PAGEASSESSMENT_RECEIVED: I18n.t('article_finder.fetching_revisions'), + REVISION_RECEIVED: I18n.t('article_finder.fetching_scores'), + REVISIONSCORE_RECEIVED: I18n.t('article_finder.fetching_pageviews'), + }; + + const displayLoaderMessage = () => { + return ( +
+
+ {loaderMessage[fetchState]} +
+ ); + }; + + const options = ( + + ); + + const listStatus = isSubmitted && !loading; + const isLoadingStatus = isSubmitted && loading; + const isSubmittedButNotLoadingStatus = !loading && isSubmitted; + const pageViewsRecievedStatus = fetchState !== 'PAGEVIEWS_RECEIVED' && !loading; + const fetchMoreResultsStatus = continue_results && isSubmitted; + + const trackedWikis = trackedWikisMaker(course); + + return ( +
+
+

+ {I18n.t( + `article_finder.${ArticleUtils.projectSuffix( + selectedWiki.project, + 'article_finder' + )}` + )} +

+
+ {I18n.t( + `article_finder.${ArticleUtils.projectSuffix( + selectedWiki.project, + 'subheading_message' + )}` + )} +
+
+
+
{searchTerm}
+
+ {options} + {filterBlock} +
+ {isSubmittedButNotLoadingStatus ? getSearchStats() : null} +
{pageViewsRecievedStatus ? displayLoaderMessage() : null}
+
{fetchMoreResultsStatus ? fetchMoreButton() : null}
+
+ {isLoadingStatus ? : null} + {listStatus ? renderList() : null} +
+ {fetchMoreResultsStatus ? fetchMoreButton() : null} +
+
+ ); }; -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ArticleFinder); +export default ArticleFinder; diff --git a/app/assets/javascripts/components/article_finder/constants.js b/app/assets/javascripts/components/article_finder/constants.js new file mode 100644 index 0000000000..5da5d22879 --- /dev/null +++ b/app/assets/javascripts/components/article_finder/constants.js @@ -0,0 +1,30 @@ +export const table_keys = { + relevanceIndex: { + label: I18n.t('article_finder.relevanceIndex'), + desktop_only: false, + }, + title: { + label: I18n.t('articles.title'), + desktop_only: false, + }, + grade: { + label: I18n.t('article_finder.page_assessment_class'), + desktop_only: false, + sortable: true, + }, + revScore: { + label: I18n.t('article_finder.completeness_estimate'), + desktop_only: false, + sortable: true, + }, + pageviews: { + label: I18n.t('article_finder.average_views'), + desktop_only: false, + sortable: true, + }, + tools: { + label: I18n.t('article_finder.tools'), + desktop_only: false, + sortable: false, + }, +}; diff --git a/app/assets/javascripts/components/article_finder/hooks/useInitialiseArticleFinder.jsx b/app/assets/javascripts/components/article_finder/hooks/useInitialiseArticleFinder.jsx new file mode 100644 index 0000000000..bdc569f0ac --- /dev/null +++ b/app/assets/javascripts/components/article_finder/hooks/useInitialiseArticleFinder.jsx @@ -0,0 +1,78 @@ +import { getFilteredArticleFinder } from '../../../selectors'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +const useArticlefinderData = () => { + const articles = useSelector(state => getFilteredArticleFinder(state)); + const unfilteredArticles = useSelector( + state => state.articleFinder.articles + ); + const wikidataLabels = useSelector(state => state.wikidataLabels.labels); + const loading = useSelector(state => state.articleFinder.loading); + const search_term = useSelector(state => state.articleFinder.search_term); + const min_views = useSelector(state => state.articleFinder.min_views); + const article_quality = useSelector( + state => state.articleFinder.article_quality + ); + const search_type = useSelector(state => state.articleFinder.search_type); + const continue_results = useSelector( + state => state.articleFinder.continue_results + ); + const offset = useSelector(state => state.articleFinder.offset); + const cmcontinue = useSelector(state => state.articleFinder.cmcontinue); + const assignments = useSelector(state => state.assignments.assignments); + const loadingAssignments = useSelector(state => state.assignments.loading); + const fetchState = useSelector(state => state.articleFinder.fetchState); + const sort = useSelector(state => state.articleFinder.sort); + const home_wiki = useSelector(state => state.articleFinder.home_wiki); + const selectedWiki = useSelector( + state => state.articleFinder.wiki || state.articleFinder.home_wiki + ); + + useEffect(() => { + // build url if any of the depencency change + buildURL(); + }, [ + search_term, + search_type, + article_quality, + min_views, + selectedWiki, + home_wiki, + ]); + + const buildURL = (search_value = '') => { + const itemToSearch = search_value || search_term; + let queryStringUrl = window.location.href.split('?')[0]; + const params_array = { search_type, article_quality, min_views }; + queryStringUrl += `?search_term=${itemToSearch}`; + Object.entries(params_array).forEach((param) => { + const [key, value] = param; + return (queryStringUrl += `&${key}=${value}`); + }); + history.replaceState(window.location.href, 'query_string', queryStringUrl); + }; + + return { + articles, + unfilteredArticles, + wikidataLabels, + loading, + search_term, + min_views, + article_quality, + search_type, + continue_results, + offset, + cmcontinue, + assignments, + loadingAssignments, + fetchState, + sort, + home_wiki, + selectedWiki, + buildURL, + }; +}; + +export default useArticlefinderData;