First pass - realigning/simplifying/refactoring (#33)
Realigning and simplifying directories and naming. - Combining “containers” into “components”. - Flattening out “data” into “reducers” and “config” to consolidate configuration-like files in one place and to make reducers a peer of its teammates (components, actions, sagas, and services). - Creating dev/prod-specific redux configurations. - Converting “index.jsx” files into files named for their contents. - Splitting up the top-level “index.jsx” file into an entry point and an “App” component. - Renaming SCSS file to “index.scss” to keep it consistent with where it’s imported. - Renaming/simplifying some variables.
This commit is contained in:
@@ -1,29 +1,42 @@
|
||||
import AsyncActionType from './AsyncActionType';
|
||||
|
||||
export const EDITABLE_FIELD_OPEN = 'EDITABLE_FIELD_OPEN';
|
||||
export const EDITABLE_FIELD_CLOSE = 'EDITABLE_FIELD_CLOSE';
|
||||
export const FIELD_OPEN = 'FIELD_OPEN';
|
||||
export const FIELD_CLOSE = 'FIELD_CLOSE';
|
||||
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
|
||||
export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE');
|
||||
export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO');
|
||||
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
|
||||
export const UPDATE_DRAFTS = 'UPDATE_DRAFTS';
|
||||
export const RECEIVE_PREFERENCES = 'RECEIVE_PREFERENCES';
|
||||
|
||||
export const openEditableField = fieldName => ({
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
|
||||
export const openField = fieldName => ({
|
||||
type: FIELD_OPEN,
|
||||
fieldName,
|
||||
});
|
||||
|
||||
export const closeEditableField = fieldName => ({
|
||||
type: EDITABLE_FIELD_CLOSE,
|
||||
export const closeField = fieldName => ({
|
||||
type: FIELD_CLOSE,
|
||||
fieldName,
|
||||
});
|
||||
|
||||
export const updateDrafts = drafts => ({
|
||||
type: UPDATE_DRAFTS,
|
||||
drafts,
|
||||
});
|
||||
|
||||
export const fetchProfileBegin = () => ({
|
||||
type: FETCH_PROFILE.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchProfileSuccess = profile => ({
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
payload: { profile },
|
||||
profile,
|
||||
});
|
||||
|
||||
export const receivePreferences = preferences => ({
|
||||
type: RECEIVE_PREFERENCES,
|
||||
preferences,
|
||||
});
|
||||
|
||||
export const fetchProfileFailure = error => ({
|
||||
@@ -57,12 +70,13 @@ export const saveProfileFailure = error => ({
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const saveProfile = (username, userAccountState, fieldName) => ({
|
||||
export const saveProfile = (username, { profileData, preferencesData }, fieldName) => ({
|
||||
type: SAVE_PROFILE.BASE,
|
||||
payload: {
|
||||
fieldName,
|
||||
username,
|
||||
userAccountState,
|
||||
profileData,
|
||||
preferencesData,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
EDITABLE_FIELD_OPEN,
|
||||
EDITABLE_FIELD_CLOSE,
|
||||
openField,
|
||||
closeField,
|
||||
FIELD_OPEN,
|
||||
FIELD_CLOSE,
|
||||
SAVE_PROFILE,
|
||||
saveProfileBegin,
|
||||
saveProfileSuccess,
|
||||
@@ -21,28 +21,28 @@ import {
|
||||
deleteProfilePhotoFailure,
|
||||
deleteProfilePhotoReset,
|
||||
deleteProfilePhoto,
|
||||
} from './profile';
|
||||
} from './ProfileActions';
|
||||
|
||||
describe('editable field actions', () => {
|
||||
it('should create an open action', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
type: FIELD_OPEN,
|
||||
fieldName: 'name',
|
||||
};
|
||||
expect(openEditableField('name')).toEqual(expectedAction);
|
||||
expect(openField('name')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create a closed action', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_CLOSE,
|
||||
type: FIELD_CLOSE,
|
||||
fieldName: 'name',
|
||||
};
|
||||
expect(closeEditableField('name')).toEqual(expectedAction);
|
||||
expect(closeField('name')).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAVE profile actions', () => {
|
||||
const userAccountState = {
|
||||
const profileData = {
|
||||
username: 'verified',
|
||||
email: 'verified@example.com',
|
||||
bio: 'A great bio.',
|
||||
@@ -51,16 +51,19 @@ describe('SAVE profile actions', () => {
|
||||
// Good enough for testing / and since we have no factories
|
||||
};
|
||||
|
||||
const preferencesData = {};
|
||||
|
||||
it('should create an action to signal the start of a profile save', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.BASE,
|
||||
payload: {
|
||||
username: 'user person',
|
||||
userAccountState,
|
||||
fieldName: 'fullName',
|
||||
profileData,
|
||||
preferencesData,
|
||||
},
|
||||
};
|
||||
expect(saveProfile('user person', userAccountState, 'fullName')).toEqual(expectedAction);
|
||||
expect(saveProfile('user person', { profileData, preferencesData }, 'fullName')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile save success', () => {
|
||||
@@ -188,17 +191,17 @@ describe('Editable field opening and closing actions', () => {
|
||||
|
||||
it('should create an action to signal the opening a field', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
type: FIELD_OPEN,
|
||||
fieldName,
|
||||
};
|
||||
expect(openEditableField(fieldName)).toEqual(expectedAction);
|
||||
expect(openField(fieldName)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal the closing a field', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_CLOSE,
|
||||
type: FIELD_CLOSE,
|
||||
fieldName,
|
||||
};
|
||||
expect(closeEditableField(fieldName)).toEqual(expectedAction);
|
||||
expect(closeField(fieldName)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import AsyncActionType from './AsyncActionType';
|
||||
|
||||
|
||||
export const FETCH_PREFERENCES = new AsyncActionType('PROFILE', 'FETCH_PREFERENCES');
|
||||
export const SAVE_PREFERENCES = new AsyncActionType('PROFILE', 'SAVE_PREFERENCES');
|
||||
|
||||
export const fetchPreferencesBegin = () => ({
|
||||
type: FETCH_PREFERENCES.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchPreferencesSuccess = preferences => ({
|
||||
type: FETCH_PREFERENCES.SUCCESS,
|
||||
preferences,
|
||||
});
|
||||
|
||||
export const fetchPreferencesFailure = error => ({
|
||||
type: FETCH_PREFERENCES.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const fetchPreferencesReset = () => ({
|
||||
type: FETCH_PREFERENCES.RESET,
|
||||
});
|
||||
|
||||
export const fetchPreferences = username => ({
|
||||
type: FETCH_PREFERENCES.BASE,
|
||||
payload: { username },
|
||||
});
|
||||
|
||||
|
||||
export const savePreferencesBegin = () => ({
|
||||
type: SAVE_PREFERENCES.BEGIN,
|
||||
});
|
||||
|
||||
export const savePreferencesSuccess = () => ({
|
||||
type: SAVE_PREFERENCES.SUCCESS,
|
||||
});
|
||||
|
||||
export const savePreferencesFailure = error => ({
|
||||
type: SAVE_PREFERENCES.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const savePreferencesReset = () => ({
|
||||
type: SAVE_PREFERENCES.RESET,
|
||||
});
|
||||
|
||||
export const savePreferences = (username, preferences) => ({
|
||||
type: SAVE_PREFERENCES.BASE,
|
||||
payload: { username, preferences },
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import apiClient from './data/apiClient';
|
||||
import apiClient from './config/apiClient';
|
||||
|
||||
const handleTrackEvents = (eventName, properties) => {
|
||||
// Simply forward track events to Segment
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
85
src/components/App.jsx
Normal file
85
src/components/App.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect, Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import SiteFooter from '@edx/frontend-component-footer';
|
||||
import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
|
||||
|
||||
import apiClient from '../config/apiClient';
|
||||
import { handleTrackEvents } from '../analytics';
|
||||
import { getLocale, getMessages } from '../i18n/i18n-loader';
|
||||
import SiteHeader from './/SiteHeader';
|
||||
import ConnectedProfilePage from './ProfilePage';
|
||||
|
||||
import HeaderLogo from '../../assets/edx-sm.png';
|
||||
import FooterLogo from '../../assets/edx-footer.png';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
const { username } = this.props;
|
||||
const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
|
||||
this.props.fetchUserAccount(userAccountApiService, username);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IntlProvider locale={getLocale()} messages={getMessages()}>
|
||||
<Provider store={this.props.store}>
|
||||
<Router>
|
||||
<div>
|
||||
<SiteHeader
|
||||
logo={HeaderLogo}
|
||||
logoDestination={process.env.MARKETING_SITE_BASE_URL}
|
||||
logoAltText={process.env.SITE_NAME}
|
||||
/>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ConnectedProfilePage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={process.env.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={process.env.SUPPORT_URL}
|
||||
contactUrl={process.env.CONTACT_URL}
|
||||
openSourceUrl={process.env.OPEN_SOURCE_URL}
|
||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||
facebookUrl={process.env.FACEBOOK_URL}
|
||||
twitterUrl={process.env.TWITTER_URL}
|
||||
youTubeUrl={process.env.YOU_TUBE_URL}
|
||||
linkedInUrl={process.env.LINKED_IN_URL}
|
||||
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
|
||||
redditUrl={process.env.REDDIT_URL}
|
||||
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
|
||||
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
|
||||
handleAllTrackEvents={handleTrackEvents}
|
||||
/>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
fetchUserAccount: PropTypes.func.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
store: PropTypes.object.isRequired, // eslint-disable-line
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
username: state.authentication.username,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
fetchUserAccount,
|
||||
},
|
||||
)(App);
|
||||
@@ -1,16 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Container, Row, Col } from 'reactstrap';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ProfileAvatar from './ProfileAvatar';
|
||||
import FullName from './FullName';
|
||||
import UserLocation from './UserLocation';
|
||||
import Education from './Education';
|
||||
import SocialLinks from './SocialLinks';
|
||||
import Bio from './Bio';
|
||||
import MyCertificates from './MyCertificates';
|
||||
// Actions
|
||||
import {
|
||||
fetchProfile,
|
||||
saveProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
openField,
|
||||
closeField,
|
||||
} from '../actions/ProfileActions';
|
||||
|
||||
class UserProfile extends React.Component {
|
||||
// Components
|
||||
import ProfileAvatar from './ProfilePage/ProfileAvatar';
|
||||
import FullName from './ProfilePage/FullName';
|
||||
import UserLocation from './ProfilePage/UserLocation';
|
||||
import Education from './ProfilePage/Education';
|
||||
import SocialLinks from './ProfilePage/SocialLinks';
|
||||
import Bio from './ProfilePage/Bio';
|
||||
import MyCertificates from './ProfilePage/MyCertificates';
|
||||
|
||||
export class ProfilePage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,7 +35,6 @@ class UserProfile extends React.Component {
|
||||
certificates: { value: null, visibility: null },
|
||||
};
|
||||
|
||||
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
this.onEdit = this.onEdit.bind(this);
|
||||
this.onSave = this.onSave.bind(this);
|
||||
@@ -38,45 +49,32 @@ class UserProfile extends React.Component {
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.props.closeEditableField(this.props.currentlyEditingField);
|
||||
this.props.closeField(this.props.currentlyEditingField);
|
||||
}
|
||||
|
||||
onEdit(fieldName) {
|
||||
this.props.openEditableField(fieldName);
|
||||
this.props.openField(fieldName);
|
||||
}
|
||||
|
||||
onSave(fieldName, value, visibility) {
|
||||
onSave(fieldName) {
|
||||
const {
|
||||
value: fieldValue,
|
||||
visibility: fieldVisibility,
|
||||
value,
|
||||
visibility,
|
||||
} = this.state[fieldName];
|
||||
|
||||
const valueToSave = value != null ? value : fieldValue;
|
||||
const visibilityToSave = visibility != null ? visibility : fieldVisibility;
|
||||
const data = {};
|
||||
|
||||
if (valueToSave != null) {
|
||||
this.props.saveProfile(
|
||||
this.props.username,
|
||||
{
|
||||
[fieldName]: valueToSave,
|
||||
},
|
||||
fieldName,
|
||||
);
|
||||
if (value != null) {
|
||||
data.profileData = { [fieldName]: value };
|
||||
}
|
||||
if (visibility != null) {
|
||||
data.preferencesData = { visibility: { [fieldName]: visibility } };
|
||||
}
|
||||
|
||||
if (visibilityToSave != null) {
|
||||
this.props.savePreferences(
|
||||
this.props.username,
|
||||
{
|
||||
visibility: {
|
||||
[fieldName]: visibilityToSave,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (valueToSave == null && visibilityToSave == null) {
|
||||
if (value == null && visibility == null) {
|
||||
this.onCancel();
|
||||
} else {
|
||||
this.props.saveProfile(this.props.username, data, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +94,7 @@ class UserProfile extends React.Component {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onVisibilityChange(fieldName, visibility) {
|
||||
this.setState({
|
||||
[fieldName]: {
|
||||
@@ -233,9 +232,7 @@ class UserProfile extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default UserProfile;
|
||||
|
||||
UserProfile.propTypes = {
|
||||
ProfilePage.propTypes = {
|
||||
currentlyEditingField: PropTypes.string,
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
@@ -255,13 +252,13 @@ UserProfile.propTypes = {
|
||||
certificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
saveProfile: PropTypes.func.isRequired,
|
||||
saveProfilePhoto: PropTypes.func.isRequired,
|
||||
deleteProfilePhoto: PropTypes.func.isRequired,
|
||||
openEditableField: PropTypes.func.isRequired,
|
||||
closeEditableField: PropTypes.func.isRequired,
|
||||
savePreferences: PropTypes.func.isRequired,
|
||||
openField: PropTypes.func.isRequired,
|
||||
closeField: PropTypes.func.isRequired,
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
@@ -271,7 +268,7 @@ UserProfile.propTypes = {
|
||||
visibility: PropTypes.object, // eslint-disable-line
|
||||
};
|
||||
|
||||
UserProfile.defaultProps = {
|
||||
ProfilePage.defaultProps = {
|
||||
currentlyEditingField: null,
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
@@ -288,3 +285,39 @@ UserProfile.defaultProps = {
|
||||
accountPrivacy: null,
|
||||
visibility: {}, // eslint-disable-line
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const profileImage =
|
||||
state.profilePage.profile.profileImage != null
|
||||
? state.profilePage.profile.profileImage.imageUrlLarge
|
||||
: null;
|
||||
return {
|
||||
isCurrentUserProfile: state.userAccount.username === state.profilePage.profile.username,
|
||||
currentlyEditingField: state.profilePage.currentlyEditingField,
|
||||
saveState: state.profilePage.saveState,
|
||||
savePhotoState: state.profilePage.savePhotoState,
|
||||
error: state.profilePage.error,
|
||||
profileImage,
|
||||
fullName: state.profilePage.profile.name,
|
||||
username: state.profilePage.profile.username,
|
||||
userLocation: state.profilePage.profile.country,
|
||||
education: state.profilePage.profile.levelOfEducation,
|
||||
socialLinks: state.profilePage.profile.socialLinks,
|
||||
bio: state.profilePage.profile.bio,
|
||||
certificates: state.profilePage.profile.certificates,
|
||||
accountPrivacy: state.profilePage.preferences.accountPrivacy,
|
||||
visibility: state.profilePage.preferences.visibility || {},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
fetchProfile,
|
||||
saveProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
openField,
|
||||
closeField,
|
||||
},
|
||||
)(ProfilePage);
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
|
||||
|
||||
import { configuration } from '../config';
|
||||
|
||||
import { configuration } from './environment';
|
||||
|
||||
const apiClient = getAuthenticatedAPIClient({
|
||||
appBaseUrl: configuration.BASE_URL,
|
||||
@@ -14,5 +13,4 @@ const apiClient = getAuthenticatedAPIClient({
|
||||
csrfCookieName: configuration.CSRF_COOKIE_NAME,
|
||||
});
|
||||
|
||||
|
||||
export default apiClient;
|
||||
25
src/config/configureStore.dev.js
Normal file
25
src/config/configureStore.dev.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from '../reducers/RootReducer';
|
||||
import rootSaga from '../sagas/RootSaga';
|
||||
|
||||
export default function configureStore() {
|
||||
const loggerMiddleware = createLogger();
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const initialState = apiClient.getAuthenticationState();
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)),
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
return store;
|
||||
}
|
||||
9
src/config/configureStore.js
Normal file
9
src/config/configureStore.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { configuration } from './environment';
|
||||
import configureStoreProd from './configureStore.prod';
|
||||
import configureStoreDev from './configureStore.dev';
|
||||
|
||||
if (configuration.ENVIRONMENT === 'production') {
|
||||
module.exports = configureStoreProd;
|
||||
} else {
|
||||
module.exports = configureStoreDev;
|
||||
}
|
||||
22
src/config/configureStore.prod.js
Normal file
22
src/config/configureStore.prod.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from '../reducers/RootReducer';
|
||||
import rootSaga from '../sagas/RootSaga';
|
||||
|
||||
export default function configureStore() {
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const initialState = apiClient.getAuthenticationState();
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
applyMiddleware(thunkMiddleware, sagaMiddleware),
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
return store;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const configuration = {
|
||||
export const configuration = {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
@@ -10,8 +10,7 @@ const configuration = {
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
CSRF_COOKIE_NAME: process.env.CSRF_COOKIE_NAME,
|
||||
ENVIRONMENT: process.env.NODE_ENV,
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
export { configuration, features };
|
||||
export const features = {};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import UserProfile from '../../components/UserProfile';
|
||||
import {
|
||||
fetchProfile,
|
||||
saveProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
} from '../../actions/profile';
|
||||
import { savePreferences } from '../../actions/preferences';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const profileImage =
|
||||
state.profilePage.profile.profileImage != null
|
||||
? state.profilePage.profile.profileImage.imageUrlLarge
|
||||
: null;
|
||||
return {
|
||||
isCurrentUserProfile: state.userAccount.username === state.profilePage.profile.username,
|
||||
currentlyEditingField: state.profilePage.currentlyEditingField,
|
||||
saveState: state.profilePage.saveState,
|
||||
savePhotoState: state.profilePage.savePhotoState,
|
||||
error: state.profilePage.error,
|
||||
profileImage,
|
||||
fullName: state.profilePage.profile.name,
|
||||
username: state.profilePage.profile.username,
|
||||
userLocation: state.profilePage.profile.country,
|
||||
education: state.profilePage.profile.levelOfEducation,
|
||||
socialLinks: state.profilePage.profile.socialLinks,
|
||||
bio: state.profilePage.profile.bio,
|
||||
certificates: state.profilePage.profile.certificates,
|
||||
accountPrivacy: state.profilePage.preferences.accountPrivacy,
|
||||
visibility: state.profilePage.preferences.visibility || {},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
fetchProfile,
|
||||
saveProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
savePreferences,
|
||||
},
|
||||
)(UserProfile);
|
||||
@@ -1,22 +0,0 @@
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from './reducers/RootReducer';
|
||||
import rootSaga from '../sagas/RootSaga';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const initialState = apiClient.getAuthenticationState();
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)),
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
export default store;
|
||||
@@ -1,80 +1,20 @@
|
||||
import 'babel-polyfill';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import SiteFooter from '@edx/frontend-component-footer';
|
||||
import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
|
||||
|
||||
import { getLocale, getMessages } from './i18n/i18n-loader';
|
||||
import apiClient from './data/apiClient';
|
||||
import { handleTrackEvents, identifyUser } from './analytics';
|
||||
import SiteHeader from './containers/SiteHeader';
|
||||
import UserProfile from './containers/UserProfile';
|
||||
import store from './data/store';
|
||||
import HeaderLogo from '../assets/edx-sm.png';
|
||||
import FooterLogo from '../assets/edx-footer.png';
|
||||
import './App.scss';
|
||||
import NotFoundPage from './components/NotFoundPage';
|
||||
import configureStore from './config/configureStore';
|
||||
import apiClient from './config/apiClient';
|
||||
import { identifyUser } from './analytics';
|
||||
|
||||
import { fetchPreferences } from './actions/preferences';
|
||||
import './index.scss';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
const { username } = store.getState().authentication;
|
||||
const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
|
||||
store.dispatch(fetchUserAccount(userAccountApiService, username));
|
||||
store.dispatch(fetchPreferences(username));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IntlProvider locale={getLocale()} messages={getMessages()}>
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<SiteHeader
|
||||
logo={HeaderLogo}
|
||||
logoDestination={process.env.MARKETING_SITE_BASE_URL}
|
||||
logoAltText={process.env.SITE_NAME}
|
||||
/>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={UserProfile} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={process.env.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={process.env.SUPPORT_URL}
|
||||
contactUrl={process.env.CONTACT_URL}
|
||||
openSourceUrl={process.env.OPEN_SOURCE_URL}
|
||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||
facebookUrl={process.env.FACEBOOK_URL}
|
||||
twitterUrl={process.env.TWITTER_URL}
|
||||
youTubeUrl={process.env.YOU_TUBE_URL}
|
||||
linkedInUrl={process.env.LINKED_IN_URL}
|
||||
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
|
||||
redditUrl={process.env.REDDIT_URL}
|
||||
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
|
||||
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
|
||||
handleAllTrackEvents={handleTrackEvents}
|
||||
/>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
import App from './components/App';
|
||||
|
||||
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(<App store={store} />, document.getElementById('root'));
|
||||
|
||||
// identify user for future analytics calls
|
||||
// TODO: Call before each page call.
|
||||
|
||||
@@ -22,7 +22,7 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
.profile-page {
|
||||
.bg-banner {
|
||||
height: 12rem;
|
||||
background-image: url('./assets/dot-pattern-light.png');
|
||||
background-image: url('../assets/dot-pattern-light.png');
|
||||
background-repeat: repeat-x;
|
||||
background-size: auto 85%;
|
||||
}
|
||||
@@ -4,46 +4,30 @@ import {
|
||||
SAVE_PROFILE,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
EDITABLE_FIELD_CLOSE,
|
||||
EDITABLE_FIELD_OPEN,
|
||||
FIELD_CLOSE,
|
||||
FIELD_OPEN,
|
||||
FETCH_PROFILE,
|
||||
} from '../../actions/profile';
|
||||
RECEIVE_PREFERENCES,
|
||||
|
||||
import {
|
||||
FETCH_PREFERENCES,
|
||||
SAVE_PREFERENCES,
|
||||
} from '../../actions/preferences';
|
||||
} from '../actions/ProfileActions';
|
||||
|
||||
const initialState = {
|
||||
error: null,
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
savePreferencesState: null,
|
||||
saveProfileState: null,
|
||||
currentlyEditingField: null,
|
||||
profile: {},
|
||||
preferences: {},
|
||||
};
|
||||
|
||||
// This function returns state based on priority:
|
||||
// if any are pending > the state is pending
|
||||
// then, if any are errors > the state is error
|
||||
// then, if any are complete > the state is complete
|
||||
// else null
|
||||
const mergeSaveStates = (statesToMerge) => {
|
||||
const statePriority = ['pending', 'error', 'complete', null];
|
||||
statesToMerge.sort((a, b) => statePriority.indexOf(a) - statePriority.indexOf(b));
|
||||
return statesToMerge[0];
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case EDITABLE_FIELD_OPEN:
|
||||
case FIELD_OPEN:
|
||||
return {
|
||||
...state,
|
||||
currentlyEditingField: action.fieldName,
|
||||
};
|
||||
case EDITABLE_FIELD_CLOSE:
|
||||
case FIELD_CLOSE:
|
||||
// Only close if the field to close is undefined or matches the field that is currently open
|
||||
if (action.fieldName === state.currentlyEditingField) {
|
||||
return {
|
||||
@@ -53,71 +37,40 @@ const profilePage = (state = initialState, action) => {
|
||||
}
|
||||
return state;
|
||||
|
||||
case FETCH_PREFERENCES.SUCCESS:
|
||||
case FETCH_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
profile: action.profile,
|
||||
};
|
||||
|
||||
case RECEIVE_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: defaultsDeep({}, action.preferences, state.preferences),
|
||||
};
|
||||
|
||||
case SAVE_PREFERENCES.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePreferencesState: 'pending',
|
||||
saveState: mergeSaveStates(['pending', state.saveProfileState]),
|
||||
};
|
||||
case SAVE_PREFERENCES.SUCCESS:
|
||||
// defaults deep used because our preferences/state object is multi-dimensional
|
||||
return {
|
||||
...state,
|
||||
savePreferencesState: 'complete',
|
||||
saveState: mergeSaveStates(['complete', state.saveProfileState]),
|
||||
};
|
||||
case SAVE_PREFERENCES.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePreferencesState: 'error',
|
||||
saveState: mergeSaveStates(['error', state.saveProfileState]),
|
||||
};
|
||||
case SAVE_PREFERENCES.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePreferencesState: null,
|
||||
saveState: mergeSaveStates([null, state.saveProfileState]),
|
||||
error: null,
|
||||
};
|
||||
|
||||
case FETCH_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
profile: action.payload.profile,
|
||||
};
|
||||
|
||||
case SAVE_PROFILE.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveProfileState: 'pending',
|
||||
saveState: mergeSaveStates(['pending', state.savePreferencesState]),
|
||||
saveState: 'pending',
|
||||
error: null,
|
||||
};
|
||||
case SAVE_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveProfileState: 'complete',
|
||||
saveState: mergeSaveStates(['complete', state.savePreferencesState]),
|
||||
saveState: 'complete',
|
||||
error: null,
|
||||
};
|
||||
case SAVE_PROFILE.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveProfileState: 'error',
|
||||
saveState: mergeSaveStates(['error', state.savePreferencesState]),
|
||||
saveState: 'error',
|
||||
error: action.payload.error,
|
||||
};
|
||||
case SAVE_PROFILE.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveProfileState: null,
|
||||
saveState: mergeSaveStates([null, state.savePreferencesState]),
|
||||
saveState: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { userAccount } from '@edx/frontend-auth';
|
||||
|
||||
import profilePage from './ProfilePageReducer';
|
||||
|
||||
const identityReducer = (state) => {
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
FETCH_PROFILE,
|
||||
fetchProfileBegin,
|
||||
fetchProfileSuccess,
|
||||
receivePreferences,
|
||||
fetchProfileFailure,
|
||||
fetchProfileReset,
|
||||
fetchProfile as fetchProfileAction,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
saveProfileSuccess,
|
||||
saveProfileFailure,
|
||||
saveProfileReset,
|
||||
closeEditableField,
|
||||
closeField,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoSuccess,
|
||||
@@ -23,20 +24,7 @@ import {
|
||||
deleteProfilePhotoSuccess,
|
||||
deleteProfilePhotoFailure,
|
||||
deleteProfilePhotoReset,
|
||||
} from '../actions/profile';
|
||||
|
||||
import {
|
||||
FETCH_PREFERENCES,
|
||||
fetchPreferencesBegin,
|
||||
fetchPreferencesSuccess,
|
||||
fetchPreferencesFailure,
|
||||
fetchPreferencesReset,
|
||||
SAVE_PREFERENCES,
|
||||
savePreferencesBegin,
|
||||
savePreferencesSuccess,
|
||||
savePreferencesFailure,
|
||||
savePreferencesReset,
|
||||
} from '../actions/preferences';
|
||||
} from '../actions/ProfileActions';
|
||||
|
||||
import * as ProfileApiService from '../services/ProfileApiService';
|
||||
|
||||
@@ -61,22 +49,27 @@ export const mapDataForRequest = (props) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
export function* handleFetchProfile(action) {
|
||||
const { username } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(fetchProfileBegin());
|
||||
|
||||
const profile = yield call(
|
||||
ProfileApiService.getProfile,
|
||||
username,
|
||||
);
|
||||
const preferences = yield call(
|
||||
ProfileApiService.getPreferences,
|
||||
username,
|
||||
);
|
||||
profile.certificates = yield call(
|
||||
ProfileApiService.getCourseCertificates,
|
||||
username,
|
||||
);
|
||||
|
||||
yield put(fetchProfileSuccess(profile));
|
||||
yield put(receivePreferences(preferences));
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
yield put(fetchProfileFailure(e.message));
|
||||
@@ -84,19 +77,38 @@ export function* handleFetchProfile(action) {
|
||||
}
|
||||
|
||||
export function* handleSaveProfile(action) {
|
||||
const { username, userAccountState } = action.payload;
|
||||
const { username, profileData, preferencesData } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(saveProfileBegin());
|
||||
const profile = yield call(
|
||||
ProfileApiService.patchProfile,
|
||||
username,
|
||||
userAccountState,
|
||||
);
|
||||
const responseData = {};
|
||||
|
||||
if (profileData != null) {
|
||||
responseData.profile = yield call(
|
||||
ProfileApiService.patchProfile,
|
||||
username,
|
||||
profileData,
|
||||
);
|
||||
}
|
||||
if (preferencesData != null) {
|
||||
responseData.preferences = yield call(
|
||||
ProfileApiService.patchPreferences,
|
||||
username,
|
||||
preferencesData,
|
||||
);
|
||||
}
|
||||
|
||||
const { profile, preferences } = responseData;
|
||||
|
||||
yield put(saveProfileSuccess());
|
||||
yield put(fetchProfileSuccess(profile));
|
||||
if (profile != null) {
|
||||
yield put(fetchProfileSuccess(profile));
|
||||
}
|
||||
if (preferences != null) {
|
||||
yield put(receivePreferences(preferences));
|
||||
}
|
||||
yield delay(300);
|
||||
yield put(closeEditableField(action.payload.fieldName));
|
||||
yield put(closeField(action.payload.fieldName));
|
||||
yield delay(300);
|
||||
yield put(saveProfileReset());
|
||||
} catch (e) {
|
||||
@@ -138,37 +150,9 @@ export function* handleDeleteProfilePhoto(action) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleFetchPreferences(action) {
|
||||
const { username } = action.payload;
|
||||
try {
|
||||
yield put(fetchPreferencesBegin());
|
||||
const userPreferences = yield call(ProfileApiService.getPreferences, username);
|
||||
yield put(fetchPreferencesSuccess(userPreferences));
|
||||
yield put(fetchPreferencesReset());
|
||||
} catch (e) {
|
||||
yield put(fetchPreferencesFailure(e));
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleSavePreferences(action) {
|
||||
const { username, preferences: preferencesToSave } = action.payload;
|
||||
try {
|
||||
yield put(savePreferencesBegin());
|
||||
const preferences = yield call(ProfileApiService.postPreferences, username, preferencesToSave);
|
||||
yield put(savePreferencesSuccess());
|
||||
yield put(fetchPreferencesSuccess(preferences));
|
||||
yield put(savePreferencesReset());
|
||||
} catch (e) {
|
||||
yield put(savePreferencesFailure(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
|
||||
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
|
||||
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
|
||||
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
|
||||
yield takeEvery(FETCH_PREFERENCES.BASE, handleFetchPreferences);
|
||||
yield takeEvery(SAVE_PREFERENCES.BASE, handleSavePreferences);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { takeEvery, put, call, delay } from 'redux-saga/effects';
|
||||
|
||||
import * as profileActions from '../actions/profile';
|
||||
import * as preferencesActions from '../actions/preferences';
|
||||
import * as profileActions from '../actions/ProfileActions';
|
||||
|
||||
jest.mock('../services/ProfileApiService', () => ({
|
||||
getProfile: jest.fn(),
|
||||
@@ -18,8 +17,6 @@ import rootSaga, {
|
||||
handleSaveProfile,
|
||||
handleSaveProfilePhoto,
|
||||
handleDeleteProfilePhoto,
|
||||
handleFetchPreferences,
|
||||
handleSavePreferences,
|
||||
} from './RootSaga';
|
||||
import * as ProfileApiService from '../services/ProfileApiService';
|
||||
/* eslint-enable import/first */
|
||||
@@ -29,30 +26,10 @@ describe('RootSaga', () => {
|
||||
it('should pass actions to the correct sagas', () => {
|
||||
const gen = rootSaga();
|
||||
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
profileActions.FETCH_PROFILE.BASE,
|
||||
handleFetchProfile,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
profileActions.SAVE_PROFILE.BASE,
|
||||
handleSaveProfile,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
profileActions.SAVE_PROFILE_PHOTO.BASE,
|
||||
handleSaveProfilePhoto,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
profileActions.DELETE_PROFILE_PHOTO.BASE,
|
||||
handleDeleteProfilePhoto,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
preferencesActions.FETCH_PREFERENCES.BASE,
|
||||
handleFetchPreferences,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(
|
||||
preferencesActions.SAVE_PREFERENCES.BASE,
|
||||
handleSavePreferences,
|
||||
));
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto)); // eslint-disable-line
|
||||
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
@@ -63,8 +40,11 @@ describe('RootSaga', () => {
|
||||
const action = profileActions.saveProfile(
|
||||
'my username',
|
||||
{
|
||||
fullName: 'Full Name',
|
||||
education: 'b',
|
||||
profileData: {
|
||||
fullName: 'Full Name',
|
||||
education: 'b',
|
||||
},
|
||||
preferencesData: null,
|
||||
},
|
||||
'ze field',
|
||||
);
|
||||
@@ -74,13 +54,13 @@ describe('RootSaga', () => {
|
||||
levelOfEducation: 'b',
|
||||
};
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveProfileBegin()));
|
||||
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', action.payload.userAccountState));
|
||||
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', action.payload.profileData));
|
||||
// The library would supply the result of the above call
|
||||
// as the parameter to the NEXT yield. Here:
|
||||
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess()));
|
||||
expect(gen.next().value).toEqual(put(profileActions.fetchProfileSuccess(profile)));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
expect(gen.next().value).toEqual(put(profileActions.closeEditableField('ze field')));
|
||||
expect(gen.next().value).toEqual(put(profileActions.closeField('ze field')));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// The code in this file is from Segment's website, with the following update:
|
||||
// - Pulls the segment key from configuration.
|
||||
// https://segment.com/docs/sources/website/analytics.js/quickstart/
|
||||
import { configuration } from './config';
|
||||
import { configuration } from './config/environment';
|
||||
|
||||
(function(){
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
import apiClient from '../data/apiClient';
|
||||
import { configuration } from '../config';
|
||||
import apiClient from '../config/apiClient';
|
||||
import { configuration } from '../config/environment';
|
||||
import { unflattenAndTransformKeys, flattenAndTransformKeys } from './utils';
|
||||
|
||||
const accountsApiBaseUrl = `${configuration.LMS_BASE_URL}/api/user/v1/accounts`;
|
||||
@@ -18,6 +18,7 @@ const clientServerKeyMap = {
|
||||
dateJoined: 'date_joined',
|
||||
languageProficiencies: 'language_proficiencies',
|
||||
accountPrivacy: 'account_privacy',
|
||||
userLocation: 'user_location',
|
||||
};
|
||||
const serverClientKeyMap = Object.entries(clientServerKeyMap).reduce((acc, [key, value]) => {
|
||||
acc[value] = key;
|
||||
@@ -106,7 +107,7 @@ export function getPreferences(username) {
|
||||
});
|
||||
}
|
||||
|
||||
export function postPreferences(username, preferences) {
|
||||
export function patchPreferences(username, preferences) {
|
||||
const url = `${preferencesApiBaseUrl}/${username}`;
|
||||
|
||||
// Flatten object for server
|
||||
|
||||
Reference in New Issue
Block a user