WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/components/article_finder/article_finder.jsx

Summary

Maintainability
F
5 days
Test Coverage
C
76%
import React, { useState, useEffect } from 'react';
import { connect } 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 { trackedWikisMaker } from '../../utils/wiki_utils';
import ArticleUtils from '../../utils/article_utils';
import ArticleFinderSearchBar from './article_finder_search_bar';

const ArticleFinder = (props) => {
  const [isSubmitted, setIsSubmitted] = useState(false);
  const [showFilters, setShowFilters] = useState(false);

  useEffect(() => {
    if (window.location.search.substring(1)) {
      getParamsURL();
    }
    if (props.course_id && props.loadingAssignments) {
      props.fetchAssignments(props.course_id);
    }
    if (props.router.location.project) {
      updateFieldsHandler('wiki', { language: props.router.location.language, project: props.router.location.project });
    } else {
      updateFieldsHandler('home_wiki', props.course.home_wiki);
    }
    return () => {
      props.resetArticleFinder();
    };
  }, []);

  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 updateFieldsHandler(key, val);
    });
  };

  const updateFieldsHandler = (key, value) => {
    const update_field = props.updateFields(key, value);
    Promise.resolve(update_field).then(() => {
      if (props.search_term.length !== 0 || key === 'search_term') {
        buildURL(key, value);
      }
    });
  };

  const toggleFilter = () => {
    setShowFilters(!showFilters);
  };

  const buildURL = (key, value) => {
    let queryStringUrl = window.location.href.split('?')[0];
    const params_array = ['search_type', 'article_quality', 'min_views'];
    const latestSearch = (key === 'search_term') ? value : props.search_term;
    queryStringUrl += `?search_term=${latestSearch}`;
    params_array.forEach((param) => {
      return queryStringUrl += (param === key) ? `&${param}=${value}` : `&${param}=${props[param]}`;
    });
    history.replaceState(window.location.href, 'query_string', queryStringUrl);
  };

  const searchArticles = async (searchTerm) => {
    setIsSubmitted(true);
    buildURL('search_term', searchTerm);
    if (searchTerm === '') {
      return setIsSubmitted(false);
    } else if (props.search_type === 'keyword') {
      return props.fetchKeywordResults(searchTerm, props.selectedWiki);
    }
    return props.fetchCategoryResults(searchTerm, props.selectedWiki);
  };

  const fetchMoreResults = () => {
    if (props.search_type === 'keyword') {
      return props.fetchKeywordResults(props.search_term, props.selectedWiki, props.offset, true);
    }
    return props.fetchCategoryResults(props.search_term, props.selectedWiki, props.cmcontinue, true);
  };

  const handleWikiChange = (wiki) => {
    wiki = wiki.value;
    setIsSubmitted(false);
    props.clearResults();
    return updateFieldsHandler('wiki', { language: wiki.language, project: wiki.project });
  };

    const searchType = (
      <div>
        <div className="search-type">
          <div>
            <label>
              <input type="radio" value="keyword" checked={props.search_type === 'keyword'} onChange={e => updateFieldsHandler('search_type', e.target.value)} />
              {I18n.t('article_finder.keyword_search')}
            </label>
          </div>
          <div>
            <label>
              <input type="radio" value="category" checked={props.search_type === 'category'} onChange={e => updateFieldsHandler('search_type', e.target.value)} />
              {I18n.t('article_finder.category_search')}
            </label>
          </div>
        </div>
      </div>
    );

    const minimumViews = (
      <div>
        <TextInput
          id="min_views"
          onChange={updateFieldsHandler}
          value={props.min_views}
          value_key="min_views"
          required
          editable
          label={I18n.t('article_finder.minimum_views_label')}
          placeholder={I18n.t('article_finder.minimum_views_label')}
        />
      </div>
    );

    const articleQuality = (
      <div>
        <div className="form-group range-container">
          <label className="mb2">{I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'article_quality')}`)}</label>
          <InputRange
            maxValue={100}
            minValue={0}
            value={props.article_quality}
            onChange={value => updateFieldsHandler('article_quality', value)}
            step={1}
          />
        </div>
      </div>
    );
    let filters;
    if (showFilters) {
      filters = (
        <div className="filters">
          {minimumViews}
          {articleQuality}
          {searchType}
        </div>
      );
    }

    let filterButton;
    if (!showFilters) {
      filterButton = (
        <button className="button dark" onClick={toggleFilter}>{I18n.t('article_finder.show_options')}</button>
      );
    } else {
      filterButton = (
        <button className="button" onClick={toggleFilter}>{I18n.t('article_finder.hide_options')}</button>
      );
    }

    let filterBlock;
    if (isSubmitted && !props.loading) {
      filterBlock = (
        <div className="filter-block">
          <div className="filter-button-block">
            {filterButton}
          </div>
          <div className="filter-items">
            {filters}
          </div>
        </div>
      );
    }

    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,
      }
    };
    if (props.sort.key) {
      const order = (props.sort.sortKey) ? 'asc' : 'desc';
      keys[props.sort.key].order = order;
    }
  if (!includes(ORESSupportedWiki.languages, props.selectedWiki.language) || !includes(ORESSupportedWiki.projects, props.selectedWiki.project)) {
      delete keys.revScore;
    }

    if (!PageAssessmentSupportedWiki[props.selectedWiki.project] || !includes(PageAssessmentSupportedWiki[props.selectedWiki.project], props.selectedWiki.language)) {
      delete keys.grade;
    }

    if (!props.course_id || !props.current_user.id || props.current_user.notEnrolled) {
      delete keys.tools;
    }

    let list;
    if (isSubmitted && !props.loading) {
      const modifiedAssignmentsArray = map(props.assignments, (element) => {
          if (!element.language && !element.project) {
            return {
                ...element,
                language: props.selectedWiki.language,
                project: props.selectedWiki.project
              };
          }
          return element;
      });

      const elements = map(props.articles, (article, title) => {
        let assignment;
        if (props.course_id) {
          if (props.current_user.isAdvancedRole) {
            assignment = find(modifiedAssignmentsArray, { article_title: title, user_id: null, language: props.selectedWiki.language, project: props.selectedWiki.project });
          } else if (props.current_user.role === STUDENT_ROLE) {
            assignment = find(modifiedAssignmentsArray, { article_title: title, user_id: props.current_user.id, language: props.selectedWiki.language, project: props.selectedWiki.project });
          }
        }

        return (
          <ArticleFinderRow
            article={article}
            title={title}
            label={props.wikidataLabels[title]}
            key={article.pageid}
            courseSlug={props.course_id}
            course={props.course}
            selectedWiki={props.selectedWiki}
            assignment={assignment}
            addAssignment={props.addAssignment}
            deleteAssignment={props.deleteAssignment}
            current_user={props.current_user}
          />
        );
      });
      list = (
        <List
          elements={elements}
          keys={keys}
          sortable={true}
          table_key="category-articles"
          className="table--expandable table--hoverable"
          none_message={I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'no_article_found')}`)}
          sortBy={props.sortArticleFinder}
        />
      );
    }

    let loader;
    if (isSubmitted && props.loading) {
      loader = <Loading />;
    }

    let fetchMoreButton;
    if (props.continue_results && isSubmitted) {
      fetchMoreButton = (
        <button className="button dark text-center fetch-more" onClick={fetchMoreResults}>{I18n.t('article_finder.more_results')}</button>
      );
    }

    let searchStats;
    if (!props.loading && isSubmitted) {
      const fetchedCount = Object.keys(props.unfilteredArticles).length;
      const filteredCount = Object.keys(props.articles).length;
      searchStats = (
        <div>
          <div className="stat-display">
            <div className="stat-display__stat" id="articles-fetched">
              <div className="stat-display__value">{fetchedCount}</div>
              <small>{I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'fetched_articles')}`)}</small>
            </div>
            <div className="stat-display__stat" id="articles-filtered">
              <div className="stat-display__value">{filteredCount}</div>
              <small>{I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'filtered_articles')}`)}</small>
            </div>
          </div>
        </div>
      );
    }

    const loaderMessage = {
      ARTICLES_LOADING: I18n.t(`article_finder.${ArticleUtils.projectSuffix(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 (props.fetchState !== 'PAGEVIEWS_RECEIVED' && !props.loading) {
      fetchingLoader = (
        <div className="text-center">
          <div className="loading__spinner__small" />
          {loaderMessage[props.fetchState]}
        </div>
      );
    }

    const trackedWikis = trackedWikisMaker(props.course);

    const options = (
      <div className="article-finder-options">
        <SelectedWikiOption
          language={props.selectedWiki.language || 'www'}
          project={props.selectedWiki.project}
          handleWikiChange={handleWikiChange}
          trackedWikis={trackedWikis}
        />
      </div>
    );

    const isButtonDisabled = props.fetchState !== 'PAGEVIEWS_RECEIVED' && !props.loading;

    return (
      <div className="container">
        <header>
          <h1 className="title">{I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'article_finder')}`)}</h1>
          <div>
            {I18n.t(`article_finder.${ArticleUtils.projectSuffix(props.selectedWiki.project, 'subheading_message')}`)}
          </div>
        </header>
        <div className="article-finder-form">

          <ArticleFinderSearchBar
            value={props.search_term}
            wiki={props.selectedWiki}
            onChange={(searchTerm) => {
              return props.updateFields('search_term', searchTerm);
            }}
            onSearch={searchArticles}
            disabled={isButtonDisabled}
          />

        </div>
        {options}
        {filterBlock}
        <div className="article-finder-stats horizontal-flex">
          {searchStats}
          <div>
            {fetchingLoader}
          </div>
          <div>
            {fetchMoreButton}
          </div>
        </div>
        {loader}
        {list}
        <div className="py2 text-center">
          {fetchMoreButton}
        </div>
      </div>
    );
};

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

export default compose(
  withRouter,
  connect(mapStateToProps, mapDispatchToProps)
)(ArticleFinder);

ArticleFinder.defaultProps = {
  course: {
    home_wiki: {
      language: 'en',
      project: 'wikipedia'
    }
  }
};