diff --git a/lms/djangoapps/support/static/support/jsx/errors_list.jsx b/lms/djangoapps/support/static/support/jsx/errors_list.jsx index cc91ed45e68635d5e2eb5216273f51e23c4a0218..86da7c13a03d2b6737e53b04dc772b0adfde1c8e 100644 --- a/lms/djangoapps/support/static/support/jsx/errors_list.jsx +++ b/lms/djangoapps/support/static/support/jsx/errors_list.jsx @@ -6,25 +6,23 @@ import PropTypes from 'prop-types'; class ShowErrors extends React.Component { render() { - if (this.props.errorList.length > 0) { - window.scrollTo(0, 0); - } - return this.props.errorList.length > 0 && - <div className="col-sm-12"> + return ( + this.props.hasErrors && <div className="col-sm-12"> <div className="alert alert-danger" role="alert"> <strong>{gettext('Please fix the following errors:')}</strong> <ul> - {this.props.errorList.map((error, i) => - <li key={i}>{error}</li>, + { Object.keys(this.props.errorList).map(key => + this.props.errorList[key] && <li key={key}>{this.props.errorList[key]}</li>, )} </ul> </div> - </div>; + </div>); } } ShowErrors.propTypes = { - errorList: PropTypes.arrayOf(PropTypes.object).isRequired, + errorList: PropTypes.objectOf(PropTypes.string).isRequired, + hasErrors: PropTypes.bool.isRequired, }; export default ShowErrors; diff --git a/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx index ab88d3c94330c15f261563575075179ffae02f27..649a07c5cc72c35ac794ec6c1f57d2ecdefb2b25 100644 --- a/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx +++ b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx @@ -5,8 +5,7 @@ import PropTypes from 'prop-types'; import { Button, StatusAlert } from '@edx/paragon'; import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; - -function LoggedInUser({ userInformation, onChangeCallback, submitForm, showWarning, showDiscussionButton, reDirectUser }) { +function LoggedInUser({ userInformation, onChangeCallback, submitForm, showWarning, showDiscussionButton, reDirectUser, errorList }) { let courseElement; let detailElement; let discussionElement = ''; @@ -97,7 +96,7 @@ function LoggedInUser({ userInformation, onChangeCallback, submitForm, showWarni <div> <div className="row"> <div className="col-sm-12"> - <div className="form-group"> + <div className={`form-group ${errorList.message ? 'has-error' : ''}`}> <label htmlFor="message">{gettext('Details')}</label> <p className="message-desc">{gettext('the more quickly and helpfully we can respond!')}</p> <textarea aria-describedby="message" className="form-control" rows="7" id="message" /> @@ -138,7 +137,7 @@ function LoggedInUser({ userInformation, onChangeCallback, submitForm, showWarni <div className="row"> <div className="col-sm-12"> - <div className="form-group"> + <div className={`form-group ${errorList.subject ? 'has-error' : ''}`}> {subjectElement} </div> </div> @@ -146,7 +145,7 @@ function LoggedInUser({ userInformation, onChangeCallback, submitForm, showWarni <div className="row"> <div className="col-sm-12"> - <div className="form-group"> + <div className={`form-group ${errorList.course ? 'has-error' : ''}`}> {courseElement} </div> </div> @@ -174,6 +173,7 @@ LoggedInUser.propTypes = { }).isRequired, showWarning: PropTypes.bool.isRequired, showDiscussionButton: PropTypes.bool.isRequired, + errorList: PropTypes.objectOf(PropTypes.string).isRequired, }; export default LoggedInUser; diff --git a/lms/djangoapps/support/static/support/jsx/single_support_form.jsx b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx index 9f13404a672f22f9a0f91acf151d6e2bc13117f6..2e9ea76467e37f92a17e6b5c8db21646c5569209 100644 --- a/lms/djangoapps/support/static/support/jsx/single_support_form.jsx +++ b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx @@ -14,140 +14,158 @@ import LoggedInUser from './logged_in_user'; import LoggedOutUser from './logged_out_user'; import Success from './success'; +const initialFormErrors = { + course: undefined, + subject: undefined, + message: undefined, + request: undefined, +}; class RenderForm extends React.Component { constructor(props) { super(props); + this.submitFormUrl = this.props.context.submitFormUrl; this.userInformation = this.props.context.user; const course = this.userInformation ? this.userInformation.course_id : ''; + this.courseDiscussionURL = '/courses/{course_id}/discussion/forum'; this.state = { currentRequest: null, - errorList: [], + errorList: initialFormErrors, success: false, formData: { + course, subject: '', message: '', - course, }, }; + this.formValidationErrors = { + course: gettext('Select a course or select "Not specific to a course" for your support request.'), + subject: gettext('Select a subject for your support request.'), + message: gettext('Enter some details for your support request.'), + request: gettext('Something went wrong. Please try again later.'), + }; this.submitForm = this.submitForm.bind(this); this.reDirectUser = this.reDirectUser.bind(this); - this.setErrorState = this.setErrorState.bind(this); this.formOnChangeCallback = this.formOnChangeCallback.bind(this); - this.showWarningMessage = this.showWarningMessage.bind(this); - this.showDiscussionButton = this.showDiscussionButton.bind(this); } - setErrorState(errors) { + getFormDataFromState() { + return this.state.formData; + } + + getFormErrorsFromState() { + return this.state.errorList; + } + + clearErrorState() { + const formErrorsInState = this.getFormErrorsFromState(); + Object.keys(formErrorsInState).map((index) => { + formErrorsInState[index] = undefined; + return formErrorsInState; + }); + } + + // eslint-disable-next-line class-methods-use-this + scrollToTop() { + return window.scrollTo(0, 0); + } + + formHasErrors() { + const errorsList = this.getFormErrorsFromState(); + return Object.keys(errorsList).filter(err => errorsList[err] !== undefined).length > 0; + } + + updateErrorInState(key, error) { + const errorList = this.getFormErrorsFromState(); + errorList[key] = error; this.setState({ - errorList: errors, + errorList, }); } formOnChangeCallback(event) { - const eventTarget = event.target; - let formData = this.state.formData; - formData[eventTarget.id] = eventTarget.value; + const formData = this.getFormDataFromState(); + formData[event.target.id] = event.target.value; this.setState({ formData }); } showWarningMessage() { - return this.state.formData && this.state.formData.subject === 'Course Content'; + const formData = this.getFormDataFromState(), + selectedSubject = formData.subject; + return formData && selectedSubject === 'Course Content'; } showDiscussionButton() { - const selectCourse = this.state.formData.course; - return this.state.formData && (selectCourse !== '' && selectCourse !== 'Not specific to a course'); + const formData = this.getFormDataFromState(), + selectedCourse = formData.course; + return formData && (selectedCourse !== '' && selectedCourse !== 'Not specific to a course'); } reDirectUser(event) { event.preventDefault(); - window.location.href = `/courses/${this.state.formData.course}/discussion/forum`; + const formData = this.getFormDataFromState(); + window.location.href = this.courseDiscussionURL.replace('{course_id}', formData.course); } submitForm(event) { event.preventDefault(); - let subject, - course; - const url = this.props.context.submitFormUrl, + const formData = this.getFormDataFromState(); + this.clearErrorState(); + this.validateFormData(formData); + if (this.formHasErrors()) { + return this.scrollToTop(); + } + this.createZendeskTicket(formData); + } + + createZendeskTicket(formData) { + const url = this.submitFormUrl, request = new XMLHttpRequest(), - $course = $('#course'), - $subject = $('#subject'), data = { comment: { - body: this.state.formData.message, + body: formData.message, }, + subject: formData.subject, // Zendesk API requires 'subject' + custom_fields: [{ + id: this.props.context.customFields.course_id, + value: formData.course, + }], tags: this.props.context.tags, - }, - errors = []; - - this.clearErrors(); - - data.requester = { - email: this.userInformation.email, - name: this.userInformation.username, - }; - - course = $course.find(':selected').val(); - if (!course) { - course = $course.val(); - } - if (!course) { - $('#course').closest('.form-group').addClass('has-error'); - errors.push(gettext('Select a course or select "Not specific to a course" for your support request.')); - } - data.custom_fields = [{ - id: this.props.context.customFields.course_id, - value: course, - }]; - subject = $subject.find(':selected').val(); - if (!subject) { - subject = $subject.val(); - } - if (!subject) { - $subject.closest('.form-group').addClass('has-error'); - errors.push(gettext('Select a subject for your support request.')); - } - data.subject = subject; // Zendesk API requires 'subject' - - if (this.validateData(data, errors)) { - request.open('POST', url, true); - request.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); - request.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - - request.send(JSON.stringify(data)); - - request.onreadystatechange = function success() { - if (request.readyState === 4 && request.status === 201) { - this.setState({ - success: true, - }); - } - }.bind(this); - - request.onerror = function error() { - this.setErrorState([gettext('Something went wrong. Please try again later.')]); - }.bind(this); - } - } - - clearErrors() { - this.setErrorState([]); - $('.form-group').removeClass('has-error'); + requester: { + email: this.userInformation.email, + name: this.userInformation.username, + }, + }; + request.open('POST', url, true); + request.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); + request.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); + request.send(JSON.stringify(data)); + request.onreadystatechange = function success() { + if (request.readyState === 4 && request.status === 201) { + this.setState({ + success: true, + }); + } + }.bind(this); + + request.onerror = function error() { + this.updateErrorInState('request', this.formValidationErrors.request); + this.scrollToTop(); + }.bind(this); } - - validateData(data, errors) { - if (!data.comment.body) { - errors.push(gettext('Enter some details for your support request.')); - $('#message').closest('.form-group').addClass('has-error'); - } - - if (!errors.length) { - return true; - } - - this.setErrorState(errors); - return false; + validateFormData(formData) { + const { course, subject, message } = formData; + + let courseError, + subjectError, + messageError; + + courseError = (course === '') ? this.formValidationErrors.course : undefined; + this.updateErrorInState('course', courseError); + subjectError = (subject === '') ? this.formValidationErrors.subject : undefined; + this.updateErrorInState('subject', subjectError); + messageError = (message === '') ? this.formValidationErrors.message : undefined; + this.updateErrorInState('message', messageError); } renderSuccess() { @@ -171,6 +189,7 @@ class RenderForm extends React.Component { showWarning={this.showWarningMessage()} showDiscussionButton={this.showDiscussionButton()} reDirectUser={this.reDirectUser} + errorList={this.getFormErrorsFromState()} />); } else { userElement = (<LoggedOutUser @@ -194,9 +213,8 @@ class RenderForm extends React.Component { <h2>{gettext('Contact Us')}</h2> </div> </div> - <div className="row form-errors"> - <ShowErrors errorList={this.state.errorList} /> + <ShowErrors errorList={this.getFormErrorsFromState()} hasErrors={this.formHasErrors()} /> </div> <div className="row"> @@ -228,7 +246,6 @@ class RenderForm extends React.Component { if (this.state.success) { return this.renderSuccess(); } - return this.renderSupportForm(); } }