From d839d177141541a4d131a28eca087b29844dbed7 Mon Sep 17 00:00:00 2001 From: tasawernawaz Date: Wed, 11 Oct 2017 13:16:11 +0500 Subject: [PATCH] update css and html/ add basics for react LEARNER-2804 --- .../support/static/support/jsx/.eslintrc.js | 10 + .../static/support/jsx/errors_list.jsx | 28 +++ .../static/support/jsx/file_upload.jsx | 170 ++++++++++++++ .../static/support/jsx/logged_in_user.jsx | 49 ++++ .../static/support/jsx/logged_out_user.jsx | 48 ++++ .../support/jsx/single_support_form.jsx | 211 ++++++++++++++++++ .../static/support/jsx/upload_progress.jsx | 41 ++++ lms/djangoapps/support/tests/test_views.py | 43 ---- lms/static/sass/views/_support.scss | 27 ++- lms/templates/support/contact_us.html | 157 +++---------- webpack.config.js | 3 + 11 files changed, 611 insertions(+), 176 deletions(-) create mode 100644 lms/djangoapps/support/static/support/jsx/.eslintrc.js create mode 100644 lms/djangoapps/support/static/support/jsx/errors_list.jsx create mode 100644 lms/djangoapps/support/static/support/jsx/file_upload.jsx create mode 100644 lms/djangoapps/support/static/support/jsx/logged_in_user.jsx create mode 100644 lms/djangoapps/support/static/support/jsx/logged_out_user.jsx create mode 100644 lms/djangoapps/support/static/support/jsx/single_support_form.jsx create mode 100644 lms/djangoapps/support/static/support/jsx/upload_progress.jsx diff --git a/lms/djangoapps/support/static/support/jsx/.eslintrc.js b/lms/djangoapps/support/static/support/jsx/.eslintrc.js new file mode 100644 index 0000000000..2c5f18759e --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + extends: 'eslint-config-edx', + root: true, + settings: { + 'import/resolver': 'webpack', + }, + rules: { + 'import/prefer-default-export': 'off', + }, +}; diff --git a/lms/djangoapps/support/static/support/jsx/errors_list.jsx b/lms/djangoapps/support/static/support/jsx/errors_list.jsx new file mode 100644 index 0000000000..f55dad0077 --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/errors_list.jsx @@ -0,0 +1,28 @@ +/* eslint react/no-array-index-key: 0 */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +class ShowErrors extends React.Component { + + render() { + window.scrollTo(0, 0); + return this.props.errorList.length > 0 && +
+
+ {gettext('Please fix the following errors:')} +
    + {this.props.errorList.map(error => +
  • {error}
  • , + )} +
+
+
; + } +} + +ShowErrors.propTypes = { + errorList: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default ShowErrors; diff --git a/lms/djangoapps/support/static/support/jsx/file_upload.jsx b/lms/djangoapps/support/static/support/jsx/file_upload.jsx new file mode 100644 index 0000000000..f2160f70e9 --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/file_upload.jsx @@ -0,0 +1,170 @@ +/* global gettext */ +/* eslint one-var: ["error", "always"] */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import ShowProgress from './upload_progress'; + + +class FileUpload extends React.Component { + constructor(props) { + super(props); + + this.uploadFile = this.uploadFile.bind(this); + this.removeFile = this.removeFile.bind(this); + this.state = { + fileList: [], + fileInProgress: null, + }; + } + + removeFile(e) { + e.preventDefault(); + const fileToken = e.target.id, + $this = this, + url = `https://arbisoft.zendesk.com/api/v2/uploads/${fileToken}.json`, + accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da', + request = new XMLHttpRequest(); + + request.open('DELETE', url, true); + request.setRequestHeader('Authorization', `Bearer ${accessToken}`); + request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + + request.send(); + + request.onreadystatechange = function removeFile() { + if (request.readyState === 4 && request.status === 204) { + $this.setState({ + fileList: $this.state.fileList.filter(file => file.fileToken !== fileToken), + }); + } + }; + } + + uploadFile(e) { + const url = 'https://arbisoft.zendesk.com/api/v2/uploads.json?filename=', + fileReader = new FileReader(), + request = new XMLHttpRequest(), + errorList = [], + $this = this, + file = e.target.files[0], + accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da', + maxFileSize = 5000000, // 5mb is max limit + allowedFileTypes = ['gif', 'png', 'jpg', 'jpeg', 'pdf']; + + // remove file from input and upload it to zendesk after validation + $(e.target).val(''); + + if (file.size > maxFileSize) { + errorList.push(gettext('Files that you upload must be smaller than 5MB in size.')); + } else if ($.inArray(file.name.split('.').pop().toLowerCase(), allowedFileTypes) === -1) { + errorList.push(gettext('Files that you upload must be PDFs or image files in .gif, .jpg, .jpeg, or .png format.')); + } + + this.props.setErrorState(errorList); + if (errorList.length > 0) { + return; + } + + request.open('POST', (url + file.name), true); + request.setRequestHeader('Authorization', `Bearer ${accessToken}`); + request.setRequestHeader('Content-Type', 'application/binary'); + + fileReader.readAsArrayBuffer(file); + + fileReader.onloadend = function success() { + $this.setState({ + fileInProgress: file.name, + currentRequest: request, + }); + request.send(fileReader.result); + }; + + request.upload.onprogress = function renderProgress(event) { + if (event.lengthComputable) { + const percentComplete = (event.loaded / event.total) * 100; + $('.progress-bar-striped').css({ width: `${percentComplete}%` }); + } + }; + + request.onreadystatechange = function success() { + if (request.readyState === 4 && request.status === 201) { + const uploadedFile = { + fileName: file.name, + fileToken: JSON.parse(request.response).upload.token, + }; + + $this.setState( + { + fileList: $this.state.fileList.concat(uploadedFile), + fileInProgress: null, + }, + ); + } + }; + + request.onerror = function error() { + $this.setState({ + fileInProgress: null, + errorList: [gettext('Something went wrong. Please try again later.')], + }); + }; + + request.onabort = function abortUpload() { + $this.setState({ + fileInProgress: null, + }); + }; + } + + render() { + return ( +
+
+
+
+ + +
+
+
+
+ {this.state.fileInProgress && + + } +
+
+ { + this.state.fileList.map(file => + (
+
+ {file.fileName} + + + +
+
), + ) + } +
+
+ ); + } +} + +FileUpload.propTypes = { + setErrorState: PropTypes.func.isRequired, +}; +export default FileUpload; diff --git a/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx new file mode 100644 index 0000000000..7cd223476c --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx @@ -0,0 +1,49 @@ +/* global gettext */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +function LoggedInUser({ userInformation }) { + return (
+
+
+

{gettext(`What can we help you with, ${userInformation.username}?`)}

+
+
+ +
+
+
+ {userInformation.enrollments.length === 0 && +
+ + +
+ } + {userInformation.enrollments.length > 0 && +
+ + +
+ } +
+
+
+
); +} + +LoggedInUser.propTypes = { + userInformation: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default LoggedInUser; diff --git a/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx b/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx new file mode 100644 index 0000000000..84cce2fa71 --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx @@ -0,0 +1,48 @@ +/* global gettext */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +function LoggedOutUser({ loginUrl }) { + return ( +
+
+
+

{gettext('Sign in to edX so we can help you better.')}

+
+
+ +
+
+ {gettext('Sign in')} +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ ); +} + +LoggedOutUser.propTypes = { + loginUrl: PropTypes.string.isRequired, +}; + +export default LoggedOutUser; diff --git a/lms/djangoapps/support/static/support/jsx/single_support_form.jsx b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx new file mode 100644 index 0000000000..a738eb9310 --- /dev/null +++ b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx @@ -0,0 +1,211 @@ +/* global gettext */ +/* eslint one-var: ["error", "always"] */ +/* eslint no-alert: "error" */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import FileUpload from './file_upload'; +import ShowErrors from './errors_list'; +import LoggedInUser from './logged_in_user'; +import LoggedOutUser from './logged_out_user'; + +// TODO +// edx zendesk APIs +// access token +// custom fields ids +// https://openedx.atlassian.net/browse/LEARNER-2736 +// https://openedx.atlassian.net/browse/LEARNER-2735 + +class RenderForm extends React.Component { + constructor(props) { + super(props); + this.state = { + currentRequest: null, + errorList: [], + }; + this.submitForm = this.submitForm.bind(this); + this.setErrorState = this.setErrorState.bind(this); + } + + setErrorState(errors) { + this.setState({ + errorList: errors, + }); + } + + submitForm() { + const url = 'https://arbisoft.zendesk.com/api/v2/tickets.json', + $userInfo = $('.user-info'), + request = new XMLHttpRequest(), + $course = $('#course'), + accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da', + data = { + subject: $('#subject').val(), + comment: { + body: $('#message').val(), + uploads: $.map($('.uploaded-files button'), n => n.id), + }, + }; + + let course; + + if ($userInfo.length) { + data.requester = $userInfo.data('email'); + course = $course.find(':selected').text(); + if (!course.length) { + course = $course.val(); + } + } else { + data.requester = $('#email').val(); + course = $course.val(); + } + + data.custom_fields = [{ + id: '114099484092', + value: course, + }]; + + if (this.validateData(data)) { + request.open('POST', url, true); + request.setRequestHeader('Authorization', `Bearer ${accessToken}`); + request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + + request.send(JSON.stringify({ + ticket: data, + })); + + request.onreadystatechange = function success() { + if (request.readyState === 4 && request.status === 201) { + // TODO needs to remove after implementing success page + const alert = 'Request submitted successfully.'; + alert(); + } + }; + + request.onerror = function error() { + this.setErrorState([gettext('Something went wrong. Please try again later.')]); + }.bind(this); + } + } + + validateData(data) { + const errors = [], + regex = /^([a-zA-Z0-9_.+-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; + + if (!data.requester) { + errors.push(gettext('Enter a valid email address.')); + $('#email').closest('.form-group').addClass('has-error'); + } else if (!regex.test(data.requester)) { + errors.push(gettext('Enter a valid email address.')); + $('#email').closest('.form-group').addClass('has-error'); + } + if (!data.subject) { + errors.push(gettext('Enter a subject for your support request.')); + $('#subject').closest('.form-group').addClass('has-error'); + } + 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; + } + + render() { + let userElement; + if (this.props.context.user) { + userElement = ; + } else { + userElement = ; + } + + return ( +
+ +
+
+

{gettext('Contact Us')}

+
+
+ +
+ +
+ +
+
+

{gettext('Your question might have already been answered.')}

+
+
+ +
+ +
+ + {userElement} + +
+
+
+ + +
+
+
+ +
+
+
+ +

{gettext('The more you tell us, the more quickly and helpfully we can respond!')}

+ -
-
-
- -
-
-
- - -
-
-
- -
-
-

${_("1 file uploaded:")}

-
-
- -
-
- my_image1.png - ${_("Remove file")} -
-
- -
-
- my_image2.png - ${_("Cancel upload")} - -
-
-
- -
-
- -
-
- -
-
- -
+ new SingleSupportForm(context); + diff --git a/webpack.config.js b/webpack.config.js index 55a5d7bd79..35e89d66db 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -28,6 +28,9 @@ var wpconfig = { Import: './cms/static/js/features/import/factories/import.js', StudioIndex: './cms/static/js/features_jsx/studio/index.jsx', + // LMS: single support form + SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx', + // Features CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js', CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',