WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/components/course/course_approval.jsx

Summary

Maintainability
F
3 days
Test Coverage
B
81%
import React, { useEffect, useState } from 'react';

import { connect } from 'react-redux';
import { difference, uniq } from 'lodash-es';

import PropTypes from 'prop-types';
import Select from 'react-select';
import TextInput from '../common/text_input';
import CreatableSelect from 'react-select/creatable';
import selectStyles from '../../styles/single_select';

import { fetchSpecialUsers } from '../../actions/settings_actions';
import { fetchAllCampaigns, addCampaign } from '../../actions/campaign_actions';
import { removeTag, fetchAllTags, addTag } from '../../actions/tag_actions';
import { linkToSalesforce } from '../../actions/course_actions';
import { addUser } from '../../actions/user_actions';
import { getCourseApprovalStaff } from '../../selectors';
import { STAFF_ROLE } from '../../constants';
import { inferDefaultCampaign } from './utils/inferDefaultCampaign';
import { extractSalesforceId } from '../../utils/salesforce_utils.js';


const CourseApproval = (props) => {
  const [selectedWikiExpert, setSelectedWikiExpert] = useState({});
  const [selectedCampaigns, setSelectedCampaigns] = useState([]);
  const [selectedTags, setSelectedTags] = useState([]);
  const [createdTagOption, setCreatedTagOption] = useState([]);
  const [salesforceId, setSalesforceId] = useState('');
  const [showInvalidIdMessage, setShowInvalidIdMessage] = useState(false);
  const [submitting, setSubmitting] = useState(false);

  useEffect(() => {
    props.fetchSpecialUsers();
    props.fetchAllCampaigns();
    props.fetchAllTags();

    // Cleanup function to clear all states after component unmounts
    return function cleanUp() {
      setSelectedWikiExpert({});
      setSelectedCampaigns([]);
      setSelectedTags([]);
      setCreatedTagOption([]);
      setSubmitting(false);
    };
  }, []);

  useEffect(() => {
    if (props.wikiEdStaff.length > 0) {
      const wikiExpert = props.wikiEdStaff.find(user => user.role === 'wikipedia_expert');
      const currentWikiExpert = props.wikiEdStaff.find(user => user.role === 'wikipedia_expert' && user.already_selected);

      // Check if there exists a wiki expert who is already allotted a staff role. If yes, set that
      // user as selectedWikiExpert. Else, set the first wikipedia expert user as the selectedWikiExpert.
      setSelectedWikiExpert(
        (currentWikiExpert !== null && currentWikiExpert !== undefined)
          ? { value: currentWikiExpert.username, label: `${currentWikiExpert.username} (${currentWikiExpert.realname})` }
          : { value: wikiExpert.username, label: `${wikiExpert.username} (${wikiExpert.realname})` }
      );
    }

    if (props.allCampaigns.length > 0) {
      const defaultCampaign = inferDefaultCampaign(props.allCampaigns, props.course.start);
      if (defaultCampaign !== null) {
        setSelectedCampaigns([{ value: defaultCampaign.title, label: defaultCampaign.title }]);
      }
    }

    if (props.tags.length > 0) {
      setSelectedTags(props.tags.map((tag) => {
        return { value: tag.tag, label: tag.tag };
      }));
    }
  }, [props.wikiEdStaff, props.allCampaigns, props.tags]);

  const setProgramManager = () => {
    const programManager = props.wikiEdStaff.find(user => user.role === 'classroom_program_manager');
    return {
      value: programManager.username,
      label: `${programManager.username} (${programManager.realname})`
    };
  };

  const setWikiExpertOptions = () => {
    const wikiExperts = props.wikiEdStaff.filter(user => user.role === 'wikipedia_expert');
    const options = wikiExperts.map((user) => {
      return {
        value: user.username,
        label: `${user.username} (${user.realname})`
      };
    });
    return options;
  };

  const handleWikiExpertChange = (selectedOption) => {
    return setSelectedWikiExpert(selectedOption);
  };

  const handleCampaignsChange = (selectedOptions) => {
    return setSelectedCampaigns(selectedOptions);
  };

  const handleTagChange = (selectedOption) => {
    if (!selectedOption) {
      return setSelectedTags(null);
    }

    // The value includes `__isNew__: true` if it's a user-created option.
    // In that case, we need to add it to the list of options, so that it shows up as selected.
    const isNew = selectedOption.__isNew__;
    if (isNew) {
      setCreatedTagOption([selectedOption]);
    }
    setSelectedTags(selectedOption);
  };

  const handleSalesforceIdChange = (key, value) => {
    return setSalesforceId(value);
  };


  const submitWikiEdStaff = (programManager, wikiExpert) => {
    const promises = [];
    // Only add the program manager, if they are not already assigned a staff role
    if (!programManager.already_selected) {
      const programManagerUserObject = {
        username: programManager.username,
        role: STAFF_ROLE,
        role_description: null,
        real_name: programManager.realname
      };
      promises.push(props.addUser(props.course.slug, { user: programManagerUserObject }));
    }

    // Only add the selected wiki expert, if they are not already assigned a staff role
    if (!wikiExpert.already_selected) {
      const wikiExpertUserObject = {
        username: wikiExpert.username,
        role: STAFF_ROLE,
        role_description: null,
        real_name: wikiExpert.realname
      };
      promises.push(props.addUser(props.course.slug, { user: wikiExpertUserObject }));
    }
    return promises;
  };

  const submitCampaigns = () => {
    const promises = [];
    if (selectedCampaigns.length > 0) {
      selectedCampaigns.forEach((campaign) => {
        promises.push(props.addCampaign(props.course.slug, campaign.value));
      });
    }
    return promises;
  };

  const submitTags = () => {
    const oldTags = props.tags.map(tag => tag.tag);
    const currentTags = selectedTags.map(tag => tag.value);

    const newTags = difference(currentTags, oldTags); // newly added tags
    const removedTags = difference(oldTags, currentTags); // tags removed by current selection

    const promises = [];
    newTags.forEach((tag) => {
      promises.push(props.addTag(props.course.slug, tag));
    });
    removedTags.forEach((tag) => {
      promises.push(props.removeTag(props.course.slug, tag));
    });
    return promises;
  };

  // Check if entered id is valid and return it if it is valid
  const validateSalesforceId = () => {
    const rawSalesforceId = salesforceId;
    if (!rawSalesforceId) {
      // Return true as empty text field is allowed
      // Return empty array required for concatenation
      return { isValid: true, promise: [] };
    }
    const _salesforceId = extractSalesforceId(rawSalesforceId);
    if (!_salesforceId) {
      // Return false if extracted id is invalid
      return { isValid: false, promise: null };
    }
    const promise = props.linkToSalesforce(props.course.id, _salesforceId);
    return { isValid: true, promise: [promise] };
  };

  const submitApprovalForm = () => {
    setSubmitting(true);

    // Get staff user objects from selected staff user options
    const programManager = props.wikiEdStaff.find(user => user.role === 'classroom_program_manager');
    const wikiExpert = props.wikiEdStaff.find(user => user.username === selectedWikiExpert.value);

    let promises = [];
    const idValidation = validateSalesforceId();
    if (!idValidation.isValid) {
      setSubmitting(false);
      setShowInvalidIdMessage(true);
      return;
    }
    promises = promises.concat(idValidation.promise);
    promises = promises.concat(submitWikiEdStaff(programManager, wikiExpert));
    promises = promises.concat(submitCampaigns());
    promises = promises.concat(submitTags());

    Promise.all(promises).finally(() => {
      setSubmitting(false);
    });
  };

  const programManager = props.wikiEdStaff.length > 0 ? setProgramManager() : null;
  const wikiExpertOptions = props.wikiEdStaff.length > 0 ? setWikiExpertOptions() : [];

  const campaignOptions = props.allCampaigns.map((campaign) => {
      return { value: campaign.title, label: campaign.title };
  });

  const allTagOptions = uniq(props.allTags).map((tag) => {
    return { label: tag, value: tag };
  });
  const tagOptions = [...createdTagOption, ...allTagOptions];


  const programManagerSelector = (
    <div className="course-approval-field form-group">
      <div className="group-left form-group">
        <label htmlFor="program_manager">Add Program Manager:</label>
      </div>
      <div className="group-right">
        <Select
          id={'program_manager'}
          value={programManager}
          onChange={null}
          options={[]}
          simpleValue
          styles={selectStyles}
        />
      </div>
    </div>
  );

  const wikiExpertSelector = (
    <div className="course-approval-field form-group">
      <div className="group-left form-group">
        <label htmlFor="wiki_expert">Add Wikipedia Expert:</label>
      </div>
      <div className="group-right">
        <Select
          id={'wiki_expert'}
          value={wikiExpertOptions.empty ? null : selectedWikiExpert}
          onChange={handleWikiExpertChange}
          options={wikiExpertOptions}
          simpleValue
          styles={selectStyles}
        />
      </div>
    </div>
  );

  const tagsSelector = (
    <div className="course-approval-field form-group">
      <div className="group-left form-group">
        <label htmlFor="campaign">Add Tags:</label>
      </div>
      <div className="group-right">
        <CreatableSelect
          id={'tags'}
          value={selectedTags}
          placeholder={I18n.t('courses.tag_select')}
          onChange={handleTagChange}
          options={tagOptions}
          styles={selectStyles}
          isMulti={true}
          isClearable={false}
        />
      </div>
    </div>
  );

  const campaignsSelector = (
    <div className="course-approval-field form-group">
      <div className="group-left form-group">
        <label htmlFor="campaign">Add Campaigns: <span className="form-required-indicator">*</span></label>
      </div>
      <div className="group-right">
        <Select
          id={'campaign'}
          value={campaignOptions.empty ? null : selectedCampaigns}
          placeholder={I18n.t('courses.campaign_select')}
          onChange={handleCampaignsChange}
          options={campaignOptions}
          simpleValue
          styles={selectStyles}
          isMulti={true}
          isClearable={false}
        />
      </div>
    </div>
  );

  let invalidIdMessage;
  if (showInvalidIdMessage) {
    invalidIdMessage = <p className="form-group invalid">The entered Id is not valid.</p>;
  }

  const salesforceIdField = (
    <div className="course-approval-field form-group">
      <div className="group-left form-group">
        <label htmlFor="campaign">Add Salesforce Id: </label>
      </div>
      <div className="group-right">
        <TextInput
          onChange={handleSalesforceIdChange}
          value={salesforceId}
          value_key="salesforceId"
          editable={true}
          type="text"
        />
        {invalidIdMessage}
      </div>
    </div>
  );

  const approveButtonState = (selectedCampaigns === null || selectedCampaigns.length === 0) ? 'disabled' : '';
  const approveButton = (submitting) ? (
    <div className="course-approval-loader">
      <div>Approving ... </div>
      <div className="loading__spinner__small" />
    </div>
    ) : (
      <div className="controls">
        <button className={`dark button ${approveButtonState}`} onClick={submitApprovalForm}>Approve Course</button>
      </div>);

  return (
    <div className="module course-approval">
      <div className="section-header">
        <h3>Course Approval Form</h3>
        {approveButton}
      </div>
      <div className="course-approval-form">
        {programManagerSelector}
        {wikiExpertSelector}
        {tagsSelector}
        {campaignsSelector}
        {salesforceIdField}
      </div>
    </div>
  );
};

CourseApproval.propTypes = {
  course: PropTypes.object,
  fetchSpecialUsers: PropTypes.func,
  fetchAllCampaigns: PropTypes.func,
  addUser: PropTypes.func,
  addCampaign: PropTypes.func,
  allCampaigns: PropTypes.array,
  wikiEdStaff: PropTypes.array,
  tags: PropTypes.array,
  allTags: PropTypes.array,
  fetchAllTags: PropTypes.func,
  addTag: PropTypes.func,
  removeTag: PropTypes.func,
  linkToSalesforce: PropTypes.func
};

const mapStateToProps = state => ({
  course: state.course,
  wikiEdStaff: getCourseApprovalStaff(state),
  allCampaigns: state.campaigns.all_campaigns,
  tags: state.tags.tags,
  allTags: state.tags.allTags
});

const mapDispatchToProps = {
  fetchSpecialUsers,
  fetchAllCampaigns,
  fetchAllTags,
  addUser,
  addCampaign,
  addTag,
  removeTag,
  linkToSalesforce
};

export default connect(mapStateToProps, mapDispatchToProps)(CourseApproval);