Pull data down to UI

This commit is contained in:
Adam Butterworth
2019-02-20 15:13:36 -05:00
committed by Adam Butterworth
parent 5fe9494189
commit 5deff373ed
20 changed files with 210 additions and 129 deletions

View File

@@ -44,6 +44,7 @@
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"i18n-iso-countries": "^3.7.8",
"lodash": "^4.17.11",
"prop-types": "^15.5.10",
"query-string": "^5.1.1",
"react": "^16.2.0",

View File

@@ -0,0 +1,51 @@
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 },
});

View File

@@ -6,8 +6,6 @@ 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 FETCH_PREFERENCES = new AsyncActionType('PROFILE', 'FETCH_PREFERENCES');
export const SAVE_PREFERENCES = new AsyncActionType('PROFILE', 'SAVE_PREFERENCES');
export const openEditableField = fieldName => ({
type: EDITABLE_FIELD_OPEN,
@@ -116,50 +114,3 @@ export const deleteProfilePhoto = username => ({
username,
},
});
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 },
});

View File

@@ -11,6 +11,7 @@ import SwitchContent from './elements/SwitchContent';
function Bio({
bio,
visibility,
editMode,
onEdit,
onChange,
@@ -37,7 +38,7 @@ function Bio({
onCancel={() => onCancel('bio')}
onSave={() => onSave('bio')}
saveState={saveState}
visibility="Everyone"
visibility={visibility}
onVisibilityChange={e => onVisibilityChange('bio', e.target.value)}
/>
</React.Fragment>
@@ -49,7 +50,7 @@ function Bio({
showEditButton
onClickEdit={() => onEdit('bio')}
showVisibility={Boolean(bio)}
visibility="Everyone"
visibility={visibility}
/>
<p className="lead">{bio}</p>
</React.Fragment>
@@ -83,12 +84,14 @@ Bio.propTypes = {
onVisibilityChange: PropTypes.func.isRequired,
saveState: PropTypes.string,
bio: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
};
Bio.defaultProps = {
editMode: 'static',
saveState: null,
bio: null,
visibility: 'private',
};

View File

@@ -12,6 +12,7 @@ import EDUCATION from '../../constants/education';
function Education({
education,
visibility,
editMode,
onEdit,
onChange,
@@ -43,7 +44,7 @@ function Education({
onCancel={() => onCancel('education')}
onSave={() => onSave('education')}
saveState={saveState}
visibility="Everyone"
visibility={visibility}
onVisibilityChange={e => onVisibilityChange('education', e.target.value)}
/>
</React.Fragment>
@@ -55,7 +56,7 @@ function Education({
showEditButton
onClickEdit={() => onEdit('education')}
showVisibility={Boolean(education)}
visibility="Everyone"
visibility={visibility}
/>
<h5>{EDUCATION[education]}</h5>
</React.Fragment>
@@ -83,12 +84,14 @@ Education.propTypes = {
onVisibilityChange: PropTypes.func.isRequired,
saveState: PropTypes.string,
education: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
};
Education.defaultProps = {
editMode: 'static',
saveState: null,
education: null,
visibility: 'private',
};

View File

@@ -10,6 +10,7 @@ import SwitchContent from './elements/SwitchContent';
function FullName({
fullName,
visibility,
editMode,
onEdit,
onChange,
@@ -36,7 +37,7 @@ function FullName({
onCancel={() => onCancel('fullName')}
onSave={() => onSave('fullName')}
saveState={saveState}
visibility="Everyone"
visibility={visibility}
onVisibilityChange={e => onVisibilityChange('fullName', e.target.value)}
/>
</React.Fragment>
@@ -48,7 +49,7 @@ function FullName({
showEditButton
onClickEdit={() => onEdit('fullName')}
showVisibility={Boolean(fullName)}
visibility="Everyone"
visibility={visibility}
/>
<h5>{fullName}</h5>
</React.Fragment>
@@ -77,12 +78,14 @@ FullName.propTypes = {
onVisibilityChange: PropTypes.func.isRequired,
saveState: PropTypes.string,
fullName: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
};
FullName.defaultProps = {
editMode: 'static',
saveState: null,
fullName: null,
visibility: 'private',
};

View File

@@ -9,6 +9,7 @@ import SwitchContent from './elements/SwitchContent';
function MyCertificates({
certificates,
visibility,
editMode,
onEdit,
onSave,
@@ -47,7 +48,7 @@ function MyCertificates({
onCancel={() => onCancel('certificates')}
onSave={() => onSave('certificates')}
saveState={saveState}
visibility="Everyone"
visibility={visibility}
onVisibilityChange={e => onVisibilityChange('certificates', e.target.value)}
/>
</React.Fragment>
@@ -59,7 +60,7 @@ function MyCertificates({
showEditButton
onClickEdit={() => onEdit('certificates')}
showVisibility={Boolean(certificates)}
visibility="Everyone"
visibility={visibility}
/>
{renderCertificates()}
</React.Fragment>
@@ -95,12 +96,14 @@ MyCertificates.propTypes = {
certificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
visibility: PropTypes.oneOf(['private', 'all_users']),
};
MyCertificates.defaultProps = {
editMode: 'static',
saveState: null,
certificates: null,
visibility: 'private',
};

View File

@@ -79,7 +79,7 @@ class SocialLinks extends React.Component {
onCancel={() => onCancel('socialLinks')}
onSave={this.onSave}
saveState={saveState}
visibility="Everyone"
visibility={this.props.visibility}
onVisibilityChange={e => onVisibilityChange('socialLinks', e.target.value)}
/>
</React.Fragment>
@@ -91,7 +91,7 @@ class SocialLinks extends React.Component {
showEditButton
onClickEdit={() => onEdit('socialLinks')}
showVisibility={socialLinks && socialLinks.length > 0}
visibility="Everyone"
visibility={this.props.visibility}
/>
<ul className="list-unstyled">
{this.props.platforms.map(({ key, name }) => (
@@ -170,6 +170,7 @@ SocialLinks.propTypes = {
key: PropTypes.string,
name: PropTypes.string,
})),
visibility: PropTypes.oneOf(['private', 'all_users']),
};
SocialLinks.defaultProps = {
@@ -180,6 +181,7 @@ SocialLinks.defaultProps = {
{ key: 'linkedin', name: 'LinkedIn' },
{ key: 'facebook', name: 'Facebook' },
],
visibility: 'private',
};
export default SocialLinks;

View File

@@ -12,6 +12,7 @@ import { ALL_COUNTRIES } from '../../constants/countries';
function UserLocation({
userLocation,
visibility,
editMode,
onEdit,
onChange,
@@ -43,7 +44,7 @@ function UserLocation({
onCancel={() => onCancel('userLocation')}
onSave={() => onSave('userLocation')}
saveState={saveState}
visibility="Everyone"
visibility={visibility}
onVisibilityChange={e => onVisibilityChange('userLocation', e.target.value)}
/>
</React.Fragment>
@@ -55,7 +56,7 @@ function UserLocation({
showEditButton
onClickEdit={() => onEdit('userLocation')}
showVisibility={Boolean(userLocation)}
visibility="Everyone"
visibility={visibility}
/>
<h5>{ALL_COUNTRIES[userLocation]}</h5>
</React.Fragment>
@@ -83,12 +84,14 @@ UserLocation.propTypes = {
onVisibilityChange: PropTypes.func.isRequired,
saveState: PropTypes.string,
userLocation: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
};
UserLocation.defaultProps = {
editMode: 'static',
saveState: null,
userLocation: null,
visibility: 'private',
};

View File

@@ -20,11 +20,11 @@ function EditControls({
bsSize="sm"
type="select"
name="select"
value={visibility}
defaultValue={visibility}
onChange={onVisibilityChange}
>
<option key="Just me" value="Just me">Just me</option>
<option key="Everyone" value="Everyone">Everyone</option>
<option key="private" value="private">Just me</option>
<option key="all_users" value="all_users">Everyone on edX</option>
</Input>
</span>
</Col>
@@ -50,13 +50,13 @@ export default EditControls;
EditControls.propTypes = {
onCancel: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
visibility: PropTypes.oneOf(['Everyone', 'Just me']),
visibility: PropTypes.oneOf(['private', 'all_users']),
onVisibilityChange: PropTypes.func,
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
};
EditControls.defaultProps = {
visibility: null,
visibility: 'private',
onVisibilityChange: null,
saveState: null,
};

View File

@@ -32,7 +32,7 @@ EditableItemHeader.propTypes = {
showVisibility: PropTypes.bool,
showEditButton: PropTypes.bool,
content: PropTypes.string,
visibility: PropTypes.oneOf(['Everyone', 'Just me']),
visibility: PropTypes.oneOf(['private', 'all_users']),
};
EditableItemHeader.defaultProps = {
@@ -40,5 +40,5 @@ EditableItemHeader.defaultProps = {
showVisibility: false,
showEditButton: false,
content: '',
visibility: 'Everyone',
visibility: 'private',
};

View File

@@ -5,11 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
function Visibility({ to }) {
const icon = to === 'Everyone' ? faEye : faEyeSlash;
const icon = to === 'private' ? faEyeSlash : faEye;
const label = to === 'private' ? 'Just me' : 'Everyone on edX';
return (
<span className="ml-auto small text-muted">
<FontAwesomeIcon icon={icon} /> {to}
<FontAwesomeIcon icon={icon} /> {label}
</span>
);
}
@@ -17,9 +18,9 @@ function Visibility({ to }) {
export default Visibility;
Visibility.propTypes = {
to: PropTypes.oneOf(['Everyone', 'Just me']),
to: PropTypes.oneOf(['private', 'all_users']),
};
Visibility.defaultProps = {
to: 'Everyone',
to: 'private',
};

View File

@@ -20,6 +20,7 @@ class UserProfile extends React.Component {
education: { value: null, visibility: null },
bio: { value: null, visibility: null },
socialLinks: { value: null, visibility: null },
visibility: {},
};
@@ -63,7 +64,7 @@ class UserProfile extends React.Component {
this.setState({
[fieldName]: {
value,
visibility: this.state[fieldName].visibility,
visibility: this.state.visibility[fieldName],
},
});
}
@@ -106,6 +107,8 @@ class UserProfile extends React.Component {
return 'editable';
};
const getVisibility = name => this.props.visibility[name];
return (
<div>
<div className="bg-banner bg-program-micro-masters d-none d-md-block p-relative" />
@@ -132,24 +135,28 @@ class UserProfile extends React.Component {
<FullName
fullName={fullName}
visibility={getVisibility('fullName')}
editMode={getMode('fullName')}
{...commonProps}
/>
<UserLocation
userLocation={userLocation}
visibility={getVisibility('userLocation')}
editMode={getMode('userLocation')}
{...commonProps}
/>
<Education
education={education}
visibility={getVisibility('education')}
editMode={getMode('education')}
{...commonProps}
/>
<SocialLinks
socialLinks={socialLinks}
visibility={getVisibility('socialLinks')}
editMode={getMode('socialLinks')}
{...commonProps}
/>
@@ -159,12 +166,14 @@ class UserProfile extends React.Component {
<Bio
bio={bio}
visibility={getVisibility('bio')}
editMode={getMode('bio')}
{...commonProps}
/>
<MyCertificates
certificates={certificates}
visibility={getVisibility('certificates')}
editMode={getMode('certificates')}
{...commonProps}
/>
@@ -209,6 +218,8 @@ UserProfile.propTypes = {
username: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
accountPrivacy: PropTypes.string,
visibility: PropTypes.object, // eslint-disable-line
};
UserProfile.defaultProps = {
@@ -225,4 +236,6 @@ UserProfile.defaultProps = {
aboutMe: null,
bio: null,
certificates: null,
accountPrivacy: null,
visibility: {}, // eslint-disable-line
};

View File

@@ -28,6 +28,8 @@ const mapStateToProps = (state) => {
socialLinks: state.profilePage.profile.socialLinks,
bio: state.profilePage.profile.bio,
certificates: null,
accountPrivacy: state.preferences.accountPrivacy,
visibility: state.preferences.visibility || {},
};
};

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
import { configuration } from '../config';
@@ -15,14 +16,39 @@ const apiClient = getAuthenticatedAPIClient({
csrfCookieName: configuration.CSRF_COOKIE_NAME,
});
const clientServerKeyMap = {
bio: 'bio',
socialLinks: 'social_links',
country: 'country',
education: 'level_of_education',
fullName: 'name',
username: 'username',
profileImage: 'profile_image',
dateJoined: 'date_joined',
languageProficiencies: 'language_proficiencies',
accountPrivacy: 'account_privacy',
};
const serverClientKeyMap = _.invert(clientServerKeyMap);
export function getPreferences(username) {
const url = `${lmsBaseUrl}/api/user/v1/preferences/${username}`;
return new Promise((resolve, reject) => {
apiClient.get(url)
.then((response) => {
resolve(response.data);
.then(({ data }) => {
const createPathsAndTransformKeys = (acc, key) => {
_.set(
acc,
key.split('.').map(pathKey => serverClientKeyMap[pathKey] || pathKey),
data[key],
);
return acc;
};
const preferences = Object.keys(data).reduce(createPathsAndTransformKeys, {});
resolve(preferences);
})
.catch((error) => {
reject(error);

View File

@@ -0,0 +1,66 @@
import {
FETCH_PREFERENCES,
SAVE_PREFERENCES,
} from '../../actions/preferences';
const initialState = {
fetchPreferencesState: null,
savePreferencesState: null,
};
const profile = (state = initialState, action) => {
switch (action.type) {
case FETCH_PREFERENCES.BEGIN:
return {
...state,
fetchPreferencesState: 'pending',
};
case FETCH_PREFERENCES.SUCCESS:
return {
...state,
fetchPreferencesState: 'complete',
...action.preferences,
};
case FETCH_PREFERENCES.FAILURE:
return {
...state,
fetchPreferencesState: 'error',
};
case FETCH_PREFERENCES.RESET:
return {
...state,
fetchPreferencesState: null,
error: null,
};
case SAVE_PREFERENCES.BEGIN:
return {
...state,
savePreferencesState: 'pending',
};
case SAVE_PREFERENCES.SUCCESS:
return {
...state,
savePreferencesState: 'complete',
...action.preferences,
};
case SAVE_PREFERENCES.FAILURE:
return {
...state,
savePreferencesState: 'error',
};
case SAVE_PREFERENCES.RESET:
return {
...state,
savePreferencesState: null,
error: null,
};
default:
return state;
}
};
export default profile;

View File

@@ -5,8 +5,6 @@ import {
EDITABLE_FIELD_CLOSE,
EDITABLE_FIELD_OPEN,
FETCH_PROFILE,
FETCH_PREFERENCES,
SAVE_PREFERENCES,
} from '../../actions/profile';
const initialState = {
@@ -15,8 +13,6 @@ const initialState = {
savePhotoState: null,
currentlyEditingField: null,
profile: {},
fetchPreferencesState: null,
savePreferencesState: null,
};
const profilePage = (state = initialState, action) => {
@@ -116,53 +112,6 @@ const profilePage = (state = initialState, action) => {
error: null,
};
case FETCH_PREFERENCES.BEGIN:
return {
...state,
fetchPreferencesState: 'pending',
};
case FETCH_PREFERENCES.SUCCESS:
return {
...state,
fetchPreferencesState: 'complete',
preferences: action.preferences,
};
case FETCH_PREFERENCES.FAILURE:
return {
...state,
fetchPreferencesState: 'error',
};
case FETCH_PREFERENCES.RESET:
return {
...state,
fetchPreferencesState: null,
error: null,
};
case SAVE_PREFERENCES.BEGIN:
return {
...state,
savePreferencesState: 'pending',
};
case SAVE_PREFERENCES.SUCCESS:
return {
...state,
savePreferencesState: 'complete',
preferences: action.preferences,
};
case SAVE_PREFERENCES.FAILURE:
return {
...state,
savePreferencesState: 'error',
};
case SAVE_PREFERENCES.RESET:
return {
...state,
savePreferencesState: null,
error: null,
};
default:
return state;
}

View File

@@ -1,6 +1,7 @@
import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
import profilePage from './ProfilePageReducer';
import preferences from './PreferencesReducer';
const identityReducer = (state) => {
const newState = { ...state };
@@ -13,6 +14,7 @@ const rootReducer = combineReducers({
authentication: identityReducer,
userAccount,
profilePage,
preferences,
});
export default rootReducer;

View File

@@ -19,7 +19,7 @@ import FooterLogo from '../assets/edx-footer.png';
import './App.scss';
import NotFoundPage from './components/NotFoundPage';
import { fetchPreferences } from './actions/profile';
import { fetchPreferences } from './actions/preferences';
class App extends Component {
componentDidMount() {

View File

@@ -23,6 +23,9 @@ import {
deleteProfilePhotoSuccess,
deleteProfilePhotoFailure,
deleteProfilePhotoReset,
} from '../actions/profile';
import {
FETCH_PREFERENCES,
fetchPreferencesBegin,
fetchPreferencesSuccess,
@@ -33,8 +36,7 @@ import {
savePreferencesSuccess,
savePreferencesFailure,
savePreferencesReset,
} from '../actions/profile';
} from '../actions/preferences';
import * as ProfileApiService from '../services/ProfileApiService';
import { getPreferences, savePreferences } from '../data/apiClient';