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 = (
-
-
- );
- let showCheckbox;
- if (this.props.assignmentsWithoutUsers.length > 0) {
- showCheckbox = true;
+ const showCloneChooserHandler = () => {
+ props.fetchAssignments(props.cloneableCourses[0].slug);
+ setShowCloneChooser(true);
+ };
+
+ const cancelClone = () => {
+ setShowCloneChooser(false);
+ };
+
+ const chooseNewCourseHandler = () => {
+ if (Features.wikiEd) {
+ setShowCourseForm(true);
} else {
- showCheckbox = false;
+ setShowWizardForm(true);
}
- const checkBoxLabel = (
-
-
-
-
- );
+ };
+
+ 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 };