Fix(settings): normalize responses (#16603)

This commit is contained in:
mrugesh mohapatra
2018-02-03 01:43:36 +05:30
committed by GitHub
16 changed files with 253 additions and 152 deletions

View File

@@ -4,26 +4,26 @@ import { CloseButton } from 'react-bootstrap';
import { connect } from 'react-redux';
import ns from './ns.json';
import { alertTypes } from './redux/utils.js';
import { alertTypes } from '../../utils/flash.js';
import {
latestMessageSelector,
clickOnClose
} from './redux';
const propTypes = {
alertType: PropTypes.oneOf(Object.keys(alertTypes)),
clickOnClose: PropTypes.func.isRequired,
message: PropTypes.string
message: PropTypes.string,
type: PropTypes.oneOf(Object.keys(alertTypes))
};
const mapStateToProps = latestMessageSelector;
const mapDispatchToProps = { clickOnClose };
export function Flash({ alertType, clickOnClose, message }) {
export function Flash({ type, clickOnClose, message }) {
if (!message) {
return null;
}
return (
<div className={`${ns}-container bg-${alertType}`}>
<div className={`${ns}-container bg-${type}`}>
<div className={`${ns}-content`}>
<p className={ `${ns}-message` }>
{ message }

View File

@@ -11,6 +11,8 @@ import * as utils from './utils.js';
import getMessagesEpic from './get-messages-epic.js';
import ns from '../ns.json';
// export all the utils
export { utils };
export const epics = [getMessagesEpic];
export const types = createTypes([
'clickOnClose',
@@ -45,13 +47,10 @@ export default composeReducers(
),
function metaReducer(state = defaultState, action) {
if (utils.isFlashAction(action)) {
const { payload: { alertType, message } } = utils.getFlashAction(action);
const { payload } = utils.getFlashAction(action);
return [
...state,
{
alertType: utils.normalizeAlertType(alertType),
message: _.escape(message)
}
...payload
];
}
return state;

View File

@@ -1,29 +1,43 @@
import _ from 'lodash/fp';
import { alertTypes, normalizeAlertType } from '../../../utils/flash.js';
export const alertTypes = _.keyBy(_.identity)([
'success',
'info',
'warning',
'danger'
]);
// interface ExpressFlash {
// [alertType]: [String...]
// }
// interface StackFlash {
// type: AlertType,
// message: String
// }
export const expressToStack = _.flow(
_.toPairs,
_.flatMap(([ type, messages ]) => messages.map(msg => ({
message: msg,
type: normalizeAlertType(type)
})))
);
export const normalizeAlertType = alertType => alertTypes[alertType] || 'info';
export const isExpressFlash = _.flow(
_.keys,
_.every(type => alertTypes[type])
);
export const getFlashAction = _.flow(
_.property('meta'),
_.property('flash')
);
// FlashMessage
// createFlashMetaAction(payload: ExpressFlash|StackFlash
export const createFlashMetaAction = payload => {
if (isExpressFlash(payload)) {
payload = expressToStack(payload);
} else {
payload = [payload];
}
return { flash: { payload } };
};
export const isFlashAction = _.flow(
getFlashAction,
Boolean
);
export const expressToStack = _.flow(
_.toPairs,
_.flatMap(([ type, messages ]) => messages.map(msg => ({
message: msg,
alertType: normalizeAlertType(type)
})))
);

View File

@@ -12,9 +12,11 @@ import { createSelector } from 'reselect';
import fetchUserEpic from './fetch-user-epic.js';
import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
import fetchChallengesEpic from './fetch-challenges-epic.js';
import nightModeEpic from './night-mode-epic.js';
import { createFilesMetaCreator } from '../files';
import { updateThemeMetacreator, entitiesSelector } from '../entities';
import { utils } from '../Flash/redux';
import { types as challenges } from '../routes/Challenges/redux';
import { challengeToFiles } from '../routes/Challenges/utils';
@@ -23,8 +25,9 @@ import ns from '../ns.json';
import { themes, invertTheme } from '../../utils/themes.js';
export const epics = [
fetchUserEpic,
fetchChallengesEpic,
fetchUserEpic,
nightModeEpic,
updateMyCurrentChallengeEpic
];
@@ -48,7 +51,7 @@ export const types = createTypes([
// night mode
'toggleNightMode',
'postThemeComplete'
createAsyncTypes('postTheme')
], ns);
const throwIfUndefined = () => {
@@ -130,6 +133,7 @@ export const createErrorObservable = error => Observable.just({
type: types.handleError,
error
});
// use sparingly
// doActionOnError(
// actionCreator: (() => Action|Null)
// ) => (error: Error) => Observable[Action]
@@ -147,9 +151,18 @@ export const toggleNightMode = createAction(
(username, theme) => updateThemeMetacreator(username, invertTheme(theme))
);
export const postThemeComplete = createAction(
types.postThemeComplete,
types.postTheme.complete,
null,
updateThemeMetacreator
utils.createFlashMetaAction
);
export const postThemeError = createAction(
types.postTheme.error,
null,
(username, theme, err) => ({
...updateThemeMetacreator(username, invertTheme(theme)),
...utils.createFlashMetaAction(err)
})
);
const defaultState = {

View File

@@ -0,0 +1,64 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { ofType } from 'redux-epic';
import store from 'store';
import { themes } from '../../utils/themes.js';
import { postJSON$ } from '../../utils/ajax-stream.js';
import {
csrfSelector,
postThemeComplete,
postThemeError,
themeSelector,
types,
usernameSelector
} from './index.js';
function persistTheme(theme) {
store.set('fcc-theme', theme);
}
export default function nightModeEpic(
actions,
{ getState },
{ document }
) {
return Observable.of(document)
// if document is undefined we do nothing (ssr trap)
.filter(Boolean)
.flatMap(({ body }) => {
const toggleBodyClass = actions
::ofType(
types.fetchUser.complete,
types.toggleNightMode,
types.postTheme.complete,
types.postTheme.error
)
.map(_.flow(getState, themeSelector))
// catch existing night mode users
.do(persistTheme)
.do(theme => {
if (theme === themes.night) {
body.classList.add(themes.night);
} else {
body.classList.remove(themes.night);
}
})
.ignoreElements();
const postThemeEpic = actions::ofType(types.toggleNightMode)
.debounce(250)
.flatMapLatest(() => {
const _csrf = csrfSelector(getState());
const theme = themeSelector(getState());
const username = usernameSelector(getState());
return postJSON$('/update-my-theme', { _csrf, theme })
.map(postThemeComplete)
.catch(err => {
return Observable.of(postThemeError(username, theme, err));
});
});
return Observable.merge(toggleBodyClass, postThemeEpic);
});
}

View File

@@ -2,11 +2,13 @@ import { isLocationAction } from 'redux-first-router';
import {
addNS,
createAction,
createAsyncTypes,
createTypes
} from 'berkeleys-redux-utils';
import userUpdateEpic from './update-user-epic.js';
import ns from '../ns.json';
import { utils } from '../../../Flash/redux';
export const epics = [
userUpdateEpic
@@ -14,7 +16,7 @@ export const epics = [
export const types = createTypes([
'toggleUserFlag',
'updateMyEmail',
createAsyncTypes('updateMyEmail'),
'updateMyLang',
'onRouteSettings',
'onRouteUpdateEmail'
@@ -24,7 +26,18 @@ export const types = createTypes([
export const onRouteSettings = createAction(types.onRouteSettings);
export const onRouteUpdateEmail = createAction(types.onRouteUpdateEmail);
export const toggleUserFlag = createAction(types.toggleUserFlag);
export const updateMyEmail = createAction(types.updateMyEmail);
export const updateMyEmail = createAction(types.updateMyEmail.start);
export const updateMyEmailComplete = createAction(
types.updateMyEmail.complete,
null,
utils.createFlashMetaAction
);
export const updateMyEmailError = createAction(
types.updateMyEmail.error,
null,
utils.createFlashMetaAction
);
export const updateMyLang = createAction(
types.updateMyLang,

View File

@@ -703,9 +703,7 @@ module.exports = function(User) {
);
return Promise.reject(err);
}
return this.update$({ theme })
.map({ updatedTo: theme })
.toPromise();
return this.update$({ theme }).toPromise();
};
// deprecated. remove once live

View File

@@ -64,11 +64,27 @@ function getCORSRequest() {
}
}
function parseXhrResponse(responseType, xhr) {
switch (responseType) {
case 'json':
if ('response' in xhr) {
return xhr.responseType ?
xhr.response :
JSON.parse(xhr.response || xhr.responseText || 'null');
} else {
return JSON.parse(xhr.responseText || 'null');
}
case 'xml':
return xhr.responseXML;
case 'text':
default:
return ('response' in xhr) ? xhr.response : xhr.responseText;
}
}
function normalizeAjaxSuccessEvent(e, xhr, settings) {
var response = ('response' in xhr) ? xhr.response : xhr.responseText;
response = settings.responseType === 'json' ? JSON.parse(response) : response;
return {
response: response,
response: parseXhrResponse(settings.responseType || xhr.responseType, xhr),
status: xhr.status,
responseType: xhr.responseType,
xhr: xhr,
@@ -266,7 +282,8 @@ export function postJSON$(url, body) {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
},
normalizeError: (e, xhr) => parseXhrResponse('json', xhr)
})
.map(({ response }) => response);
}
@@ -291,6 +308,7 @@ export function getJSON$(url) {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
},
normalizeError: (e, xhr) => parseXhrResponse('json', xhr)
}).map(({ response }) => response);
}

10
common/utils/flash.js Normal file
View File

@@ -0,0 +1,10 @@
import _ from 'lodash';
export const alertTypes = _.keyBy([
'success',
'info',
'warning',
'danger'
], _.identity);
export const normalizeAlertType = alertType => alertTypes[alertType] || 'info';