Made requested changes and additional fixes

This commit is contained in:
Thomas Tracy
2020-03-13 14:46:25 -04:00
parent 7d8e2f2f85
commit c40222973c
11 changed files with 170 additions and 97 deletions

View File

@@ -17,4 +17,4 @@ SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
TEMP_COACHING_FEATURE_FLAG=true
COACHING_ENABLED=true

5
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -13,7 +13,7 @@ import {
getCountryList,
getLanguageList,
} from '@edx/frontend-platform/i18n';
import { Hyperlink, Input, ValidationFormGroup } from '@edx/paragon';
import { Hyperlink } from '@edx/paragon';
import messages from './AccountSettingsPage.messages';
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
@@ -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,44 +328,13 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
{process.env.TEMP_COACHING_FEATURE_FLAG &&
<>
<EditableField
name="phone_number"
type="text"
value={this.props.formValues.phone_number}
label={this.props.intl.formatMessage(messages['account.settings.field.phone_number'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
{...editableFieldProps}
/>
<ValidationFormGroup
for="coachingConsent"
helpText={this.props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
invalid={
!this.props.formValues.phone_number && this.props.formValues.coaching_consent
}
invalidMessage={this.props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
className="custom-control custom-switch"
>
<Input
name="coaching_consent"
className="custom-control-input"
type="checkbox"
id="coachingConsent"
checked={this.props.formValues.coaching_consent}
value={this.props.formValues.coaching_consent}
onChange={(e) => {
this.handleEditableFieldChange(e.target.name, e.target.checked);
if (this.props.formValues.phone_number) {
this.handleSubmit(e.target.name, e.target.checked);
} else {
this.handleSubmit(e.target.name, false);
}
}}
/>
<label className="custom-control-label" htmlFor="coachingConsent">{this.props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
</ValidationFormGroup>
</>
{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>
@@ -519,6 +489,11 @@ AccountSettingsPage.propTypes = {
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,
}).isRequired,
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,

View File

@@ -257,31 +257,6 @@ const messages = defineMessages({
defaultMessage: 'Other',
description: 'The label for catch-all gender option.',
},
'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 phone number is required to sign up for coaching',
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
},
'account.settings.field.language.proficiencies': {
id: 'account.settings.field.language.proficiencies',
defaultMessage: 'Spoken languages',

View 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));

View 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;

View 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;
}

View File

@@ -1,4 +1,4 @@
import { call, put, delay, takeEvery, all, debounce } from 'redux-saga/effects';
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
import { publish } from '@edx/frontend-platform';
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
@@ -108,7 +108,7 @@ export function* handleFetchTimeZones(action) {
export default function* saga() {
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
yield debounce(500, SAVE_SETTINGS.BASE, handleSaveSettings);
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
yield all([
deleteAccountSaga(),

View File

@@ -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' },
@@ -149,34 +150,6 @@ export async function getProfileDataManager(username, userRoles) {
return null;
}
/**
* 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.coaching_consent;
}
/**
* patch all of the settings related to coaching.
* @param {Number} userId users are identified in the api by LMS id
* @param {Object} commitValues { coaching_consent }
*/
export async function patchCoachingPreferences(userId, commitValues) {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
const body = {
...commitValues,
user: userId,
};
const options = { headers: { 'Content-Type': 'application/json' } };
getAuthenticatedHttpClient()
.patch(requestUrl, body, options);
return commitValues;
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
@@ -197,7 +170,7 @@ export async function getSettings(username, userRoles, userId) {
thirdPartyAuthProviders: results[2],
profileDataManager: results[3],
timeZones: results[4],
coaching_consent: results[5],
coaching: results[5],
};
}
@@ -210,7 +183,7 @@ export async function patchSettings(username, commitValues, userId) {
// 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_consent'];
const coachingKeys = ['coaching'];
const accountCommitValues = omit(commitValues, preferenceKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);

View File

@@ -49,6 +49,7 @@ initialize({
config: () => {
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL,
COACHING_ENABLED: process.env.COACHING_ENABLED,
}, 'App loadConfig override handler');
},
},