feat(flash): Enable flash from the api

This commit is contained in:
Bouncey
2018-08-30 15:27:53 +01:00
committed by mrugesh mohapatra
parent 01bc66cab3
commit 3f4af667ef
8 changed files with 97 additions and 27 deletions

View File

@ -3,11 +3,16 @@ import PropTypes from 'prop-types';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createStore } from './src/redux/createStore'; import { createStore } from './src/redux/createStore';
import AppMountNotifier from './src/components/AppMountNotifier';
const store = createStore(); const store = createStore();
export const wrapRootElement = ({ element }) => { export const wrapRootElement = ({ element }) => {
return <Provider store={store}>{element}</Provider>; return (
<Provider store={store}>
<AppMountNotifier render={() => element} />
</Provider>
);
}; };
wrapRootElement.propTypes = { wrapRootElement.propTypes = {

View File

@ -69,22 +69,10 @@ export default function prodErrorHandler() {
} }
if (type === 'html') { if (type === 'html') {
if (isDev) {
return res.render(
'dev-error',
{
...handled,
stack: createStackHtml(err),
errorTitle: createErrorTitle(err),
title: 'freeCodeCamp - Server Error',
status
}
);
}
if (typeof req.flash === 'function') { if (typeof req.flash === 'function') {
req.flash(handled.type || 'danger', message); req.flash(handled.type || 'danger', message);
} }
return res.redirect(redirectTo); return res.redirectWithFlash(redirectTo);
// json // json
} else if (type === 'json') { } else if (type === 'json') {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');

View File

@ -1,14 +1,17 @@
import { Observable } from 'rx'; import qs from 'query-string';
// add rx methods to express // add rx methods to express
export default function() { export default function() {
return function expressExtensions(req, res, next) { return function expressExtensions(req, res, next) {
// express flash will overwrite render with one that will res.redirectWithFlash = uri => {
// dump flash messages to locals on every call to render const flash = req.flash();
// Use this when that behavior is not wanted res.redirect(
res.renderWithoutFlash = res.render; `${uri}?${qs.stringify(
// render to observable stream using build in render { messages: qs.stringify(flash, { arrayFormat: 'index' }) },
res.render$ = Observable.fromNodeCallback(res.render, res); { arrayFormat: 'index' }
)}`
);
};
res.sendFlash = (type, message) => { res.sendFlash = (type, message) => {
if (type && message) { if (type && message) {
req.flash(type, message); req.flash(type, message);

View File

@ -0,0 +1,30 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { appMount } from '../redux';
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
bindActionCreators({ appMount }, dispatch);
class AppMountNotifer extends Component {
componentDidMount() {
return this.props.appMount();
}
render() {
return this.props.render();
}
}
AppMountNotifer.displayName = 'AppMountNotifier';
AppMountNotifer.propTypes = {
appMount: PropTypes.func.isRequired,
render: PropTypes.func.isRequired
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(AppMountNotifer);

View File

@ -3,4 +3,5 @@
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
flex-direction: row-reverse; flex-direction: row-reverse;
margin-bottom: 0px;
} }

View File

@ -16,7 +16,7 @@ function Flash({ messages, onClose }) {
key={id} key={id}
onDismiss={createDismissHandler(onClose, id)} onDismiss={createDismissHandler(onClose, id)}
> >
{message} <div dangerouslySetInnerHTML={{ __html: message }} />
</Alert> </Alert>
)); ));
} }

View File

@ -0,0 +1,28 @@
import { put, takeEvery } from 'redux-saga/effects';
import qs from 'query-string';
import { createFlashMessage } from '../components/Flash/redux';
function* parseMessagesSaga() {
const search = window.location.search.slice();
// TODO(Bouncey): Find a way to clear the search with causing a re-render
if (search) {
const { messages } = qs.parse(search, { arrayFormat: 'index' });
if (messages) {
const flashMap = qs.parse(messages, { arrayFormat: 'index' });
const flash = Object.keys(flashMap).reduce(
(acc, type) => [
...acc,
...flashMap[type].map(message => ({ type, message }))
],
[]
);
for (let i = 0; i < flash.length; i++) {
yield put(createFlashMessage(flash[i]));
}
}
}
}
export function createAppMountSaga(types) {
return [takeEvery(types.appMount, parseMessagesSaga)];
}

View File

@ -3,6 +3,8 @@ import { createAction, handleActions } from 'redux-actions';
import { createTypes, createAsyncTypes } from '../utils/createTypes'; import { createTypes, createAsyncTypes } from '../utils/createTypes';
import { createFetchUserSaga } from './fetch-user-saga'; import { createFetchUserSaga } from './fetch-user-saga';
import { createAcceptTermsSaga } from './accept-terms-saga'; import { createAcceptTermsSaga } from './accept-terms-saga';
import { createAppMountSaga } from './app-mount-saga';
import { createUpdateMyEmailSaga } from './update-email-saga';
const ns = 'app'; const ns = 'app';
@ -18,22 +20,35 @@ const initialState = {
}; };
const types = createTypes( const types = createTypes(
[...createAsyncTypes('fetchUser'), ...createAsyncTypes('acceptTerms')], [
'appMount',
...createAsyncTypes('fetchUser'),
...createAsyncTypes('acceptTerms'),
...createAsyncTypes('updateMyEmail')
],
ns ns
); );
export const sagas = [ export const sagas = [
...createAcceptTermsSaga(types),
...createAppMountSaga(types),
...createFetchUserSaga(types), ...createFetchUserSaga(types),
...createAcceptTermsSaga(types) ...createUpdateMyEmailSaga(types)
]; ];
export const appMount = createAction(types.appMount);
export const acceptTerms = createAction(types.acceptTerms);
export const acceptTermsComplete = createAction(types.acceptTermsComplete);
export const acceptTermsError = createAction(types.acceptTermsError);
export const fetchUser = createAction(types.fetchUser); export const fetchUser = createAction(types.fetchUser);
export const fetchUserComplete = createAction(types.fetchUserComplete); export const fetchUserComplete = createAction(types.fetchUserComplete);
export const fetchUserError = createAction(types.fetchUserError); export const fetchUserError = createAction(types.fetchUserError);
export const acceptTerms = createAction(types.acceptTerms); export const updateMyEmail = createAction(types.updateMyEmail);
export const acceptTermsComplete = createAction(types.acceptTermsComplete); export const updateMyEmailComplete = createAction(types.updateMyEmailComplete);
export const acceptTermsError = createAction(types.acceptTermsError); export const updateMyEmailError = createAction(types.updateMyEmailError);
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length; export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
export const userFetchStateSelector = state => state[ns].fetchState; export const userFetchStateSelector = state => state[ns].fetchState;