Commit aa980554 by Tasawer Nawaz Committed by GitHub

Merge pull request #16240 from edx/tasawer/learner-2804/add-reactjs-for-single-support-form

add reactjs for single support form
parents 168b6b36 d839d177
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': 'webpack',
},
rules: {
'import/prefer-default-export': 'off',
},
};
/* 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 &&
<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 =>
<li>{error}</li>,
)}
</ul>
</div>
</div>;
}
}
ShowErrors.propTypes = {
errorList: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default ShowErrors;
/* 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 (
<div className="file-container">
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label htmlFor="attachment">{gettext('Add Attachment')}
<span> {gettext('(Optional)')}</span>
</label>
<input
id="attachment"
className="file file-loading"
type="file"
accept=".pdf, .jpeg, .png, .jpg, .gif"
onChange={this.uploadFile}
/>
</div>
</div>
</div>
<div className="progress-container">
{this.state.fileInProgress &&
<ShowProgress
fileName={this.state.fileInProgress}
request={this.state.currentRequest}
/>
}
</div>
<div className="uploaded-files">
{
this.state.fileList.map(file =>
(<div key={file.fileToken} className="row">
<div className="col-sm-12">
<span className="file-name">{file.fileName}</span>
<span className="file-action remove-upload">
<button className="btn btn-link" id={file.fileToken} onClick={this.removeFile}>{gettext('Remove file')}</button>
</span>
</div>
</div>),
)
}
</div>
</div>
);
}
}
FileUpload.propTypes = {
setErrorState: PropTypes.func.isRequired,
};
export default FileUpload;
/* global gettext */
import React from 'react';
import PropTypes from 'prop-types';
function LoggedInUser({ userInformation }) {
return (<div>
<div className="row">
<div
className="col-sm-12 user-info"
data-username={userInformation.username}
data-email={userInformation.email}
>
<p>{gettext(`What can we help you with, ${userInformation.username}?`)}</p>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="form-group">
{userInformation.enrollments.length === 0 &&
<div>
<label htmlFor="course">{gettext('Course Name')}<span> {gettext('(Optional)')}</span></label>
<input type="text" className="form-control" id="course" />
</div>
}
{userInformation.enrollments.length > 0 &&
<div>
<label className="label-course" htmlFor="course">{gettext('Course Name')}</label>
<select className="form-control select-course" id="course">
{userInformation.enrollments.map(enrollment =>
(<option key={enrollment.course_id} value={enrollment.course_id}>
{enrollment.course_name}
</option>),
)}
</select>
</div>
}
</div>
</div>
</div>
</div>);
}
LoggedInUser.propTypes = {
userInformation: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default LoggedInUser;
/* global gettext */
import React from 'react';
import PropTypes from 'prop-types';
function LoggedOutUser({ loginUrl }) {
return (
<div>
<div className="row">
<div className="col-sm-12">
<p>{gettext('Sign in to edX so we can help you better.')}</p>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<a href={loginUrl} className="btn btn-primary btn-signin">{gettext('Sign in')}</a>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label htmlFor="email">{gettext('Your Email Address')}</label>
<input type="text" className="form-control" id="email" />
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label
htmlFor="course"
>{gettext('Course Name')}<span> {gettext('(Optional)')}</span></label>
<input type="text" className="form-control" id="course" />
</div>
</div>
</div>
</div>
);
}
LoggedOutUser.propTypes = {
loginUrl: PropTypes.string.isRequired,
};
export default LoggedOutUser;
/* 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 = <LoggedInUser userInformation={this.props.context.user} />;
} else {
userElement = <LoggedOutUser loginUrl={this.props.context.loginQuery} />;
}
return (
<div className="contact-us-wrapper">
<div className="row">
<div className="col-sm-12">
<h2>{gettext('Contact Us')}</h2>
</div>
</div>
<div className="row form-errors">
<ShowErrors errorList={this.state.errorList} />
</div>
<div className="row">
<div className="col-sm-12">
<p>{gettext('Your question might have already been answered.')}</p>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<a
href={this.props.context.marketingUrl}
className="btn btn-secondary help-button"
>{gettext('Search the edX Help Center')}</a>
</div>
</div>
{userElement}
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label htmlFor="subject">{gettext('Subject')}</label>
<input type="text" className="form-control" id="subject" />
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label htmlFor="message">{gettext('Details')}</label>
<p
className="message-desc"
>{gettext('The more you tell us, the more quickly and helpfully we can respond!')}</p>
<textarea
aria-describedby="message"
className="form-control"
rows="7"
id="message"
/>
</div>
</div>
</div>
<FileUpload setErrorState={this.setErrorState} />
<div className="row">
<div className="col-sm-12">
<button
className="btn btn-primary btn-submit"
onClick={this.submitForm}
>{gettext('Submit')}</button>
</div>
</div>
</div>
);
}
}
RenderForm.propTypes = {
context: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export class SingleSupportForm {
constructor(context) {
ReactDOM.render(
<RenderForm context={context} />,
document.getElementById('root'),
);
}
}
/* global gettext */
import React from 'react';
import PropTypes from 'prop-types';
class ShowProgress extends React.Component {
constructor(props) {
super(props);
this.abortRequest = this.abortRequest.bind(this);
}
abortRequest(e) {
e.preventDefault();
this.props.request.abort();
}
render() {
return (
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<span className="file-name">{this.props.fileName}</span>
<span className="file-action abort-upload">
<button className="btn btn-link" onClick={this.abortRequest}>{gettext('Cancel upload')}</button>
</span>
<div className="progress">
<div className="progress-bar progress-bar-striped zero-width" role="progressbar" />
</div>
</div>
</div>
</div>
);
}
}
ShowProgress.propTypes = {
fileName: PropTypes.string.isRequired,
request: PropTypes.objectOf(XMLHttpRequest).isRequired,
};
export default ShowProgress;
......@@ -380,46 +380,3 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
)
verified_mode.expiration_datetime = datetime(year=1970, month=1, day=9, tzinfo=UTC)
verified_mode.save()
class ContactUsViewTests(ModuleStoreTestCase):
url = reverse('support:contact_us')
def setUp(self):
super(ContactUsViewTests, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password='test')
self.user_enrollment = CourseEnrollmentFactory.create(
user=self.user,
)
def test_get_with_logged_in_user(self):
""" Verify that logged in users will see courses dropdown."""
response = self.client.get(self.url)
expected = '<option value="{course_id}">'.format(course_id=self.user_enrollment.course.id)
self.assertContains(response, expected)
def test_get_without_course_enrollment(self):
""" Verify that logged in users will see not courses dropdown,
if they are not enrolled in any course.
"""
self.client.logout()
new_user = UserFactory()
self.client.login(username=new_user.username, password='test')
response = self.client.get(self.url)
self._assert_without_course_enrollment(response)
def test_get_with_anonymous_user(self):
""" Verify that logged out users will see not courses dropdown.
They will see sign in button.
"""
self.client.logout()
response = self.client.get(self.url)
self.assertContains(response, 'class="btn btn-primary btn-signin">Sign in</a>')
self._assert_without_course_enrollment(response)
def _assert_without_course_enrollment(self, response):
""" Assert that users will not see simple course text input."""
expected = '<input type="text" class="form-control" id="course">'
self.assertContains(response, expected)
......@@ -165,7 +165,6 @@
.help-button {
margin-bottom: $baseline;
width: $baseline * 8;
height: $baseline * 2;
font-weight: $font-regular;
font-size: $support-form-base-font-size + 2;
......@@ -203,7 +202,10 @@
.progress-bar {
background-color: $blue;
width: 25%;
}
.zero-width {
width: 0;
}
}
......@@ -231,6 +233,10 @@
font-size: $support-form-base-font-size + 2;
margin-bottom: $baseline - 10;
button {
padding: 0;
}
}
.btn-signin {
......@@ -245,6 +251,23 @@
}
}
input[type='text'] {
font-size: $support-form-base-font-size - 2;
font-family: $sans-serif;
font-style: normal;
font-weight: $font-regular;
}
.alert-danger {
color: $palette-error-text !important;
font-size: $support-form-base-font-size;
}
.has-error{
label{
color: $danger-red;
}
}
@media only screen and (min-width: 768px) {
.row {
max-width: $baseline * 25;
......
......@@ -2,10 +2,12 @@
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%inherit file="../main.html"/>
<%namespace file='../main.html' import="login_query"/>
<%namespace name='static' file='../static_content.html'/>
<%block name="title">
<title>
......@@ -19,142 +21,35 @@ from django.utils.translation import ugettext as _
<%block name="body">
<div class="container contact-us-wrapper">
<div class="row">
<div class="col-sm-12">
<h2>${_("Contact Us")}</h2>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<p>${_("Your question may have already been answered.")}</p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<a href="${marketing_link('FAQ')}" class="btn btn-secondary help-button">${_("Visit edX Help")}</a>
</div>
</div>
<!--logged out users-->
% if not user.is_authenticated():
<div class="row">
<div class="col-sm-12">
<p>${_("Sign in for a faster response")}</p>
</div>
</div>
<!-- Sign-in button brings user to sign-in page. After signing in, user is brough to logged in state of contact form.-->
<div class="row">
<div class="col-sm-12">
<a href="/login${login_query()}" class="btn btn-primary btn-signin">${_("Sign in")}</a>
</div>
</div>
<!-- No autofilled email in logged out state.-->
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="email">${_("Email")}</label>
<input type="text" class="form-control" id="email">
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="course">${_("Course Name")}<span> ${_("(Optional)")}</span></label>
<input type="text" class="form-control" id="course">
</div>
</div>
</div>
% else:
<!--logged in users-->
<div class="row">
<div class="col-sm-12">
<p>${_("What can we help you with, {username}?").format(username=user.username)}</p>
</div>
</div>
<br>
<div id="root" class="container">
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
% if user_enrollments:
<label class="label-course" for="course">${_("Course Name")}</label>
<select class="form-control select-course" id="course">
% for enrollment in user_enrollments:
<option value="${enrollment.course.id}">${enrollment.course.display_name}</option>
% endfor
</select>
% else:
<label for="course">${_("Course Name")}<span> ${_("(Optional)")}</span></label>
<input type="text" class="form-control" id="course">
% endif
</div>
</div>
</div>
<%static:webpack entry="SingleSupportForm">
var context = {
'marketingUrl': "${marketing_link('FAQ') | n, js_escaped_string}",
'loginQuery': "/login${login_query() | n, js_escaped_string}",
}
% if user.is_authenticated():
context['user'] = {
'username': "${user.username | n, js_escaped_string}",
'email': "${user.email | n, js_escaped_string}"
}
% if user_enrollments:
enrollments = []
% for enrollment in user_enrollments:
enrollments.push({
'course_id': "${enrollment.course.id | n, js_escaped_string}",
'course_name': "${enrollment.course.display_name | n, js_escaped_string}",
})
%endfor
context['user']['enrollments'] = enrollments
% endif
% endif
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="message">${_("Message")}</label>
<p class="message-desc">${_("The more you tell us, themore quickly and helpfully we can respond!")}</p>
<textarea aria-describedby="message-desc" class="form-control" rows="7" id="message"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="attachment">${_("Add Attachment")}
<span>${_("(Optional)")}</span>
</label>
<input id="attachment" multiple type="file" class="file file-loading" data-allowed-file-extensions='["png", "jpg", "gif", "tif", "jpeg"]'>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<p>${_("1 file uploaded:")}</p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<span class="file-name">my_image1.png</span>
<span class="file-action"><a href="#">${_("Remove file")}</a></span>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<span class="file-name">my_image2.png</span>
<span class="file-action"><a href="#">${_("Cancel upload")}</a></span>
<div class="progress">
<div class="progress-bar progress-bar-striped" role="progressbar"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button class="btn btn-primary">${_("Submit")}</button>
</div>
</div>
</div>
new SingleSupportForm(context);
</%static:webpack>
</%block>
......@@ -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',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment