Compare commits
9 Commits
registrati
...
ttracy/mic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4905498132 | ||
|
|
88cae3a141 | ||
|
|
b34ba9f241 | ||
|
|
03f3007351 | ||
|
|
c40222973c | ||
|
|
7d8e2f2f85 | ||
|
|
a9d3463619 | ||
|
|
d53c9c11a9 | ||
|
|
2dd8e460b1 |
2
.env
2
.env
@@ -15,4 +15,4 @@ SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
ENABLE_LOGIN_AND_REGISTRATION=false
|
||||
COACHING_ENABLED=null
|
||||
|
||||
@@ -5,7 +5,7 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:1997/login'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
NODE_ENV='development'
|
||||
@@ -16,4 +16,5 @@ SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_LOGIN_AND_REGISTRATION=true
|
||||
# Temporary, Remove this once we are ready to release the feature.
|
||||
COACHING_ENABLED=''
|
||||
|
||||
@@ -15,4 +15,4 @@ SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_LOGIN_AND_REGISTRATION=false #hackathon 23 todo
|
||||
COACHING_ENABLED=false
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1112,15 +1112,15 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-footer": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.0.7.tgz",
|
||||
"integrity": "sha512-CCEjGToWhkhfAcgr27Y660y7Sg7qiHfC+SV83dcAQnK47m5fwLQaJZZ9PRwbotXZZ/Ynmqpltd9Lgk7JQ7p0eg==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.0.8.tgz",
|
||||
"integrity": "sha512-5mcOle7pZ9ITQ3VF76Xi4LpeERQ40IfnmiZQVMfrBXX6PlURROPl+jI1Q2GgIS2CDsSc8DWfYPvUKdJm0U34jw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.8"
|
||||
"@fortawesome/react-fontawesome": "0.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
@@ -1157,9 +1157,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.2.0.tgz",
|
||||
"integrity": "sha512-fy3ergsRxozDofUyNvY62KU1+rZt6kZVdiSovMWYGGV7QYNYLnjyajfyb1gRhsnnRa6uubuIqcQOsH3AeLE4eg==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.1.14.tgz",
|
||||
"integrity": "sha512-wEKKNcjKVNCHL0rgdXIlCF0dwRZc9kuuip2At7+LZ2CxPGJc3s4WGgcsA5yO16YjG2RV+gh2aZV5B/8aczM3Bw==",
|
||||
"requires": {
|
||||
"@cospired/i18n-iso-languages": "2.1.1",
|
||||
"axios": "0.18.1",
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"form-urlencoded": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-4.1.1.tgz",
|
||||
"integrity": "sha1-Nv/xWCFoRT1XrYnVUAhbPJ3pv4I="
|
||||
"integrity": "sha512-q4SOWDgKw2Mmec2HNYzTNr/7ZXecEkO9QOwzpzx6q9HR+z+no6CsDFpAELdiyk7G3fUV5WvKy57utZxKgleqhg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1288,11 +1288,11 @@
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.8.tgz",
|
||||
"integrity": "sha512-I5h9YQg/ePA3Br9ISS18fcwOYmzQYDSM1ftH03/8nHkiqIVHtUyQBw482+60dnzvlr82gHt3mGm+nDUp159FCw==",
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.9.tgz",
|
||||
"integrity": "sha512-49V3WNysLZU5fZ3sqSuys4nGRytsrxJktbv3vuaXkEoxv22C6T7TEG0TW6+nqVjMnkfCQd5xOnmJoZHMF78tOw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.10"
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@jest/console": {
|
||||
@@ -11519,6 +11519,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
|
||||
},
|
||||
"lodash.defaultsdeep": {
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
|
||||
@@ -14603,7 +14608,7 @@
|
||||
"pubsub-js": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.7.0.tgz",
|
||||
"integrity": "sha1-7Kl/mkIXvvYrLTqqFVIAUmDMLkk="
|
||||
"integrity": "sha512-Pb68P9qFZxnvDipHMuj9oT1FoIgBcXJ9C9eWdHCLZAnulaUoJ3+Y87RhGMYilWpun6DMWVmvK70T4RP4drZMSA=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
@@ -14734,7 +14739,8 @@
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||
"dev": true
|
||||
},
|
||||
"querystring-es3": {
|
||||
"version": "0.2.1",
|
||||
@@ -14949,7 +14955,7 @@
|
||||
"react-intl": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
|
||||
"integrity": "sha1-yXxdF9RxjxV1/b1adp+WAYo7GEM=",
|
||||
"integrity": "sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"intl-format-cache": "^2.0.5",
|
||||
|
||||
@@ -29,15 +29,15 @@
|
||||
"ie 11"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/frontend-component-footer": "10.0.7",
|
||||
"@edx/frontend-component-footer": "10.0.8",
|
||||
"@edx/frontend-component-header": "2.0.5",
|
||||
"@edx/frontend-platform": "1.2.0",
|
||||
"@edx/frontend-platform": "1.1.14",
|
||||
"@edx/paragon": "7.1.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.8",
|
||||
"@fortawesome/react-fontawesome": "0.1.9",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.2.6",
|
||||
"font-awesome": "4.7.0",
|
||||
@@ -45,6 +45,7 @@
|
||||
"formdata-polyfill": "3.0.19",
|
||||
"history": "4.10.1",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.findindex": "4.6.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.isempty": "4.4.0",
|
||||
@@ -55,7 +56,6 @@
|
||||
"memoize-one": "5.1.1",
|
||||
"newrelic": "5.13.1",
|
||||
"prop-types": "15.7.2",
|
||||
"querystring": "0.2.0",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2",
|
||||
"react-redux": "7.1.3",
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
GENDER_OPTIONS,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import CoachingToggle from './coaching/CoachingToggle';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -327,6 +328,14 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED &&
|
||||
this.props.formValues.coaching.eligible_for_coaching &&
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="social-media">
|
||||
@@ -474,10 +483,16 @@ AccountSettingsPage.propTypes = {
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
language_proficiencies: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
social_link_linkedin: PropTypes.string,
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
coaching: PropTypes.objectOf(PropTypes.shape({
|
||||
coaching_consent: PropTypes.string.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
})),
|
||||
}).isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
|
||||
@@ -35,4 +35,13 @@
|
||||
margin-bottom: map-get($spacers, 5);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.custom-switch {
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
.custom-control-label {
|
||||
left: 2.25rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
src/account-settings/coaching/CoachingToggle.jsx
Normal file
76
src/account-settings/coaching/CoachingToggle.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup, Input } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
|
||||
const CoatchingToggle = props => (
|
||||
<>
|
||||
<EditableField
|
||||
name="phone_number"
|
||||
type="text"
|
||||
value={props.phone_number}
|
||||
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
|
||||
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
|
||||
onChange={props.updateDraft}
|
||||
onSubmit={props.saveSettings}
|
||||
/>
|
||||
<ValidationFormGroup
|
||||
for="coachingConsent"
|
||||
helpText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
invalid={!!props.error}
|
||||
invalidMessage={props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
className="custom-control custom-switch"
|
||||
>
|
||||
<Input
|
||||
name={props.name}
|
||||
className="custom-control-input"
|
||||
disabled={props.saveState === 'pending'}
|
||||
type="checkbox"
|
||||
id="coachingConsent"
|
||||
checked={props.coaching.coaching_consent}
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
const value = {
|
||||
...props.coaching,
|
||||
phone_number: props.phone_number,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
}}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor="coachingConsent">{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
|
||||
</ValidationFormGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
CoatchingToggle.defaultProps = {
|
||||
phone_number: '',
|
||||
error: '',
|
||||
};
|
||||
|
||||
CoatchingToggle.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
coaching: PropTypes.objectOf(PropTypes.shape({
|
||||
coaching_consent: PropTypes.string.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
})).isRequired,
|
||||
saveState: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
phone_number: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
})(injectIntl(CoatchingToggle));
|
||||
31
src/account-settings/coaching/CoachingToggle.messages.js
Normal file
31
src/account-settings/coaching/CoachingToggle.messages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.phone_number': {
|
||||
id: 'account.settings.field.phone_number',
|
||||
defaultMessage: 'Phone Number',
|
||||
description: 'The label for a phone numbers setting in the user profile',
|
||||
},
|
||||
'account.settings.field.phone_number.empty': {
|
||||
id: 'account.settings.field.phone_number.empty',
|
||||
defaultMessage: 'Add a phone number',
|
||||
description: 'placeholder for a profiles empty phone number field',
|
||||
},
|
||||
'account.settings.field.coaching_consent': {
|
||||
id: 'account.settings.field.coaching_consent',
|
||||
defaultMessage: 'Coaching consent',
|
||||
description: 'The label for the coaching consent setting in the user profile',
|
||||
},
|
||||
'account.settings.field.coaching_consent.tooltip': {
|
||||
id: 'account.settings.field.coaching_consent.tooltip',
|
||||
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.',
|
||||
description: 'A tooltip explaining what coaching is and who it is for',
|
||||
},
|
||||
'account.settings.field.coaching_consent.error': {
|
||||
id: 'account.settings.field.coaching_consent.error',
|
||||
defaultMessage: 'A valid US phone number is required to opt into coaching',
|
||||
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
36
src/account-settings/coaching/data/service.js
Normal file
36
src/account-settings/coaching/data/service.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
/**
|
||||
* get all settings related to the coaching plugin. Settings used
|
||||
* by Microbachelors students.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getCoachingPreferences(userId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the settings related to coaching.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { coaching }
|
||||
*/
|
||||
export async function patchCoachingPreferences(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
|
||||
const { coaching } = commitValues;
|
||||
coaching.user = userId;
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, coaching)
|
||||
.catch((error) => {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
throw apiError;
|
||||
});
|
||||
return commitValues;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import { getSettings, patchSettings, getTimeZones } from './service';
|
||||
export function* handleFetchSettings() {
|
||||
try {
|
||||
yield put(fetchSettingsBegin());
|
||||
const { username, roles: userRoles } = getAuthenticatedUser();
|
||||
const { username, userId, roles: userRoles } = getAuthenticatedUser();
|
||||
|
||||
const {
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
|
||||
@@ -45,6 +45,7 @@ export function* handleFetchSettings() {
|
||||
getSettings,
|
||||
username,
|
||||
userRoles,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (values.country) yield put(fetchTimeZones(values.country));
|
||||
@@ -65,7 +66,7 @@ export function* handleSaveSettings(action) {
|
||||
try {
|
||||
yield put(saveSettingsBegin());
|
||||
|
||||
const { username } = getAuthenticatedUser();
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
let savedValues = null;
|
||||
@@ -83,7 +84,7 @@ export function* handleSaveSettings(action) {
|
||||
handleRtl();
|
||||
savedValues = commitData;
|
||||
} else {
|
||||
savedValues = yield call(patchSettings, username, commitData);
|
||||
savedValues = yield call(patchSettings, username, commitData, userId);
|
||||
}
|
||||
yield put(saveSettingsSuccess(savedValues, commitData));
|
||||
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
|
||||
|
||||
@@ -6,6 +6,7 @@ import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -151,15 +152,16 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
|
||||
* Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
|
||||
*/
|
||||
export async function getSettings(username, userRoles) {
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
const results = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -168,20 +170,23 @@ export async function getSettings(username, userRoles) {
|
||||
thirdPartyAuthProviders: results[2],
|
||||
profileDataManager: results[3],
|
||||
timeZones: results[4],
|
||||
coaching: results[5],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to PATCH everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
|
||||
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
|
||||
*/
|
||||
export async function patchSettings(username, commitValues) {
|
||||
export async function patchSettings(username, commitValues, userId) {
|
||||
// Note: time_zone exists in the return value from user/v1/accounts
|
||||
// but it is always null and won't update. It also exists in
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
@@ -190,6 +195,9 @@ export async function patchSettings(username, commitValues) {
|
||||
if (!isEmpty(preferenceCommitValues)) {
|
||||
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
||||
}
|
||||
if (!isEmpty(coachingCommitValues)) {
|
||||
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1168px" height="540px" viewBox="0 0 1168 540" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>logo</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<polygon id="Path" fill="#209FDA" fill-rule="nonzero" points="1166.81993 85.5 1166.81993 2.84217094e-14 953.759925 2.84217094e-14 953.759925 85.5 1002.17993 85.5 915.859925 191.98 829.459925 85.5 878.099925 85.5 878.099925 2.84217094e-14 718.919925 2.84217094e-14 718.919925 95.72 856.479925 265.26 718.919925 434.96 718.919925 452.02 784.499925 452.02 784.499925 539.64 878.099925 539.64 878.099925 452.02 823.919925 452.02 915.919925 338.52 915.939925 338.52 1008.03993 452.02 953.759925 452.02 953.759925 539.64 1166.81993 539.64 1166.81993 452.02 1126.85993 452.02 975.319925 265.26 1121.01993 85.5"></polygon>
|
||||
<polygon id="Path" fill="#026BA4" fill-rule="nonzero" points="664.019925 7.10542736e-15 664.019925 85.5 710.619925 85.5 718.919925 95.72 718.919925 7.10542736e-15"></polygon>
|
||||
<polygon id="Path" fill="#026BA4" fill-rule="nonzero" points="718.919925 452.02 718.919925 434.96 705.079925 452.02 664.019925 452.02 664.019925 539.64 784.499925 539.64 784.499925 452.02"></polygon>
|
||||
<path d="M321.999925,411.86 L397.659925,411.86 C388.805702,433.829527 376.258024,454.122269 360.559925,471.86 C344.364089,454.216816 331.320914,433.921419 321.999925,411.86" id="Path" fill="#78212E" fill-rule="nonzero"></path>
|
||||
<path d="M360.559925,189.28 C338.58337,213.190393 322.501981,241.908137 313.599925,273.14 C317.134915,280.039338 320.007771,287.25831 322.179925,294.7 L397.059925,294.7 C399.306706,287.354671 402.25356,280.242036 405.859925,273.46 C397.464721,242.277678 381.959326,213.464341 360.559925,189.28 Z M322.179925,294.7 C328.784599,317.438017 328.978396,341.558795 322.739925,364.4 L396.399925,364.4 C389.855554,341.597488 390.06397,317.386469 396.999925,294.7 L322.179925,294.7 Z M322.179925,294.7 L308.679925,294.7 C304.690779,317.752715 304.575868,341.309464 308.339925,364.4 L322.739925,364.4 C328.978396,341.558795 328.784599,317.438017 322.179925,294.7 L322.179925,294.7 Z" id="Shape" fill="#78212E" fill-rule="nonzero"></path>
|
||||
<path d="M710.619925,85.5 L664.019925,85.5 L664.019925,0.02 L576.019925,0.02 L576.019925,85.5 L632.859925,85.5 L632.859925,159.2 C598.417874,134.487772 557.04992,121.286425 514.659925,121.48 C456.044663,121.405246 400.107354,146.01621 360.559925,189.28 C381.937732,213.470272 397.422343,242.283149 405.799925,273.46 C426.944121,233.500977 468.451514,208.51034 513.659925,208.52 C581.059925,208.52 632.879925,263.16 632.879925,330.52 L632.879925,331.2 C632.539925,398.28 580.879925,452.56 513.659925,452.56 C468.477451,452.593197 426.976426,427.652566 405.799925,387.74 L405.799925,387.74 C401.869213,380.340239 398.718926,372.551658 396.399925,364.5 L308.399925,364.5 C309.686934,372.450225 311.443338,380.317312 313.659925,388.06 C315.970162,396.190434 318.775397,404.171995 322.059925,411.96 L397.659925,411.96 C388.805702,433.929527 376.258024,454.222269 360.559925,471.96 C400.107354,515.22379 456.044663,539.834754 514.659925,539.76 C571.465111,540.091874 625.745998,516.316729 664.019925,474.34 L664.019925,452.04 L705.059925,452.04 L718.899925,434.96 L718.899925,95.74 L710.619925,85.5 Z M632.879925,501.9 L632.879925,539.74 L664.019925,539.74 L664.019925,474.18 C654.623775,484.469293 644.18821,493.758755 632.879925,501.9 L632.879925,501.9 Z M313.599925,273.14 C311.569597,280.231983 309.927163,287.429316 308.679925,294.7 L322.179925,294.7 C320.007771,287.25831 317.134915,280.039338 313.599925,273.14 L313.599925,273.14 Z" id="Shape" fill="#8A8C8F" fill-rule="nonzero"></path>
|
||||
<path d="M410.399925,294.7 C409.199925,287.5 407.659925,280.4 405.799925,273.46 C402.19356,280.242036 399.246706,287.354671 396.999925,294.7 C390.06397,317.386469 389.855554,341.597488 396.399925,364.4 L410.719925,364.4 C414.264276,341.293291 414.156293,317.77319 410.399925,294.7 L410.399925,294.7 Z M209.059925,121.48 C107.422724,121.487508 20.5081632,194.571683 3.05992537,294.7 L91.3999254,294.7 C107.135726,243.467257 154.465065,208.503753 208.059925,208.52 C252.638644,208.335148 293.496156,233.351373 313.599925,273.14 C322.501981,241.908137 338.58337,213.190393 360.559925,189.28 C322.206855,145.880863 266.976617,121.163964 209.059925,121.48 L209.059925,121.48 Z M297.479925,411.86 C275.077969,437.877726 242.392659,452.761934 208.059925,452.58 C153.691226,452.598435 105.87164,416.63791 90.7999254,364.4 L308.339925,364.4 C304.575868,341.309464 304.690779,317.752715 308.679925,294.7 L3.05992537,294.7 C-0.902504563,317.755068 -1.01739385,341.307372 2.71992537,364.4 L2.71992537,364.4 C19.3292424,465.441984 106.661918,539.594765 209.059925,539.6 C266.986094,539.900862 322.217868,515.161403 360.559925,471.74 C344.364089,454.096816 331.320914,433.801419 321.999925,411.74 L297.479925,411.86 Z" id="Shape" fill="#B72768" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -5,12 +5,7 @@ import {
|
||||
storeName as accountSettingsStoreName,
|
||||
} from '../account-settings';
|
||||
|
||||
import {
|
||||
reducer as registrationReducer,
|
||||
} from '../registration';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[accountSettingsStoreName]: accountSettingsReducer,
|
||||
registration: registrationReducer,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { saga as accountSettingsSaga } from '../account-settings';
|
||||
import { saga as registrationSaga } from '../registration';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
accountSettingsSaga(),
|
||||
registrationSaga(),
|
||||
]);
|
||||
yield all([accountSettingsSaga()]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'babel-polyfill';
|
||||
import 'formdata-polyfill';
|
||||
import { AppProvider, ErrorPage, AuthenticatedPageRoute } from '@edx/frontend-platform/react';
|
||||
import { subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
@@ -11,58 +11,22 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import AccountSettingsPage, { NotFoundPage } from './account-settings';
|
||||
import LoginPage from './registration/LoginPage';
|
||||
import RegistrationPage from './registration/RegistrationPage';
|
||||
import appMessages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
import logo from './assets/headerlogo.svg';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Switch>
|
||||
<AuthenticatedPageRoute exact path="/">
|
||||
<Header />
|
||||
<main>
|
||||
<AccountSettingsPage />
|
||||
</main>
|
||||
</AuthenticatedPageRoute>
|
||||
<Route path="/notfound">
|
||||
<Header />
|
||||
<main>
|
||||
<NotFoundPage />
|
||||
</main>
|
||||
</Route>
|
||||
{
|
||||
getConfig().ENABLE_LOGIN_AND_REGISTRATION &&
|
||||
<>
|
||||
<Route path="/login" >
|
||||
<div className="registration-header">
|
||||
<img src={logo} alt="edX" className="logo" />
|
||||
</div>
|
||||
<main>
|
||||
<LoginPage />
|
||||
</main>
|
||||
</Route>
|
||||
<Route path="/registration">
|
||||
<div className="registration-header">
|
||||
<img src={logo} alt="edX" className="logo" />
|
||||
</div>
|
||||
<main>
|
||||
<RegistrationPage />
|
||||
</main>
|
||||
</Route>
|
||||
</>
|
||||
}
|
||||
<Route path="*">
|
||||
<Header />
|
||||
<main>
|
||||
<NotFoundPage />
|
||||
</main>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/" component={AccountSettingsPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
@@ -79,13 +43,13 @@ initialize({
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: false,
|
||||
requireAuthenticatedUser: true,
|
||||
hydrateAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
ENABLE_LOGIN_AND_REGISTRATION: process.env.ENABLE_LOGIN_AND_REGISTRATION,
|
||||
COACHING_ENABLED: process.env.COACHING_ENABLED,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import "./account-settings/style";
|
||||
@import "./registration/style";
|
||||
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFacebookF, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
export default class LoginPage extends React.Component {
|
||||
state = {
|
||||
password: '',
|
||||
email: '',
|
||||
errors: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
emailValid: false,
|
||||
passwordValid: false,
|
||||
formValid: false,
|
||||
}
|
||||
|
||||
handleOnChange(e) {
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
this.validateInput(e.target.name, e.target.value);
|
||||
}
|
||||
|
||||
validateInput(inputName, value) {
|
||||
let inputErrors = this.state.errors;
|
||||
let emailValid = this.state.emailValid;
|
||||
let passwordValid = this.state.passwordValid;
|
||||
|
||||
switch (inputName) {
|
||||
case 'email':
|
||||
emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i);
|
||||
inputErrors.email = emailValid ? '' : null;
|
||||
break;
|
||||
case 'password':
|
||||
passwordValid = value.length >= 8 && value.match(/\d+/g);
|
||||
inputErrors.password = passwordValid ? '' : null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errors: inputErrors,
|
||||
emailValid,
|
||||
passwordValid,
|
||||
}, this.validateForm);
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.setState({
|
||||
formValid: this.state.emailValid && this.state.passwordValid,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="d-flex justify-content-center registration-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="d-flex flex-row">
|
||||
<p>We <span>❤️</span>our learners.</p>
|
||||
<p> First time here?</p>
|
||||
<a className="ml-2" href="/registration">Join our community!</a>
|
||||
</div>
|
||||
<form className="m-0">
|
||||
<div className="form-group">
|
||||
<h3 className="text-center mt-3">Sign In</h3>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<ValidationFormGroup
|
||||
for="email"
|
||||
invalid={this.state.errors.email !== ''}
|
||||
invalidMessage="The email address you've provided isn't formatted correctly."
|
||||
>
|
||||
<label htmlFor="loginEmail" className="h6 mr-1">Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
id="loginEmail"
|
||||
type="email"
|
||||
placeholder="email@domain.com"
|
||||
value={this.state.email}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
style={{ width: '400px' }}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
<p className="mb-4">The email address you used to register with edX.</p>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<label htmlFor="loginPassword" className="h6 mr-1">Password</label>
|
||||
<Input
|
||||
name="password"
|
||||
id="loginPassword"
|
||||
type="password"
|
||||
value={this.state.password}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="btn-primary submit">Sign in</Button>
|
||||
</form>
|
||||
<div className="section-heading-line mb-4">
|
||||
<h4>or sign in with</h4>
|
||||
</div>
|
||||
<div className="row text-center d-block mb-4">
|
||||
<button className="btn-social facebook"><FontAwesomeIcon className="mr-2" icon={faFacebookF} />Facebook</button>
|
||||
<button className="btn-social google"><FontAwesomeIcon className="mr-2" icon={faGoogle} />Google</button>
|
||||
<button className="btn-social microsoft"><FontAwesomeIcon className="mr-2" icon={faMicrosoft} />Microsoft</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Input, ValidationFormGroup, StatusAlert } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFacebookF, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faGraduationCap } from '@fortawesome/free-solid-svg-icons';
|
||||
import countryList from './countryList';
|
||||
|
||||
import { registerNewUser } from './data/actions';
|
||||
|
||||
export class RegistrationPage extends React.Component {
|
||||
state = {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailValid: false,
|
||||
nameValid: false,
|
||||
usernameValid: false,
|
||||
passwordValid: false,
|
||||
countryValid: false,
|
||||
formValid: false,
|
||||
open: false,
|
||||
}
|
||||
|
||||
handleSelectCountry = (e) => {
|
||||
this.setState({
|
||||
country: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
console.log('clicked submit', e);
|
||||
e.preventDefault();
|
||||
this.setState({ open: true });
|
||||
|
||||
const payload = {
|
||||
email: this.state.email,
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
name: this.state.name,
|
||||
honor_code: true,
|
||||
country: this.state.country,
|
||||
};
|
||||
|
||||
this.props.registerNewUser(payload);
|
||||
}
|
||||
|
||||
resetStatusAlertWrapperState() {
|
||||
this.setState({ open: false });
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
handleOnChange(e) {
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
this.validateInput(e.target.name, e.target.value);
|
||||
}
|
||||
|
||||
validateInput(inputName, value) {
|
||||
let inputErrors = this.state.errors;
|
||||
let emailValid = this.state.emailValid;
|
||||
let nameValid = this.state.nameValid;
|
||||
let usernameValid = this.state.usernameValid;
|
||||
let passwordValid = this.state.passwordValid;
|
||||
let countryValid = this.state.countryValid;
|
||||
|
||||
switch (inputName) {
|
||||
case 'email':
|
||||
emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i);
|
||||
inputErrors.email = emailValid ? '' : null;
|
||||
break;
|
||||
case 'name':
|
||||
nameValid = value.length >= 1;
|
||||
inputErrors.name = nameValid ? '' : null;
|
||||
break;
|
||||
case 'username':
|
||||
usernameValid = value.length >= 2 && value.length <= 30;
|
||||
inputErrors.username = usernameValid ? '' : null;
|
||||
break;
|
||||
case 'password':
|
||||
passwordValid = value.length >= 8 && value.match(/\d+/g);
|
||||
inputErrors.password = passwordValid ? '' : null;
|
||||
break;
|
||||
case 'country':
|
||||
countryValid = value !== 'Country or Region of Residence (required)';
|
||||
inputErrors.country = countryValid ? '' : null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errors: inputErrors,
|
||||
emailValid,
|
||||
nameValid,
|
||||
usernameValid,
|
||||
passwordValid,
|
||||
countryValid,
|
||||
}, this.validateForm);
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.setState({
|
||||
formValid: this.state.emailValid && this.state.nameValid &&
|
||||
this.state.usernameValid && this.state.passwordValid && this.state.countryValid,
|
||||
});
|
||||
}
|
||||
|
||||
renderCountryList() {
|
||||
const items = [{ value: 'Country or Region of Residence (required)', label: 'Country or Region of Residence (required)' }];
|
||||
const countries = Object.values(countryList);
|
||||
for (let i = 0; i < countries.length; i += 1) {
|
||||
items.push({ value: countries[i], label: countries[i] });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="registration-container d-flex flex-column align-items-center mx-auto" style={{ width: '30rem' }}>
|
||||
<div className="mb-4">
|
||||
<FontAwesomeIcon className="d-block mx-auto fa-2x" icon={faGraduationCap} />
|
||||
<h4 className="d-block mx-auto">Start learning now!</h4>
|
||||
</div>
|
||||
<div className="d-block mb-4">
|
||||
<span className="d-block mx-auto mb-4 section-heading-line">Create an account using</span>
|
||||
<button className="btn-social facebook"><FontAwesomeIcon className="mr-2" icon={faFacebookF} />Facebook</button>
|
||||
<button className="btn-social google"><FontAwesomeIcon className="mr-2" icon={faGoogle} />Google</button>
|
||||
<button className="btn-social microsoft"><FontAwesomeIcon className="mr-2" icon={faMicrosoft} />Microsoft</button>
|
||||
<span className="d-block mx-auto text-center mt-4 section-heading-line">or create a new one here</span>
|
||||
</div>
|
||||
|
||||
<form className="mb-4 mx-auto form-group">
|
||||
<ValidationFormGroup
|
||||
for="email"
|
||||
invalid={this.state.errors.email !== ''}
|
||||
invalidMessage="Enter a valid email address that contains at least 3 characters."
|
||||
>
|
||||
<label htmlFor="registrationEmail" className="h6 pt-3">Email (required)</label>
|
||||
<Input
|
||||
name="email"
|
||||
id="registrationEmail"
|
||||
type="email"
|
||||
placeholder="email@domain.com"
|
||||
value={this.state.email}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="name"
|
||||
invalid={this.state.errors.name !== ''}
|
||||
invalidMessage="Enter your full name."
|
||||
>
|
||||
<label htmlFor="registrationName" className="h6 pt-3">Full Name (required)</label>
|
||||
<Input
|
||||
name="name"
|
||||
id="registrationName"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={this.state.name}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="username"
|
||||
invalid={this.state.errors.username !== ''}
|
||||
invalidMessage="Username must be between 2 and 30 characters long."
|
||||
>
|
||||
<label htmlFor="registrationUsername" className="h6 pt-3">Public Username (required)</label>
|
||||
<Input
|
||||
name="username"
|
||||
id="registrationUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.username}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="password"
|
||||
invalid={this.state.errors.password !== ''}
|
||||
invalidMessage="This password is too short. It must contain at least 8 characters. This password must contain at least 1 number."
|
||||
>
|
||||
<label htmlFor="registrationPassword" className="h6 pt-3">Password (required)</label>
|
||||
<Input
|
||||
name="password"
|
||||
id="registrationPassword"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={this.state.password}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="country"
|
||||
invalid={this.state.errors.country !== ''}
|
||||
invalidMessage="Select your country or region of residence."
|
||||
>
|
||||
<label htmlFor="registrationCountry" className="h6 pt-3">Country (required)</label>
|
||||
<Input
|
||||
type="select"
|
||||
placeholder="Country or Region of Residence"
|
||||
value={this.state.country}
|
||||
options={this.renderCountryList()}
|
||||
onChange={this.handleSelectCountry}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<span>By creating an account, you agree to the <a href="https://www.edx.org/edx-terms-service">Terms of Service and Honor Code</a> and you acknowledge that edX and each Member process your personal data in accordance with the <a href="https://www.edx.org/edx-privacy-policy">Privacy Policy</a>.</span>
|
||||
<Button
|
||||
className="btn-primary mt-4 submit"
|
||||
onClick={this.handleSubmit}
|
||||
inputRef={(input) => {
|
||||
this.button = input;
|
||||
}}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
open={this.state.open}
|
||||
dialog="❤️❤️❤️ Your account was has been created! Welcome to our learning community! ❤️❤️❤️"
|
||||
onClose={this.resetStatusAlertWrapperState}
|
||||
/>
|
||||
</form>
|
||||
<div className="text-center mb-2 pt-2">
|
||||
<span>Already have an edX account?</span>
|
||||
<a href="/login"> Sign in.</a>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
() => ({}),
|
||||
{
|
||||
registerNewUser,
|
||||
},
|
||||
)(RegistrationPage);
|
||||
@@ -1,72 +0,0 @@
|
||||
.registration-container {
|
||||
margin: 4rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.registration-header {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
height: 3.75rem;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.registration-header img {
|
||||
height: 1.75rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
padding: 0.5rem 1rem;
|
||||
color: white;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.facebook {
|
||||
border-color: #4267b2;
|
||||
background-color: #4267b2;
|
||||
}
|
||||
|
||||
.google {
|
||||
border-color: #4285f4;
|
||||
background-color: #4285f4;
|
||||
}
|
||||
|
||||
.microsoft {
|
||||
border-color: #2f2f2f;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: inherit;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-heading-line{
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
const countryList = {
|
||||
AF: 'Afghanistan',
|
||||
AL: 'Albania',
|
||||
DZ: 'Algeria',
|
||||
AS: 'American Samoa',
|
||||
AD: 'Andorra',
|
||||
AO: 'Angola',
|
||||
AI: 'Anguilla',
|
||||
AQ: 'Antarctica',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AR: 'Argentina',
|
||||
AM: 'Armenia',
|
||||
AW: 'Aruba',
|
||||
AU: 'Australia',
|
||||
AT: 'Austria',
|
||||
AZ: 'Azerbaijan',
|
||||
BS: 'Bahamas (the)',
|
||||
BH: 'Bahrain',
|
||||
BD: 'Bangladesh',
|
||||
BB: 'Barbados',
|
||||
BY: 'Belarus',
|
||||
BE: 'Belgium',
|
||||
BZ: 'Belize',
|
||||
BJ: 'Benin',
|
||||
BM: 'Bermuda',
|
||||
BT: 'Bhutan',
|
||||
BO: 'Bolivia (Plurinational State of)',
|
||||
BQ: 'Bonaire, Sint Eustatius and Saba',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BW: 'Botswana',
|
||||
BV: 'Bouvet Island',
|
||||
BR: 'Brazil',
|
||||
IO: 'British Indian Ocean Territory (the)',
|
||||
BN: 'Brunei Darussalam',
|
||||
BG: 'Bulgaria',
|
||||
BF: 'Burkina Faso',
|
||||
BI: 'Burundi',
|
||||
CV: 'Cabo Verde',
|
||||
KH: 'Cambodia',
|
||||
CM: 'Cameroon',
|
||||
CA: 'Canada',
|
||||
KY: 'Cayman Islands (the)',
|
||||
CF: 'Central African Republic (the)',
|
||||
TD: 'Chad',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CX: 'Christmas Island',
|
||||
CC: 'Cocos (Keeling) Islands (the)',
|
||||
CO: 'Colombia',
|
||||
KM: 'Comoros (the)',
|
||||
CD: 'Congo (the Democratic Republic of the)',
|
||||
CG: 'Congo (the)',
|
||||
CK: 'Cook Islands (the)',
|
||||
CR: 'Costa Rica',
|
||||
HR: 'Croatia',
|
||||
CU: 'Cuba',
|
||||
CW: 'Curaçao',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czechia',
|
||||
CI: 'Côte d\'Ivoire',
|
||||
DK: 'Denmark',
|
||||
DJ: 'Djibouti',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic (the)',
|
||||
EC: 'Ecuador',
|
||||
EG: 'Egypt',
|
||||
SV: 'El Salvador',
|
||||
GQ: 'Equatorial Guinea',
|
||||
ER: 'Eritrea',
|
||||
EE: 'Estonia',
|
||||
SZ: 'Eswatini',
|
||||
ET: 'Ethiopia',
|
||||
FK: 'Falkland Islands (the) [Malvinas]',
|
||||
FO: 'Faroe Islands (the)',
|
||||
FJ: 'Fiji',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GF: 'French Guiana',
|
||||
PF: 'French Polynesia',
|
||||
TF: 'French Southern Territories (the)',
|
||||
GA: 'Gabon',
|
||||
GM: 'Gambia (the)',
|
||||
GE: 'Georgia',
|
||||
DE: 'Germany',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GR: 'Greece',
|
||||
GL: 'Greenland',
|
||||
GD: 'Grenada',
|
||||
GP: 'Guadeloupe',
|
||||
GU: 'Guam',
|
||||
GT: 'Guatemala',
|
||||
GG: 'Guernsey',
|
||||
GN: 'Guinea',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HT: 'Haiti',
|
||||
HM: 'Heard Island and McDonald Islands',
|
||||
VA: 'Holy See (the)',
|
||||
HN: 'Honduras',
|
||||
HK: 'Hong Kong',
|
||||
HU: 'Hungary',
|
||||
IS: 'Iceland',
|
||||
IN: 'India',
|
||||
ID: 'Indonesia',
|
||||
IR: 'Iran (Islamic Republic of)',
|
||||
IQ: 'Iraq',
|
||||
IE: 'Ireland',
|
||||
IM: 'Isle of Man',
|
||||
IL: 'Israel',
|
||||
IT: 'Italy',
|
||||
JM: 'Jamaica',
|
||||
JP: 'Japan',
|
||||
JE: 'Jersey',
|
||||
JO: 'Jordan',
|
||||
KZ: 'Kazakhstan',
|
||||
KE: 'Kenya',
|
||||
KI: 'Kiribati',
|
||||
KP: 'Korea (the Democratic People\'s Republic of)',
|
||||
KR: 'Korea (the Republic of)',
|
||||
KW: 'Kuwait',
|
||||
KG: 'Kyrgyzstan',
|
||||
LA: 'Lao People\'s Democratic Republic (the)',
|
||||
LV: 'Latvia',
|
||||
LB: 'Lebanon',
|
||||
LS: 'Lesotho',
|
||||
LR: 'Liberia',
|
||||
LY: 'Libya',
|
||||
LI: 'Liechtenstein',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MO: 'Macao',
|
||||
MG: 'Madagascar',
|
||||
MW: 'Malawi',
|
||||
MY: 'Malaysia',
|
||||
MV: 'Maldives',
|
||||
ML: 'Mali',
|
||||
MT: 'Malta',
|
||||
MH: 'Marshall Islands (the)',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MU: 'Mauritius',
|
||||
YT: 'Mayotte',
|
||||
MX: 'Mexico',
|
||||
FM: 'Micronesia (Federated States of)',
|
||||
MD: 'Moldova (the Republic of)',
|
||||
MC: 'Monaco',
|
||||
MN: 'Mongolia',
|
||||
ME: 'Montenegro',
|
||||
MS: 'Montserrat',
|
||||
MA: 'Morocco',
|
||||
MZ: 'Mozambique',
|
||||
MM: 'Myanmar',
|
||||
NA: 'Namibia',
|
||||
NR: 'Nauru',
|
||||
NP: 'Nepal',
|
||||
NL: 'Netherlands (the)',
|
||||
NC: 'New Caledonia',
|
||||
NZ: 'New Zealand',
|
||||
NI: 'Nicaragua',
|
||||
NE: 'Niger (the)',
|
||||
NG: 'Nigeria',
|
||||
NU: 'Niue',
|
||||
NF: 'Norfolk Island',
|
||||
MP: 'Northern Mariana Islands (the)',
|
||||
NO: 'Norway',
|
||||
OM: 'Oman',
|
||||
PK: 'Pakistan',
|
||||
PW: 'Palau',
|
||||
PS: 'Palestine, State of',
|
||||
PA: 'Panama',
|
||||
PG: 'Papua New Guinea',
|
||||
PY: 'Paraguay',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines (the)',
|
||||
PN: 'Pitcairn',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PR: 'Puerto Rico',
|
||||
QA: 'Qatar',
|
||||
MK: 'Republic of North Macedonia',
|
||||
RO: 'Romania',
|
||||
RU: 'Russian Federation (the)',
|
||||
RW: 'Rwanda',
|
||||
RE: 'Réunion',
|
||||
BL: 'Saint Barthélemy',
|
||||
SH: 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
LC: 'Saint Lucia',
|
||||
MF: 'Saint Martin (French part)',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
WS: 'Samoa',
|
||||
SM: 'San Marino',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SA: 'Saudi Arabia',
|
||||
SN: 'Senegal',
|
||||
RS: 'Serbia',
|
||||
SC: 'Seychelles',
|
||||
SL: 'Sierra Leone',
|
||||
SG: 'Singapore',
|
||||
SX: 'Sint Maarten (Dutch part)',
|
||||
SK: 'Slovakia',
|
||||
SI: 'Slovenia',
|
||||
SB: 'Solomon Islands',
|
||||
SO: 'Somalia',
|
||||
ZA: 'South Africa',
|
||||
GS: 'South Georgia and the South Sandwich Islands',
|
||||
SS: 'South Sudan',
|
||||
ES: 'Spain',
|
||||
LK: 'Sri Lanka',
|
||||
SD: 'Sudan (the)',
|
||||
SR: 'Suriname',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SE: 'Sweden',
|
||||
CH: 'Switzerland',
|
||||
SY: 'Syrian Arab Republic',
|
||||
TW: 'Taiwan (Province of China)',
|
||||
TJ: 'Tajikistan',
|
||||
TZ: 'Tanzania, United Republic of',
|
||||
TH: 'Thailand',
|
||||
TL: 'Timor-Leste',
|
||||
TG: 'Togo',
|
||||
TK: 'Tokelau',
|
||||
TO: 'Tonga',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Turkey',
|
||||
TM: 'Turkmenistan',
|
||||
TC: 'Turks and Caicos Islands (the)',
|
||||
TV: 'Tuvalu',
|
||||
UG: 'Uganda',
|
||||
UA: 'Ukraine',
|
||||
AE: 'United Arab Emirates (the)',
|
||||
GB: 'United Kingdom of Great Britain and Northern Ireland (the)',
|
||||
UM: 'United States Minor Outlying Islands (the)',
|
||||
US: 'United States of America (the)',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VU: 'Vanuatu',
|
||||
VE: 'Venezuela (Bolivarian Republic of)',
|
||||
VN: 'Viet Nam',
|
||||
VG: 'Virgin Islands (British)',
|
||||
VI: 'Virgin Islands (U.S.)',
|
||||
WF: 'Wallis and Futuna',
|
||||
EH: 'Western Sahara',
|
||||
YE: 'Yemen',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
AX: 'Åland Islands',
|
||||
};
|
||||
|
||||
export default countryList;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { AsyncActionType } from './utils';
|
||||
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
|
||||
|
||||
// Register
|
||||
|
||||
export const registerNewUser = registrationInfo => ({
|
||||
type: REGISTER_NEW_USER.BASE,
|
||||
payload: { registrationInfo },
|
||||
});
|
||||
|
||||
export const registerNewUserBegin = () => ({
|
||||
type: REGISTER_NEW_USER.BEGIN,
|
||||
});
|
||||
|
||||
export const registerNewUserSuccess = () => ({
|
||||
type: REGISTER_NEW_USER.SUCCESS,
|
||||
});
|
||||
|
||||
export const registerNewUserFailure = () => ({
|
||||
type: REGISTER_NEW_USER.FAILURE,
|
||||
});
|
||||
|
||||
// Login
|
||||
export const loginRequest = creds => ({
|
||||
type: LOGIN_REQUEST.BASE,
|
||||
payload: { creds },
|
||||
});
|
||||
|
||||
export const loginRequestBegin = () => ({
|
||||
type: LOGIN_REQUEST.BEGIN,
|
||||
});
|
||||
|
||||
export const loginRequestSuccess = () => ({
|
||||
type: LOGIN_REQUEST.SUCCESS,
|
||||
});
|
||||
|
||||
export const loginRequestFailure = () => ({
|
||||
type: LOGIN_REQUEST.FAILURE,
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
REGISTER_NEW_USER,
|
||||
LOGIN_REQUEST,
|
||||
} from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
registrationResult: {},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case REGISTER_NEW_USER.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case REGISTER_NEW_USER.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case REGISTER_NEW_USER.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case LOGIN_REQUEST.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case LOGIN_REQUEST.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case LOGIN_REQUEST.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
REGISTER_NEW_USER,
|
||||
registerNewUserBegin,
|
||||
registerNewUserFailure,
|
||||
registerNewUserSuccess,
|
||||
LOGIN_REQUEST,
|
||||
loginRequestBegin,
|
||||
loginRequestFailure,
|
||||
loginRequestSuccess,
|
||||
} from './actions';
|
||||
|
||||
|
||||
// Services
|
||||
import { postNewUser, login } from './service';
|
||||
|
||||
export function* handleNewUserRegistration(action) {
|
||||
try {
|
||||
yield put(registerNewUserBegin());
|
||||
|
||||
yield call(postNewUser, action.payload.registrationInfo);
|
||||
|
||||
yield put(registerNewUserSuccess());
|
||||
} catch (e) {
|
||||
yield put(registerNewUserFailure());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleLoginRequest(action) {
|
||||
try {
|
||||
yield put(loginRequestBegin());
|
||||
|
||||
yield call(login, action.payload.creds);
|
||||
|
||||
yield put(loginRequestSuccess());
|
||||
} catch (e) {
|
||||
yield put(loginRequestFailure());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
|
||||
yield takeEvery(LOGIN_REQUEST.BASE, handleLoginRequest);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getHttpClient } from '@edx/frontend-platform/auth';
|
||||
import querystring from 'querystring';
|
||||
|
||||
export async function postNewUser(registrationInformation) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
|
||||
const { data } = await getHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
querystring.stringify(registrationInformation),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
console.log('You messed up');
|
||||
throw (e);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function login(creds) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
|
||||
const { data } = await getHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
creds,
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
console.log('You messed up');
|
||||
throw (e);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;
|
||||
@@ -1,38 +0,0 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
// If the passed in object is not an object, return it.
|
||||
if (
|
||||
object === undefined ||
|
||||
object === null ||
|
||||
(typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
} from './dataUtils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
export {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
modifyObjectKeys,
|
||||
snakeCaseObject,
|
||||
} from './dataUtils';
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
export { default as handleFailure } from './sagaUtils';
|
||||
export { unpackFieldErrors, handleRequestError } from './serviceUtils';
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a state tree and an array representing a set of keys to traverse in that tree, returns
|
||||
* the portion of the tree at that key path.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* const result = getModuleState(
|
||||
* {
|
||||
* first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
* second: { other: 'data', }
|
||||
* },
|
||||
* ['first', 'red']
|
||||
* );
|
||||
*
|
||||
* result will be:
|
||||
*
|
||||
* {
|
||||
* awesome: 'sauce'
|
||||
* }
|
||||
*/
|
||||
export function getModuleState(state, originalPath) {
|
||||
const path = [...originalPath]; // don't modify your argument
|
||||
if (path.length < 1) {
|
||||
return state;
|
||||
}
|
||||
const key = path.shift();
|
||||
if (state[key] === undefined) {
|
||||
throw new Error(`Unexpected state key ${key} given to getModuleState. Is your state path set up correctly?`);
|
||||
}
|
||||
return getModuleState(state[key], path);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
||||
|
||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleState', () => {
|
||||
const state = {
|
||||
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
second: { other: 'data' },
|
||||
};
|
||||
|
||||
it('should return everything if given an empty path', () => {
|
||||
expect(getModuleState(state, [])).toEqual(state);
|
||||
});
|
||||
|
||||
it('should resolve paths correctly', () => {
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first'],
|
||||
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
|
||||
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first', 'red'],
|
||||
)).toEqual({ awesome: 'sauce' });
|
||||
|
||||
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
|
||||
});
|
||||
|
||||
it('should throw an exception on a bad path', () => {
|
||||
expect(() => {
|
||||
getModuleState(state, ['uhoh']);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should return non-objects correctly', () => {
|
||||
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
|
||||
if (error.fieldErrors && failureAction !== null) {
|
||||
yield put(failureAction({ fieldErrors: error.fieldErrors }));
|
||||
}
|
||||
logError(error);
|
||||
if (failureAction !== null) {
|
||||
yield put(failureAction(error.message));
|
||||
}
|
||||
if (failureRedirectPath !== null) {
|
||||
history.push(failureRedirectPath);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Turns field errors of the form:
|
||||
*
|
||||
* {
|
||||
* "name":{
|
||||
* "developer_message": "Nerdy message here",
|
||||
* "user_message": "This value is invalid."
|
||||
* },
|
||||
* "other_field": {
|
||||
* "developer_message": "Other Nerdy message here",
|
||||
* "user_message": "This other value is invalid."
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Into:
|
||||
*
|
||||
* {
|
||||
* "name": "This value is invalid.",
|
||||
* "other_field": "This other value is invalid"
|
||||
* }
|
||||
*/
|
||||
export function unpackFieldErrors(fieldErrors) {
|
||||
return Object.entries(fieldErrors).reduce((acc, [k, v]) => {
|
||||
acc[k] = v.user_message;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes and re-throws request errors. If the response contains a field_errors field, will
|
||||
* massage the data into a form expected by the client.
|
||||
*
|
||||
* Field errors will be packaged as an api error with a fieldErrors field usable by the client.
|
||||
* Takes an optional unpack function which is used to process the field errors,
|
||||
* otherwise uses the default unpackFieldErrors function.
|
||||
*
|
||||
* @param error The original error object.
|
||||
* @param unpackFunction (Optional) A function to use to unpack the field errors as a replacement
|
||||
* for the default.
|
||||
*/
|
||||
export function handleRequestError(error, unpackFunction = unpackFieldErrors) {
|
||||
if (error.response && error.response.data.field_errors) {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = unpackFunction(error.response.data.field_errors);
|
||||
throw apiError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as LoginPage } from './LoginPage';
|
||||
export { RegistrationPage } from './RegistrationPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
Reference in New Issue
Block a user