MetaPhase-Consulting/State-TalentMAP

View on GitHub
src/Components/GlossaryEditor/GlossaryEditorPage/GlossaryEditorPage.jsx

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import { Component } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import groupBy from 'lodash/groupBy';
import { EMPTY_FUNCTION, GLOSSARY_ARRAY, GLOSSARY_ERROR_OBJECT, GLOSSARY_SUCCESS_OBJECT } from '../../../Constants/PropTypes';
import Spinner from '../../Spinner';
import GlossaryEditorContainer from '../GlossaryEditorContainer';
import GlossaryEditorSearch from '../GlossaryEditorSearch';
import { filterByProps } from '../../../utilities';
import GlossaryEditorPageHeader from '../GlossaryEditorPageHeader';

class GlossaryEditorPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      searchText: '',
      firstLetter: null,
      localSearchIsLoading: false,
    };
    // Create an instance attribute for storing a reference to debounced requests
    this.debouncedText = debounce(EMPTY_FUNCTION);
    this.debouncedLetter = debounce(EMPTY_FUNCTION);
    this.debounceTimeMs = 300;
  }

  // Update search text string and set localSearchIsLoading to false.
  changeText(text) {
    this.setState({ searchText: text.q, localSearchIsLoading: false });
  }

  // Update first letter string and set localSearchIsLoading to false.
  // This will be used for filtering by first letter.
  changeFirstLetter(firstLetter) {
    this.setState({ firstLetter, localSearchIsLoading: false });
  }

  // Perform client-side search but apply loading prop to provide
  // a more natural search exprience.
  debouncedChangeText = text => {
    this.setState({ localSearchIsLoading: true });
    this.debouncedText.cancel();
    this.debouncedText = debounce(q => this.changeText(q), this.debounceTimeMs);
    this.debouncedText(text);
  };

  debouncedChangeLetter = letter => {
    this.setState({ localSearchIsLoading: true });
    this.debouncedLetter.cancel();
    this.debouncedLetter = debounce(q => this.changeFirstLetter(q), this.debounceTimeMs);
    this.debouncedLetter(letter);
  };

  filteredTermsBySearch() {
    const { searchText } = this.state;
    const { glossaryItems } = this.props;

    // filter where the keyword matches part of the title or definition
    let filteredTerms = filterByProps(searchText, ['title', 'definition'], glossaryItems);
    filteredTerms = groupBy(filteredTerms, (term) => {
      const first = term.title.substr(0, 1).toUpperCase();
      // check if it's a letter
      const firstIsAlpha = first.match(/^[a-zA-Z]*$/);
      // if so, assign it to its first letter
      if (firstIsAlpha) {
        return first;
      // else, assign it to the '#' prop
      } return '#';
    });
    return filteredTerms;
  }

  filteredTermsBySearchAndGlossary() {
    const { firstLetter } = this.state;

    const filteredTerms = this.filteredTermsBySearch();

    // Keep the same structure as filteredTermsBySearch() by assigning
    // a single letter property to an object.
    if (firstLetter) {
      const filteredObject = {};
      filteredObject[firstLetter] = filteredTerms[firstLetter];
      return filteredObject;
    }

    return filteredTerms;
  }

  render() {
    const {
      glossaryIsLoading,
      glossaryHasErrored,
      submitGlossaryTerm,
      submitNewGlossaryTerm,
      glossaryPatchHasErrored,
      glossaryPatchSuccess,
      glossaryPostHasErrored,
      glossaryPostSuccess,
      onGlossaryEditorCancel,
    } = this.props;

    const { localSearchIsLoading } = this.state;

    const glossaryIsLoadingAsyncOrSync = glossaryIsLoading || localSearchIsLoading;
    const isLoading = glossaryIsLoadingAsyncOrSync && !glossaryHasErrored;

    const filteredGlossary = this.filteredTermsBySearchAndGlossary();
    const availableLetters = Object.keys(filteredGlossary);
    return (
      <div className="bidder-portfolio-page glossary-editor-page">
        <GlossaryEditorPageHeader
          submitNewGlossaryTerm={submitNewGlossaryTerm}
          onGlossaryEditorCancel={onGlossaryEditorCancel}
          glossaryPostHasErrored={glossaryPostHasErrored}
          glossaryPostSuccess={glossaryPostSuccess}
        />
        <GlossaryEditorSearch
          onUpdate={this.debouncedChangeText}
          submitGlossaryTerm={submitGlossaryTerm}
        />
        <div className="usa-grid-full bidder-portfolio-container profile-content-inner-container">
          <div className={`usa-grid-full bidder-portfolio-listing ${isLoading ? 'results-loading' : ''}`}>
            {
              isLoading &&
                <Spinner type="homepage-position-results" size="big" />
            }
            {
              !isLoading &&
                <GlossaryEditorContainer
                  submitGlossaryTerm={submitGlossaryTerm}
                  submitGlossaryFirstLetter={this.debouncedChangeLetter}
                  glossaryItems={filteredGlossary}
                  availableLetters={availableLetters}
                  glossaryPatchHasErrored={glossaryPatchHasErrored}
                  glossaryPatchSuccess={glossaryPatchSuccess}
                  onGlossaryEditorCancel={onGlossaryEditorCancel}
                />
            }
          </div>
        </div>
      </div>
    );
  }
}

GlossaryEditorPage.propTypes = {
  glossaryIsLoading: PropTypes.bool.isRequired,
  glossaryHasErrored: PropTypes.bool.isRequired,
  glossaryItems: GLOSSARY_ARRAY.isRequired,
  submitGlossaryTerm: PropTypes.func.isRequired,
  submitNewGlossaryTerm: PropTypes.func.isRequired,
  glossaryPatchHasErrored: GLOSSARY_ERROR_OBJECT,
  glossaryPatchSuccess: GLOSSARY_SUCCESS_OBJECT,
  glossaryPostHasErrored: GLOSSARY_ERROR_OBJECT,
  glossaryPostSuccess: GLOSSARY_SUCCESS_OBJECT,
  onGlossaryEditorCancel: PropTypes.func,
};

GlossaryEditorPage.defaultProps = {
  glossaryPatchHasErrored: {},
  glossaryPatchSuccess: {},
  glossaryPostHasErrored: {},
  glossaryPostSuccess: {},
  onGlossaryEditorCancel: EMPTY_FUNCTION,
};

export default GlossaryEditorPage;