KarrLab/datanator_frontend

View on GitHub
src/scenes/SearchResults/SearchResultsList.js

Summary

Maintainability
F
5 days
Test Coverage
A
100%
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { Link } from "react-router-dom";
import { getDataFromApi, genApiErrorHandler } from "~/services/RestApi";
import axios from "axios";
import { parseHistoryLocationPathname } from "~/utils/utils";

class SearchResultsList extends Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    "get-results-url": PropTypes.func.isRequired,
    "format-results": PropTypes.func.isRequired,
    "html-anchor-id": PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    "page-size": PropTypes.number,
  };

  static defaultProps = {
    "page-size": 10,
  };

  constructor(props) {
    super(props);

    this.unlistenToHistory = null;
    this.cancelTokenSource = null;
    this.pageCount = 0;
    this.results = null;

    this.state = {
      results: null,
      numResults: null,
      locationPathname: "",
    };

    this.formatResult = this.formatResult.bind(this);
  }

  componentDidMount() {
    this.setState({ locationPathname: this.props.history.location.pathname });
    this.unlistenToHistory = this.props.history.listen((location) => {
      if (location.pathname !== this.state.locationPathname) {
        this.setState({ locationPathname: location.pathname });
        this.updateStateFromLocation();
      }
    });
    this.updateStateFromLocation();
  }

  componentWillUnmount() {
    this.unlistenToHistory();
    this.unlistenToHistory = null;
    if (this.cancelTokenSource) {
      this.cancelTokenSource.cancel();
    }
  }

  updateStateFromLocation() {
    if (this.unlistenToHistory) {
      const route = parseHistoryLocationPathname(this.props.history);
      this.query = route.query;
      this.organism = route.organism;
      this.pageCount = 0;
      this.results = null;
      this.setState({
        results: null,
        numResults: null,
      });
      this.fetchResults();
    }
  }

  fetchResults() {
    const url = this.props["get-results-url"](
      this.query,
      this.pageCount,
      this.props["page-size"]
    );

    // cancel earlier query
    if (this.cancelTokenSource) {
      this.cancelTokenSource.cancel();
    }

    this.cancelTokenSource = axios.CancelToken.source();

    getDataFromApi(url, { cancelToken: this.cancelTokenSource.token })
      .then((response) => {
        this.pageCount++;

        this.updateResults(
          this.props["format-results"](response.data, this.organism)
        );
      })
      .catch(
        genApiErrorHandler.bind(
          null,
          url,
          "We were unable to conduct your search for '" + this.query + "'."
        )
      )
      .finally(() => {
        this.cancelTokenSource = null;
      });
  }

  updateResults(newUnformattedResults) {
    const newResults = newUnformattedResults.results.map(this.formatResult);
    let results;
    if (this.results == null) {
      results = newResults;
    } else {
      results = this.results.concat(newResults);
    }
    this.results = results;

    this.setState({
      results: results,
      numResults: newUnformattedResults.numResults,
    });
  }

  formatResult(result, iResult) {
    return (
      <li key={this.pageCount * this.props["page-size"] + iResult}>
        <div className="search-result-title">
          <Link to={result.route}>{result.title}</Link>
        </div>
        <div className="search-result-description">{result.description}</div>
      </li>
    );
  }

  render() {
    const results = this.state.results;
    const numResults = this.state.numResults;
    const pageSize = this.props["page-size"];
    const numMore = Math.min(pageSize, numResults - pageSize * this.pageCount);

    return (
      <div className="content-block section" id={this.props["html-anchor-id"]}>
        <h2 className="content-block-heading">
          {this.props.title}
          {this.state.results
            ? " (" + this.state.results.length + " of " + numResults + ")"
            : ""}
        </h2>
        <div className="content-block-content">
          {results == null && <div className="loader"></div>}

          {results != null && (
            <div>
              {results.length > 0 && (
                <ul className="search-results-list">{results}</ul>
              )}

              {results.length === 0 && (
                <p className="no-search-results">No results found.</p>
              )}

              {results.length > 0 && numMore > 0 && (
                <button
                  className="more-search-results-button"
                  type="button"
                  onClick={() => {
                    this.fetchResults();
                  }}
                >
                  Load {numMore} more of {numResults}
                </button>
              )}
            </div>
          )}
        </div>
      </div>
    );
  }
}

export default withRouter(SearchResultsList);