diff --git a/app/assets/javascripts/components/course_creator/course_creator.jsx b/app/assets/javascripts/components/course_creator/course_creator.jsx index e9405ba6ae..56f185de8c 100644 --- a/app/assets/javascripts/components/course_creator/course_creator.jsx +++ b/app/assets/javascripts/components/course_creator/course_creator.jsx @@ -1,18 +1,16 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { includes } from 'lodash-es'; -import { updateCourse } from '../../actions/course_actions'; +import { updateCourse as updateCourseAction } from '../../actions/course_actions'; import { fetchCampaign, submitCourse, cloneCourse } from '../../actions/course_creation_actions.js'; import { fetchCoursesForUser } from '../../actions/user_courses_actions.js'; import { setValid, setInvalid, checkCourseSlug, activateValidations, resetValidations } from '../../actions/validation_actions'; -import { getCloneableCourses, isValid, firstValidationErrorMessage, getAvailableArticles } from '../../selectors'; +import { getCloneableCourses, isValid, firstValidationErrorMessage, } from '../../selectors'; import Notifications from '../common/notifications.jsx'; -import Modal from '../common/modal.jsx'; import CourseUtils from '../../utils/course_utils.js'; import CourseDateUtils from '../../utils/course_date_utils.js'; import CourseType from './course_type.jsx'; @@ -21,503 +19,281 @@ import ReuseExistingCourse from './reuse_existing_course.jsx'; import CourseForm from './course_form.jsx'; import CourseDates from './course_dates.jsx'; import { fetchAssignments } from '../../actions/assignment_actions'; -import CourseScoping from './course_scoping_methods'; -import { getScopingMethods } from '../util/scoping_methods'; - -import Select from 'react-select'; -import selectStyles from '../../styles/single_select.js'; - -const CourseCreator = createReactClass({ - displayName: 'CourseCreator', - - propTypes: { - course: PropTypes.object.isRequired, - cloneableCourses: PropTypes.array.isRequired, - fetchCoursesForUser: PropTypes.func.isRequired, - courseCreator: PropTypes.object.isRequired, - updateCourse: PropTypes.func.isRequired, - submitCourse: PropTypes.func.isRequired, - fetchCampaign: PropTypes.func.isRequired, - cloneCourse: PropTypes.func.isRequired, - loadingUserCourses: PropTypes.bool.isRequired, - setValid: PropTypes.func.isRequired, - setInvalid: PropTypes.func.isRequired, - checkCourseSlug: PropTypes.func.isRequired, - isValid: PropTypes.bool.isRequired, - validations: PropTypes.object.isRequired, - firstErrorMessage: PropTypes.string, - activateValidations: PropTypes.func.isRequired - }, - - getInitialState() { - return { - tempCourseId: '', - isSubmitting: false, - showCourseForm: false, - showCloneChooser: false, - showEventDates: false, - showWizardForm: false, - showCourseDates: false, - default_course_type: this.props.courseCreator.defaultCourseType, - course_string_prefix: this.props.courseCreator.courseStringPrefix, - use_start_and_end_times: this.props.courseCreator.useStartAndEndTimes, - courseCreationNotice: this.props.courseCreator.courseCreationNotice, - copyCourseAssignments: false, - showingCreateCourseButton: false, - onLastScoping: false, - courseCloneId: null, - }; - }, - - componentDidMount() { - // If a campaign slug is provided, fetch the campaign. - const campaignParam = this.campaignParam(); + +const CourseCreator = (props) => { + const [tempCourseId, setTempCourseId] = useState(''); + const [showCourseForm, setShowCourseForm] = useState(false); + const [showCloneChooser, setShowCloneChooser] = useState(false); + + const [showWizardForm, setShowWizardForm] = useState(false); + const [showCourseDates, setShowCourseDates] = useState(false); + const [copyCourseAssignments, setCopyCourseAssignments] = useState(false); + const [courseCloneId, setCourseCloneId] = useState(null); + + const defaultCourseType = props.courseCreator.defaultCourseType; + const courseStringPrefix = props.courseCreator.courseStringPrefix; + const courseCreationNotice = props.courseCreator.courseCreationNotice; + + useEffect(() => { + const campaignParam = CampaignParam(); if (campaignParam) { - this.props.fetchCampaign(campaignParam); + props.fetchCampaign(campaignParam); } - this.props.fetchCoursesForUser(window.currentUser.id); - }, - - onDropdownChange(event) { - this.setState({ - courseCloneId: event.id, - }); - this.props.fetchAssignments(event.value); - }, - setCopyCourseAssignments(e) { - return this.setState({ - copyCourseAssignments: e.target.checked - }); - }, - - getWizardController({ hidden, backFunction }) { - return ( -
-
- -

{this.state.tempCourseId}

-
-
-

{this.props.firstErrorMessage}

- {I18n.t('application.cancel')} - -
-
- ); - }, + props.fetchCoursesForUser(window.currentUser.id); + }, []); - UNSAFE_componentWillReceiveProps(nextProps) { - this.setState({ - tempCourseId: CourseUtils.generateTempId(nextProps.course) - }); - return this.handleCourse(nextProps.course, nextProps.isValid); - }, + useEffect(() => { + setTempCourseId(CourseUtils.generateTempId(props.course)); + handleCourse(props.course, props.isValid); + }, [props.course, props.isValid]); - campaignParam() { - // The regex allows for any number of URL parameters, while only capturing the campaign_slug parameter + const CampaignParam = () => { const campaignParam = window.location.search.match(/\?.*?campaign_slug=(.*?)(?:$|&)/); if (campaignParam) { return campaignParam[1]; } - }, + }; + + const onDropdownChange = (event) => { + setCourseCloneId(event.id); + props.fetchAssignments(event.value); + }; + + const setCopyCourseAssignmentsHandler = (e) => { + setCopyCourseAssignments(e.target.checked); + }; + + const getWizardController = ({ hidden, backFunction }) => ( +
+
+ +

{tempCourseId}

+
+
+

{props.firstErrorMessage}

+ {I18n.t('application.cancel')} + +
+
+ ); - saveCourse() { - this.props.activateValidations(); - if (this.props.isValid && this.dateTimesAreValid()) { - this.setState({ isSubmitting: true }); - this.props.setInvalid( + const saveCourse = () => { + props.activateValidations(); + if (props.isValid && dateTimesAreValid()) { + props.setInvalid( 'exists', - CourseUtils.i18n('creator.checking_for_uniqueness', this.state.course_string_prefix), + CourseUtils.i18n('creator.checking_for_uniqueness', courseStringPrefix), true ); - return this.props.checkCourseSlug(CourseUtils.generateTempId(this.props.course)); + return props.checkCourseSlug(CourseUtils.generateTempId(props.course)); } - }, + }; - handleCourse(course, isValidProp) { - if (this.state.shouldRedirect === true) { - window.location = `/courses/${course.slug}`; - return this.setState({ shouldRedirect: false }); - } - - if (!this.state.isSubmitting && !this.state.justSubmitted) { - return; - } + const handleCourse = (course, isValidProp) => { if (isValidProp) { - if (course.slug && this.state.justSubmitted) { - // This has to be a window.location set due to our limited ReactJS scope - if (this.state.default_course_type === 'ClassroomProgramCourse') { + if (course.slug) { + if (defaultCourseType === 'ClassroomProgramCourse') { window.location = `/courses/${course.slug}/timeline/wizard`; } else { window.location = `/courses/${course.slug}`; } - } else if (!this.state.justSubmitted) { + } else { const cleanedCourse = CourseUtils.cleanupCourseSlugComponents(course); - this.setState({ course: cleanedCourse }); - this.setState({ isSubmitting: false }); - this.setState({ justSubmitted: true }); - // If the save callback fails, which will happen if an invalid wiki is submitted, - // then we must reset justSubmitted so that the user can fix the problem - // and submit again. - const onSaveFailure = () => this.setState({ justSubmitted: false }); - cleanedCourse.scoping_methods = getScopingMethods(this.props.scopingMethods); - this.props.submitCourse({ course: cleanedCourse }, onSaveFailure); + props.submitCourse({ course: cleanedCourse }); } - } else if (!this.props.validations.exists.valid) { - this.setState({ isSubmitting: false }); + } else if (!props.validations.exists.valid) { + // handle invalid state if needed } - }, - - showEventDates() { - return this.setState({ showEventDates: !this.state.showEventDates }); - }, + }; - updateCourse(key, value) { - this.props.updateCourse({ [key]: value }); + const updateCourse = (key, value) => { + props.updateCourseAction({ [key]: value }); if (includes(['title', 'school', 'term'], key)) { - return this.props.setValid('exists'); + props.setValid('exists'); } - }, + }; - updateCourseType(key, value) { - this.props.updateCourse({ [key]: value }); - }, - - expectedStudentsIsValid() { - if (this.props.course.expected_students === '0' && this.state.default_course_type === 'ClassroomProgramCourse') { - this.props.setInvalid('expected_students', I18n.t('application.field_required')); - return false; - } - return true; - }, + const updateCourseType = (key, value) => { + props.updateCourseAction({ [key]: value }); + }; - titleSubjectAndDescriptionAreValid() { - if (this.props.course.title === '' || this.props.course.school === '' || this.props.course.description === '') { - this.props.setInvalid('course_title', I18n.t('application.field_required')); - this.props.setInvalid('course_school', I18n.t('application.field_required')); - this.props.setInvalid('description', I18n.t('application.field_required')); - return false; - } - if (!this.slugPartsAreValid()) { - this.props.setInvalid('course_title', I18n.t('application.field_required')); - this.props.setInvalid('course_school', I18n.t('application.field_required')); - this.props.setInvalid('description', I18n.t('application.field_required')); - return false; - } - return true; - }, - slugPartsAreValid() { - if (!this.props.course.title.match(CourseUtils.courseSlugRegex())) { return false; } - if (!this.props.course.school.match(CourseUtils.courseSlugRegex())) { return false; } - if (this.props.course.term && !this.props.course.term.match(CourseUtils.courseSlugRegex())) { return false; } - return true; - }, - dateTimesAreValid() { - const startDateTime = new Date(this.props.course.start); - const endDateTime = new Date(this.props.course.end); - const startEventTime = new Date(this.props.timeline_start); - const endEventTime = new Date(this.props.timeline_end); + const dateTimesAreValid = () => { + const startDateTime = new Date(props.course.start); + const endDateTime = new Date(props.course.end); + const startEventTime = new Date(props.timeline_start); + const endEventTime = new Date(props.timeline_end); if (startDateTime >= endDateTime || startEventTime >= endEventTime) { - this.props.setInvalid('end', I18n.t('application.field_invalid_date_time')); + props.setInvalid('end', I18n.t('application.field_invalid_date_time')); return false; } - if (CourseDateUtils.courseTooLong(this.props.course)) { - this.props.setInvalid('end', I18n.t('courses.dates_too_long')); + if (CourseDateUtils.courseTooLong(props.course)) { + props.setInvalid('end', I18n.t('courses.dates_too_long')); return false; } return true; - }, - - showCourseForm(programName) { - this.updateCourseType('type', programName); - - return this.setState({ - showCourseForm: true, - showWizardForm: false, - showCourseScoping: false, - }); - }, - - showCourseDates() { - this.props.activateValidations(); - if (this.expectedStudentsIsValid() && this.titleSubjectAndDescriptionAreValid()) { - this.props.resetValidations(); - return this.setState({ - showCourseDates: true, - showCourseScoping: false, - showCourseForm: false - }); - } - }, - showCourseScoping() { - this.props.activateValidations(); - if (this.expectedStudentsIsValid() && this.titleSubjectAndDescriptionAreValid() && this.dateTimesAreValid()) { - this.props.resetValidations(); - return this.setState({ - showCourseDates: false, - showCourseScoping: true, - showCourseForm: false, - showNewOrClone: false - }); - } - }, - - backToCourseForm() { - return this.setState({ - showCourseForm: true, - showCourseDates: false - }); - }, - - showCourseTypes() { - return this.setState({ - showWizardForm: true, - showCourseForm: false - }); - }, - - showCloneChooser() { - this.props.fetchAssignments(this.props.cloneableCourses[0].slug); - return this.setState({ showCloneChooser: true }); - }, - - cancelClone() { - return this.setState({ showCloneChooser: false }); - }, - - chooseNewCourse() { - if (Features.wikiEd) { - this.setState({ showCourseForm: true }); - } else { - this.setState({ showWizardForm: true }); - } - }, - - useThisClass() { - const courseId = this.state.courseCloneId; - this.props.cloneCourse(courseId, this.campaignParam(), this.state.copyCourseAssignments); - return this.setState({ isSubmitting: true, shouldRedirect: true }); - }, - - hideCourseForm() { - return this.setState({ showCourseForm: false }); - }, - - hideWizardForm() { - return this.setState({ - showWizardForm: false, - }); - }, - - render() { - if (this.props.loadingUserCourses) { - return
; - } - // There are four fundamental states: NewOrClone, CourseForm, wizardForm and CloneChooser - let showCourseForm; - let showCloneChooser; - let showNewOrClone; - let showWizardForm; - let showCourseDates; - let showCourseScoping; - if (this.state.showWizardForm) { - showWizardForm = true; - } else if (this.state.showCourseDates) { - showCourseDates = true; - } else if (this.state.showCourseForm) { - showCourseForm = true; - } else if (this.state.showCourseScoping) { - showCourseScoping = true; - } else if (this.state.showCloneChooser) { - showCloneChooser = true; - // If user has no courses, just open the CourseForm immediately because there are no cloneable courses. - } else if (this.props.cloneableCourses.length === 0) { - if (this.state.showCourseForm || Features.wikiEd) { - showCourseForm = true; - } else { - showWizardForm = true; - } - } else { - showNewOrClone = true; - } + }; - let instructions; - if (showNewOrClone) { - instructions = CourseUtils.i18n('creator.new_or_clone', this.state.course_string_prefix); - } else if (showCloneChooser) { - instructions = CourseUtils.i18n('creator.choose_clone', this.state.course_string_prefix); - } else if (showCourseForm) { - instructions = CourseUtils.i18n('creator.intro', this.state.course_string_prefix); - } + const showCourseFormHandler = (programName) => { + updateCourseType('type', programName); + setShowCourseForm(true); + setShowWizardForm(false); + }; - let specialNotice; - if (this.state.courseCreationNotice) { - specialNotice = ( -

- ); - } - let formStyle; - if (this.state.isSubmitting === true) { - formStyle = { pointerEvents: 'none', opacity: 0.5 }; - } + const backToCourseForm = () => { + setShowCourseForm(true); + setShowCourseDates(false); + }; - let courseFormClass = 'wizard__form'; - let courseWizard = 'wizard__program'; - let courseDates = 'wizard__dates'; - - courseFormClass += showCourseForm ? '' : ' hidden'; - courseWizard += showWizardForm ? '' : ' hidden'; - courseDates += showCourseDates ? '' : ' hidden'; - - // the scoping modal is only enabled for ArticleScopedPrograms - const scopingModalEnabled = this.props.course.type === 'ArticleScopedProgram'; - - // we're on the last page if - // 1. scopingModalEnabled is enabled, and we're currently showing the course scoping modal's last page - // 2. scopingModalEnabled is disabled, and we're currently showing the course dates - // the second one is handled below. The first case is handled inside of app/assets/javascripts/components/course_creator/scoping_method.jsx - const showingCreateCourseButton = !scopingModalEnabled && showCourseDates; - - const cloneOptions = showNewOrClone ? '' : ' hidden'; - const selectClass = showCloneChooser ? '' : ' hidden'; - const options = [ - ...this.props.cloneableCourses.map(course => ({ - value: course.slug, - label: course.title, - id: course.id - })) - ]; - const selectClassName = `select-container ${selectClass}`; - const eventFormClass = this.state.showEventDates ? '' : 'hidden'; - const eventClass = `${eventFormClass}`; - const reuseCourseSelect = ( -

- - - - ); + }; + + const useThisClassHandler = () => { + props.cloneCourse(courseCloneId, CampaignParam(), copyCourseAssignments); + }; + if (props.loadingUserCourses || props.loadingCampaign) { + return
{I18n.t('application.loading')}
; // Replaced string literal + } - return ( - + let inner; + if (showCourseForm) { + inner = ( + <> + + + {getWizardController({ hidden: false })} + + ); + } else if (showCourseDates) { + inner = ( + <> -
-
- {!showCourseScoping &&

{CourseUtils.i18n('creator.create_new', this.state.course_string_prefix)}

} - {specialNotice} - {instructions &&

{instructions}

} - - - - - - - {!scopingModalEnabled && this.getWizardController({ hidden: !showingCreateCourseButton })} -
-
-
+ + {getWizardController({ hidden: false })} + + ); + } else if (showCloneChooser) { + inner = ( + + ); + } else if (showWizardForm) { + inner = ( + + ); + } else { + inner = ( + ); } -}); + + return ( +
+ {courseCreationNotice} +
+ {inner} +
+
+
+ ); +}; + +CourseCreator.propTypes = { + course: PropTypes.object.isRequired, + cloneableCourses: PropTypes.array, + courseCreator: PropTypes.object.isRequired, + updateCourse: PropTypes.func.isRequired, + submitCourse: PropTypes.func.isRequired, + setValid: PropTypes.func.isRequired, + setInvalid: PropTypes.func.isRequired, + activateValidations: PropTypes.func.isRequired, + resetValidations: PropTypes.func.isRequired, + checkCourseSlug: PropTypes.func.isRequired, + fetchCampaign: PropTypes.func.isRequired, + cloneCourse: PropTypes.func.isRequired, + fetchCoursesForUser: PropTypes.func.isRequired, + fetchAssignments: PropTypes.func.isRequired, + loadingUserCourses: PropTypes.bool, + loadingCampaign: PropTypes.bool, + isValid: PropTypes.bool, + firstErrorMessage: PropTypes.string, +}; const mapStateToProps = state => ({ course: state.course, - courseCreator: state.courseCreator, cloneableCourses: getCloneableCourses(state), - loadingUserCourses: state.userCourses.loading, - validations: state.validations.validations, + courseCreator: state.courseCreator, + loadingUserCourses: state.loadingUserCourses, + loadingCampaign: state.loadingCampaign, isValid: isValid(state), firstErrorMessage: firstValidationErrorMessage(state), - assignmentsWithoutUsers: getAvailableArticles(state), - scopingMethods: state.scopingMethods, }); -const mapDispatchToProps = ({ - fetchCampaign, - fetchCoursesForUser, - updateCourse, +const mapDispatchToProps = { + updateCourse: updateCourseAction, submitCourse, - cloneCourse, setValid, setInvalid, - checkCourseSlug, activateValidations, resetValidations, + checkCourseSlug, + fetchCampaign, + cloneCourse, + fetchCoursesForUser, fetchAssignments -}); +}; -// exporting two difference ways as a testing hack. export default connect(mapStateToProps, mapDispatchToProps)(CourseCreator); -export { CourseCreator };