This commit is contained in:
Rick Reilly
2020-02-13 12:47:15 -05:00
parent 75ff0f8079
commit 6721869a2d
6 changed files with 32 additions and 461 deletions

View File

@@ -35,7 +35,7 @@ class RegistrationPage extends React.Component {
}
handleSubmit = (e) => {
console.log('clicked submit', e);
console.log('submit', e);
e.preventDefault();
}
@@ -47,12 +47,12 @@ class RegistrationPage extends React.Component {
}
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;
const inputErrors = this.state.errors;
let { emailValid } = this.state;
let { nameValid } = this.state;
let { usernameValid } = this.state;
let { passwordValid } = this.state;
let { countryValid } = this.state;
switch (inputName) {
case 'email':
@@ -218,4 +218,4 @@ class RegistrationPage extends React.Component {
}
}
export default RegistrationPage;
export default connect(RegistrationPage);

View File

@@ -4,21 +4,19 @@ export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_N
// SAVE SETTINGS ACTIONS
export const registerNewUser = (formId, commitValues) => ({
export const registerNewUser = registrationInfo => ({
type: REGISTER_NEW_USER.BASE,
payload: { formId, commitValues },
payload: { registrationInfo },
});
export const registerNewUserBegin = () => ({
type: REGISTER_NEW_USER.BEGIN,
});
export const registerNewUserSuccess = (values, confirmationValues) => ({
export const registerNewUserSuccess = () => ({
type: REGISTER_NEW_USER.SUCCESS,
payload: { values, confirmationValues },
});
export const registerNewUserFailure = ({ fieldErrors, message }) => ({
export const registerNewUserFailure = () => ({
type: REGISTER_NEW_USER.FAILURE,
payload: { errors: fieldErrors, message },
});

View File

@@ -2,11 +2,6 @@ import {
REGISTER_NEW_USER,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
export const defaultState = {
registrationResult: {},
};

View File

@@ -1,118 +1,30 @@
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';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { call, put, takeEvery } from 'redux-saga/effects';
// Actions
import {
FETCH_SETTINGS,
fetchSettingsBegin,
fetchSettingsSuccess,
fetchSettingsFailure,
closeForm,
SAVE_SETTINGS,
saveSettingsBegin,
saveSettingsSuccess,
saveSettingsFailure,
savePreviousSiteLanguage,
FETCH_TIME_ZONES,
fetchTimeZones,
fetchTimeZonesSuccess,
REGISTER_NEW_USER,
registerNewUserBegin,
registerNewUserFailure,
registerNewUserSuccess,
} from './actions';
// Sub-modules
import { saga as deleteAccountSaga } from '../delete-account';
import { saga as resetPasswordSaga } from '../reset-password';
import {
saga as siteLanguageSaga,
patchPreferences,
postSetLang,
} from '../site-language';
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
// Services
import { getSettings, patchSettings, getTimeZones } from './service';
import { postNewUser } from './service';
export function* handleFetchSettings() {
export function* handleNewUserRegistration(action) {
try {
yield put(fetchSettingsBegin());
const { username, roles: userRoles } = getAuthenticatedUser();
yield put(registerNewUserBegin());
const {
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
} = yield call(
getSettings,
username,
userRoles,
);
yield call(postNewUser, action.payload.registrationInfo);
if (values.country) yield put(fetchTimeZones(values.country));
yield put(fetchSettingsSuccess({
values,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
}));
yield put(registerNewUserSuccess());
} catch (e) {
yield put(fetchSettingsFailure(e.message));
yield put(registerNewUserFailure());
throw e;
}
}
export function* handleSaveSettings(action) {
try {
yield put(saveSettingsBegin());
const { username } = getAuthenticatedUser();
const { commitValues, formId } = action.payload;
const commitData = { [formId]: commitValues };
let savedValues = null;
if (formId === 'siteLanguage') {
const previousSiteLanguage = getLocale();
// The following two requests need to be done sequentially, with patching preferences before
// the post to setlang. They used to be done in parallel, but this might create ambiguous
// behavior.
yield call(patchPreferences, username, { prefLang: commitValues });
yield call(postSetLang, commitValues);
yield put(savePreviousSiteLanguage(previousSiteLanguage));
publish(LOCALE_CHANGED, getLocale());
handleRtl();
savedValues = commitData;
} else {
savedValues = yield call(patchSettings, username, commitData);
}
yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
yield delay(1000);
yield put(closeForm(action.payload.formId));
} catch (e) {
if (e.fieldErrors) {
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
} else {
yield put(saveSettingsFailure(e.message));
throw e;
}
}
}
export function* handleFetchTimeZones(action) {
const response = yield call(getTimeZones, action.payload.country);
yield put(fetchTimeZonesSuccess(response, action.payload.country));
}
export default function* saga() {
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
yield all([
deleteAccountSaga(),
siteLanguageSaga(),
resetPasswordSaga(),
thirdPartyAuthSaga(),
]);
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
}

View File

@@ -1,161 +0,0 @@
import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
export const storeName = 'accountSettings';
export const accountSettingsSelector = state => ({ ...state[storeName] });
const editableFieldNameSelector = (state, props) => props.name;
const valuesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.values,
);
const draftsSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.drafts,
);
const previousSiteLanguageSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.previousSiteLanguage,
);
const editableFieldErrorSelector = createSelector(
editableFieldNameSelector,
accountSettingsSelector,
(name, accountSettings) => accountSettings.errors[name],
);
const editableFieldConfirmationValuesSelector = createSelector(
editableFieldNameSelector,
accountSettingsSelector,
(name, accountSettings) => accountSettings.confirmationValues[name],
);
const isEditingSelector = createSelector(
editableFieldNameSelector,
accountSettingsSelector,
(name, accountSettings) => accountSettings.openFormId === name,
);
const saveStateSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.saveState,
);
export const editableFieldSelector = createStructuredSelector({
error: editableFieldErrorSelector,
confirmationValue: editableFieldConfirmationValuesSelector,
saveState: saveStateSelector,
isEditing: isEditingSelector,
});
export const profileDataManagerSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.profileDataManager,
);
export const staticFieldsSelector = createSelector(
accountSettingsSelector,
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
);
export const hiddenFieldsSelector = createSelector(
accountSettingsSelector,
accountSettings => (accountSettings.profileDataManager ? [] : ['secondary_email']),
);
/**
* If there's no draft present at all (undefined), use the original committed value.
*/
function chooseFormValue(draft, committed) {
return draft !== undefined ? draft : committed;
}
const formValuesSelector = createSelector(
valuesSelector,
draftsSelector,
(values, drafts) => {
const formValues = {};
Object.entries(values).forEach(([name, value]) => {
formValues[name] = chooseFormValue(drafts[name], value) || '';
});
return formValues;
},
);
const transformTimeZonesToOptions = timeZoneArr => timeZoneArr
.map(({ time_zone, description }) => ({ // eslint-disable-line camelcase
value: time_zone, label: description,
}));
const timeZonesSelector = createSelector(
accountSettingsSelector,
accountSettings => transformTimeZonesToOptions(accountSettings.timeZones),
);
const countryTimeZonesSelector = createSelector(
accountSettingsSelector,
accountSettings => transformTimeZonesToOptions(accountSettings.countryTimeZones),
);
const activeAccountSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.values.is_active,
);
export const siteLanguageSelector = createSelector(
previousSiteLanguageSelector,
draftsSelector,
(previousValue, drafts) => ({
previousValue,
draft: drafts.siteLanguage,
}),
);
export const betaLanguageBannerSelector = createStructuredSelector({
siteLanguageList: siteLanguageListSelector,
siteLanguage: siteLanguageSelector,
});
export const accountSettingsPageSelector = createSelector(
accountSettingsSelector,
siteLanguageOptionsSelector,
siteLanguageSelector,
formValuesSelector,
profileDataManagerSelector,
staticFieldsSelector,
hiddenFieldsSelector,
timeZonesSelector,
countryTimeZonesSelector,
activeAccountSelector,
(
accountSettings,
siteLanguageOptions,
siteLanguage,
formValues,
profileDataManager,
staticFields,
hiddenFields,
timeZoneOptions,
countryTimeZoneOptions,
activeAccount,
) => ({
siteLanguageOptions,
siteLanguage,
loading: accountSettings.loading,
loaded: accountSettings.loaded,
loadingError: accountSettings.loadingError,
timeZoneOptions,
countryTimeZoneOptions,
isActive: activeAccount,
formValues,
profileDataManager,
staticFields,
hiddenFields,
tpaProviders: accountSettings.thirdPartyAuth.providers,
}),
);

View File

@@ -1,194 +1,21 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import omit from 'lodash.omit';
import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
function unpackAccountResponseData(data) {
const unpackedData = data;
// This is handled by preferences
delete unpackedData.time_zone;
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
const platformData = data.social_links.find(({ platform }) => platform === id);
unpackedData[key] = typeof platformData === 'object' ? platformData.social_link : '';
});
if (Array.isArray(data.language_proficiencies)) {
if (data.language_proficiencies.length) {
unpackedData.language_proficiencies = data.language_proficiencies[0].code;
} else {
unpackedData.language_proficiencies = '';
}
}
return unpackedData;
}
function packAccountCommitData(commitData) {
const packedData = commitData;
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
// Skip missing values. Empty strings are valid values and should be preserved.
if (commitData[key] === undefined) return;
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
delete packedData[key];
});
if (commitData.language_proficiencies !== undefined) {
if (commitData.language_proficiencies) {
packedData.language_proficiencies = [{ code: commitData.language_proficiencies }];
} else {
// An empty string should be sent as an array.
packedData.language_proficiencies = [];
}
}
if (commitData.year_of_birth !== undefined) {
if (commitData.year_of_birth) {
packedData.year_of_birth = commitData.year_of_birth;
} else {
// An empty string should be sent as null.
packedData.year_of_birth = null;
}
}
return packedData;
}
export async function getAccount(username) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
return unpackAccountResponseData(data);
}
export async function patchAccount(username, commitValues) {
export default async function postNewUser(registrationInformation) {
const requestConfig = {
headers: { 'Content-Type': 'application/merge-patch+json' },
headers: { 'Content-Type': 'application/json' },
};
const { data } = await getAuthenticatedHttpClient()
.patch(
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`,
packAccountCommitData(commitValues),
.post(
`${getConfig().LMS_BASE_URL}user_api/v1/account/registration/`,
registrationInformation,
requestConfig,
)
.catch((error) => {
const unpackFunction = (fieldErrors) => {
const unpackedFieldErrors = fieldErrors;
if (fieldErrors.social_links) {
SOCIAL_PLATFORMS.forEach(({ key }) => {
unpackedFieldErrors[key] = fieldErrors.social_links;
});
}
return unpackFieldErrors(unpackedFieldErrors);
};
handleRequestError(error, unpackFunction);
.catch((e) => {
console.log('You messed up');
throw (e);
});
return unpackAccountResponseData(data);
}
export async function getPreferences(username) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return data;
}
export async function patchPreferences(username, commitValues) {
const requestConfig = { headers: { 'Content-Type': 'application/merge-patch+json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`;
// Ignore the success response, the API does not currently return any data.
await getAuthenticatedHttpClient()
.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
return commitValues;
}
export async function getTimeZones(forCountry) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
params: { country_code: forCountry },
})
.catch(handleRequestError);
return data;
}
/**
* Determine if the user's profile data is managed by a third-party identity provider.
*/
export async function getProfileDataManager(username, userRoles) {
const userRoleNames = userRoles.map(role => role.split(':')[0]);
if (userRoleNames.includes('enterprise_learner')) {
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
if ('results' in data) {
for (let i = 0; i < data.results.length; i += 1) {
const enterprise = data.results[i].enterprise_customer;
if (enterprise.sync_learner_profile_data) {
return enterprise.name;
}
}
}
}
return null;
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
*/
export async function getSettings(username, userRoles) {
const results = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
getTimeZones(),
]);
return {
...results[0],
...results[1],
thirdPartyAuthProviders: results[2],
profileDataManager: results[3],
timeZones: results[4],
};
}
/**
* A single function to PATCH everything considered a setting.
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
*/
export async function patchSettings(username, commitValues) {
// 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 accountCommitValues = omit(commitValues, preferenceKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
patchRequests.push(patchAccount(username, accountCommitValues));
}
if (!isEmpty(preferenceCommitValues)) {
patchRequests.push(patchPreferences(username, preferenceCommitValues));
}
const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys
// will override account keys. Notably time_zone.
const combinedResults = Object.assign({}, ...results);
return combinedResults;
}