WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/components/overview/course_cloned_modal.jsx

Summary

Maintainability
F
5 days
Test Coverage
F
1%
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Modal from '../common/modal.jsx';
import TextInput from '../common/text_input.jsx';
import DatePicker from '../common/date_picker.jsx';
import TextAreaInput from '../common/text_area_input.jsx';
import Calendar from '../common/calendar.jsx';
import CourseUtils from '../../utils/course_utils.js';
import CourseDateUtils from '../../utils/course_date_utils.js';

const CourseClonedModal = createReactClass({
  displayName: 'CourseClonedModal',

  propTypes: {
    course: PropTypes.object.isRequired,
    initiateConfirm: PropTypes.func.isRequired,
    deleteCourse: PropTypes.func.isRequired,
    updateCourse: PropTypes.func.isRequired,
    updateClonedCourse: PropTypes.func.isRequired,
    currentUser: PropTypes.object.isRequired,
    setValid: PropTypes.func.isRequired,
    setInvalid: PropTypes.func.isRequired,
    isValid: PropTypes.bool.isRequired,
    activateValidations: PropTypes.func.isRequired,
    firstErrorMessage: PropTypes.string,
    courseCreationNotice: PropTypes.string
  },

  statics: {
    getDerivedStateFromProps(props, state) {
        return {
          tempCourseId: CourseUtils.generateTempId(state.course)
        };
    }
  },

  getInitialState() {
    return {
      course: this.props.course
    };
  },

  componentDidMount() {
    if (this.props.course.type !== 'ClassroomProgramCourse') { return; }
    this.props.addValidation('weekdays', 'Set the meeting days.');
    return this.props.addValidation('holidays', 'Mark the holidays, or check "I have no class holidays".');
  },

  componentDidUpdate(prevProps, prevState) {
    let isPersisting = prevState.isPersisting;
    if (this.props.firstErrorMessage && !prevProps.firstErrorMessage) {
      try {
        document.querySelector('.wizard').scrollTo({ top: 0, behavior: 'smooth' });
      } catch (_err) {
        // eslint-disable-next-line no-console
        console.log('scrollTo not supported');
      }

      isPersisting = false;
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({
        isPersisting
      });
    }
  },

  setAnyDatesSelected(bool) {
    return this.setState({ anyDatesSelected: bool });
  },

  setBlackoutDatesSelected(bool) {
    return this.setState({ blackoutDatesSelected: bool });
  },

  setNoBlackoutDatesChecked() {
    const { checked } = this.noDates;
    if (checked) { this.props.setValid('holidays'); }
    return this.updateCourse('no_day_exceptions', checked);
  },

  cloneCompletedStatus: 2,

  cancelCloneCourse() {
    const i18nPrefix = this.props.course.string_prefix;
    const confirmMessage = CourseUtils.i18n('creator.cancel_course_clone_confirm', i18nPrefix);
    const onConfirm = () => {
      this.props.deleteCourse(this.state.course.slug);
    };
    this.props.initiateConfirm({ confirmMessage, onConfirm });
  },

  updateCourse(valueKey, value) {
    const updatedCourse = Object.assign({}, this.state.course);
    updatedCourse[valueKey] = value;
    this.setState({
      valuesUpdated: true,
      course: updatedCourse
    });

    // Term starts out blank and must be added.
    if (valueKey === 'term') {
      this.props.setValid('exists');
    }
    // If term is already set and any slug components are changed, reset 'exists' to valid.
    if (updatedCourse.term && ['title', 'school', 'term'].includes(valueKey)) {
      this.props.setValid('exists');
    }
  },

  updateCalendar(updatedCourse) {
    if (updatedCourse.weekdays.indexOf(1) >= 0) {
      this.props.setValid('weekdays');
    }
    if (__guard__(updatedCourse.day_exceptions, x => x.length) > 0 || updatedCourse.no_day_exceptions) {
      this.props.setValid('holidays');
    }
    return this.props.updateCourse(updatedCourse);
  },

  updateCourseDates(valueKey, value) {
    const updatedCourse = CourseDateUtils.updateCourseDates(this.state.course, valueKey, value);
    return this.setState({
      dateValuesUpdated: true,
      course: updatedCourse
    });
  },

  saveCourse() {
    this.props.activateValidations();
    if (this.props.isValid) {
      this.props.setInvalid('exists', I18n.t('courses.creator.checking_for_uniqueness'), true);
      const { slug } = this.state.course;
      const updatedCourse = CourseUtils.cleanupCourseSlugComponents(this.state.course);
      updatedCourse.cloned_status = this.cloneCompletedStatus;

      const newSlug = CourseUtils.generateTempId(updatedCourse);
      updatedCourse.slug = newSlug;
      this.props.updateClonedCourse(updatedCourse, slug, newSlug);
      return this.setState({ isPersisting: true });
    }
  },

  isNewCourse(course) {
    // it's "new" if the cloned_course status comes back from the server as updated.
    return course.cloned_status === 2;
  },

  saveEnabled() {
    // You must be logged in and have permission to edit the course.
    // This will be the case if you created it (and are therefore the instructor) or if you are an admin.
    if (!this.props.currentUser.isAdvancedRole) { return false; }
    return true;
  },

  render() {
    const i18nPrefix = this.props.course.string_prefix;
    let buttonClass = 'button dark';
    buttonClass += this.state.isPersisting ? ' working' : '';

    let errorMessage;
    if (this.props.firstErrorMessage) {
      errorMessage = <div className="warning">{this.props.firstErrorMessage}</div>;
    } else if (!this.props.currentUser.id) {
      errorMessage = <div className="warning">{I18n.t('courses.please_log_in')}</div>;
    } else if (!this.props.currentUser.isAdvancedRole) {
      errorMessage = <div className="warning">{CourseUtils.i18n('not_permitted', i18nPrefix)}</div>;
    }

    let specialNotice;
    if (this.props.courseCreationNotice) {
      specialNotice = (
        <p className="timeline-warning" dangerouslySetInnerHTML={{ __html: this.props.courseCreationNotice }} />
      );
    }

    const dateProps = CourseDateUtils.dateProps(this.state.course);
    const saveDisabled = this.saveEnabled() ? '' : 'disabled';

    // Form components that are conditional on course type
    let expectedStudents;
    let courseSubject;
    let fullDates;
    let rightColumn;
    let infoIcon;
    // Specific to ClassroomProgramCourse
    if (this.props.course.type === 'ClassroomProgramCourse') {
      expectedStudents = (
        <TextInput
          id="course_expected_students"
          onChange={this.updateCourse}
          value={this.state.course.expected_students.toString()}
          value_key="expected_students"
          editable={true}
          type="number"
          label={I18n.t('courses.creator.expected_number')}
          placeholder={I18n.t('courses.creator.expected_number')}
        />
      );
      courseSubject = (
        <TextInput
          id="course_subject"
          onChange={this.updateCourse}
          value={this.state.course.subject}
          value_key="subject"
          editable={true}
          label={I18n.t('courses.creator.course_subject')}
          placeholder={I18n.t('courses.creator.subject')}
        />
      );
      fullDates = (
        <div>
          <DatePicker
            id="course_start"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.start : null}
            value_key="start"
            rerenderHoc={this.state.dateValuesUpdated}
            required={true}
            editable={true}
            label={I18n.t('courses.creator.start_date')}
            placeholder={I18n.t('courses.creator.start_date_placeholder')}
            validation={CourseDateUtils.isDateValid}
            isClearable={false}
          />
          <DatePicker
            id="course_end"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.end : null}
            value_key="end"
            rerenderHoc={this.state.dateValuesUpdated}
            required={true}
            editable={true}
            label={I18n.t('courses.creator.end_date')}
            placeholder={I18n.t('courses.creator.end_date_placeholder')}
            date_props={dateProps.end}
            validation={CourseDateUtils.isDateValid}
            enabled={Boolean(this.state.course.start)}
            isClearable={false}
          />
          <DatePicker
            id="timeline_start"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.timeline_start : null}
            value_key="timeline_start"
            rerenderHoc={this.state.dateValuesUpdated}
            editable={true}
            label={I18n.t('courses.creator.assignment_start')}
            placeholder={I18n.t('courses.creator.assignment_start_placeholder')}
            date_props={dateProps.timeline_start}
            validation={CourseDateUtils.isDateValid}
            enabled={Boolean(this.state.course.start)}
            isClearable={false}
          />
          <DatePicker
            id="timeline_end"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.timeline_end : null}
            value_key="timeline_end"
            rerenderHoc={this.state.dateValuesUpdated}
            editable={true}
            label={I18n.t('courses.creator.assignment_end')}
            placeholder={I18n.t('courses.creator.assignment_end_placeholder')}
            date_props={dateProps.timeline_end}
            validation={CourseDateUtils.isDateValid}
            enabled={Boolean(this.state.course.start)}
            isClearable={false}
          />
        </div>
      );
      rightColumn = (
        <div className="column">
          <Calendar
            course={this.state.course}
            editable={true}
            setAnyDatesSelected={this.setAnyDatesSelected}
            setBlackoutDatesSelected={this.setBlackoutDatesSelected}
            shouldShowSteps={false}
            calendarInstructions={I18n.t('courses.creator.cloned_course_calendar_instructions')}
            updateCourse={this.updateCalendar}
          />
          <label> {I18n.t('courses.creator.no_class_holidays')}
            <input id="no_holidays" type="checkbox" onChange={this.setNoBlackoutDatesChecked} ref={(checkbox) => { this.noDates = checkbox; }} />
          </label>
        </div>
      );
    // Specific to non-ClassroomProgramCourse
    } else {
      infoIcon = (
        <div className="tooltip-trigger">
          <img src ="/assets/images/info.svg" alt = "tooltip default logo" />
          <div className="tooltip large dark">
            <p>
              {CourseUtils.i18n('creator.course_when', i18nPrefix)}
            </p>
          </div>
        </div>
      );
      rightColumn = (
        <div className="column">
          <DatePicker
            id="course_start"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.start : null}
            value_key="start"
            required={true}
            editable={true}
            label={CourseUtils.i18n('creator.start_date', i18nPrefix)}
            placeholder={I18n.t('courses.creator.start_date_placeholder')}
            validation={CourseDateUtils.isDateValid}
            isClearable={false}
            showTime={true}
          />
          <DatePicker
            id="course_end"
            onChange={this.updateCourseDates}
            value={this.state.dateValuesUpdated ? this.state.course.end : null}
            value_key="end"
            required={true}
            editable={true}
            label={CourseUtils.i18n('creator.end_date', i18nPrefix)}
            placeholder={I18n.t('courses.creator.end_date_placeholder')}
            date_props={dateProps.end}
            validation={CourseDateUtils.isDateValid}
            enabled={Boolean(this.state.course.start)}
            isClearable={false}
            showTime={true}
          />
          <p className="form-help-text">
            {I18n.t('courses.time_zone_message')}
          </p>
        </div>
      );
    }
    const termInput = (
      <div className="terminput">
        <TextInput
          id="course_term"
          onChange={this.updateCourse}
          value={this.state.course.term}
          value_key="term"
          required={true}
          validation={CourseUtils.courseSlugRegex()}
          editable={true}
          label={CourseUtils.i18n('creator.course_term', i18nPrefix)}
          placeholder={CourseUtils.i18n('creator.course_term_placeholder', i18nPrefix)}
        />
        {infoIcon}
      </div>
    );

    return (
      <Modal>
        <div className="container">
          <div className="wizard__panel active cloned-course">
            {specialNotice}
            <h3 id="clone_modal_header">{CourseUtils.i18n('creator.clone_successful', i18nPrefix)}</h3>
            <p>{CourseUtils.i18n('creator.clone_successful_details', i18nPrefix)}</p>
            {errorMessage}
            <div className="wizard__form">
              <div className="column" id="details_column">
                <TextInput
                  id="course_title"
                  onChange={this.updateCourse}
                  value={this.state.course.title}
                  value_key="title"
                  required={true}
                  validation={CourseUtils.courseSlugRegex()}
                  editable={true}
                  label={CourseUtils.i18n('creator.course_title', i18nPrefix)}
                  placeholder={CourseUtils.i18n('title', i18nPrefix)}
                />
                <TextInput
                  id="course_school"
                  onChange={this.updateCourse}
                  value={this.state.course.school}
                  value_key="school"
                  required={true}
                  validation={CourseUtils.courseSlugRegex()}
                  editable={true}
                  label={CourseUtils.i18n('creator.course_school', i18nPrefix)}
                  placeholder={CourseUtils.i18n('school', i18nPrefix)}
                />
                {termInput}
                {courseSubject}
                {expectedStudents}
                {fullDates}
                <TextAreaInput
                  id="course_description"
                  onChange={this.updateCourse}
                  value={this.state.course.description}
                  value_key="description"
                  editable={true}
                  placeholder={CourseUtils.i18n('creator.course_description', i18nPrefix)}
                />
              </div>
              {rightColumn}
              <button onClick={this.cancelCloneCourse} className="button light">{CourseUtils.i18n('creator.cancel_course_clone', i18nPrefix)}</button>
              <button onClick={this.saveCourse} disabled={saveDisabled} className={buttonClass}>{CourseUtils.i18n('creator.save_cloned_course', i18nPrefix)}</button>
            </div>
          </div>
        </div>
      </Modal>
    );
  }
});

export default CourseClonedModal;

function __guard__(value, transform) {
  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}