mirror of
https://github.com/strapi/strapi.git
synced 2025-08-07 08:16:35 +00:00
Use react-router 4
This commit is contained in:
parent
385a486716
commit
fe8fa6a54b
@ -10,58 +10,32 @@ import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { applyRouterMiddleware, Router, browserHistory } from 'react-router';
|
||||
import { syncHistoryWithStore } from 'react-router-redux';
|
||||
import useScroll from 'react-router-scroll';
|
||||
import { ConnectedRouter } from 'react-router-redux';
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
import _ from 'lodash';
|
||||
import 'sanitize.css/sanitize.css';
|
||||
|
||||
import LanguageProvider from 'containers/LanguageProvider';
|
||||
import NotificationProvider from 'containers/NotificationProvider';
|
||||
import { selectLocationState } from 'containers/App/selectors';
|
||||
|
||||
import App from 'containers/App';
|
||||
import { showNotification } from 'containers/NotificationProvider/actions';
|
||||
import { pluginLoaded, updatePlugin } from 'containers/App/actions';
|
||||
|
||||
import createRoutes from './routes';
|
||||
import configureStore from './store';
|
||||
import { translationMessages, languages } from './i18n';
|
||||
|
||||
// Create redux store with history
|
||||
// this uses the singleton browserHistory provided by react-router
|
||||
// Optionally, this could be changed to leverage a created history
|
||||
// e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();`
|
||||
const initialState = {};
|
||||
const store = configureStore(initialState, browserHistory);
|
||||
|
||||
// Sync history and store, as the react-router-redux reducer
|
||||
// is under the non-default key ("routing"), selectLocationState
|
||||
// must be provided for resolving how to retrieve the "route" in the state
|
||||
const history = syncHistoryWithStore(browserHistory, store, {
|
||||
selectLocationState: selectLocationState(),
|
||||
});
|
||||
|
||||
// Set up the router, wrapping all Routes in the App component
|
||||
const rootRoute = {
|
||||
component: App,
|
||||
childRoutes: createRoutes(store),
|
||||
};
|
||||
const history = createHistory();
|
||||
const store = configureStore(initialState, history);
|
||||
|
||||
const render = (translatedMessages) => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<LanguageProvider messages={translatedMessages}>
|
||||
<NotificationProvider>
|
||||
<Router
|
||||
history={history}
|
||||
routes={rootRoute}
|
||||
render={
|
||||
// Scroll to top when going to a new page, imitating default browser
|
||||
// behaviour
|
||||
applyRouterMiddleware(useScroll())
|
||||
}
|
||||
/>
|
||||
</NotificationProvider>
|
||||
<ConnectedRouter history={history}>
|
||||
<App />
|
||||
</ConnectedRouter>
|
||||
</LanguageProvider>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
@ -160,7 +134,7 @@ window.Strapi = {
|
||||
store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
|
||||
},
|
||||
}),
|
||||
router: browserHistory,
|
||||
// router: browserHistory,
|
||||
languages,
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './styles.scss';
|
||||
|
||||
@ -13,7 +13,7 @@ class LeftMenuHeader extends React.Component { // eslint-disable-line react/pref
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.leftMenuHeader}>
|
||||
<Link to="/" className={styles.leftMenuHeaderLink}>
|
||||
<Link to="/admin" className={styles.leftMenuHeaderLink}>
|
||||
<span className={styles.projectName}></span>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './styles.scss';
|
||||
|
||||
|
@ -68,17 +68,17 @@ class LeftMenuLinkContainer extends React.Component { // eslint-disable-line rea
|
||||
<LeftMenuLink
|
||||
icon="cubes"
|
||||
label={messages.listPlugins.id}
|
||||
destination="/list-plugins"
|
||||
destination="/admin/list-plugins"
|
||||
/>
|
||||
<LeftMenuLink
|
||||
icon="download"
|
||||
label={messages.installNewPlugin.id}
|
||||
destination="/install-plugin"
|
||||
destination="/admin/install-plugin"
|
||||
/>
|
||||
<LeftMenuLink
|
||||
icon="gear"
|
||||
label={messages.configuration.id}
|
||||
destination="/configuration"
|
||||
destination="/admin/configuration"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* AdminPage
|
||||
*
|
||||
* This is the first thing users see of our AdminPage, at the '/' route
|
||||
*
|
||||
* NOTE: while this component should technically be a stateless functional
|
||||
* component (SFC), hot reloading does not currently support SFCs. If hot
|
||||
* reloading is not a neccessity for you then you can refactor it and remove
|
||||
* the linting exception.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import HomePage from 'containers/HomePage';
|
||||
import PluginPage from 'containers/PluginPage';
|
||||
import ComingSoonPage from 'containers/ComingSoonPage';
|
||||
import LeftMenu from 'containers/LeftMenu';
|
||||
import Content from 'containers/Content';
|
||||
import NotFoundPage from 'containers/NotFoundPage';
|
||||
|
||||
import { selectPlugins } from 'containers/App/selectors';
|
||||
import { hideNotification } from 'containers/NotificationProvider/actions';
|
||||
|
||||
import Header from 'components/Header/index';
|
||||
|
||||
import styles from './syles.scss';
|
||||
|
||||
export class AdminPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<LeftMenu plugins={this.props.plugins} />
|
||||
<div className={styles.adminPageRightWrapper}>
|
||||
<Header />
|
||||
<Content {...this.props}>
|
||||
<Switch>
|
||||
<Route path="/admin" component={HomePage} exact />
|
||||
<Route path="/admin/plugins" component={PluginPage} />
|
||||
<Route path="/admin/list-plugins" component={ComingSoonPage} exact />
|
||||
<Route path="/admin/install-plugin" component={ComingSoonPage} exact />
|
||||
<Route path="/admin/configuration" component={ComingSoonPage} exact />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Content>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminPage.contextTypes = {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
AdminPage.propTypes = {
|
||||
plugins: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
plugins: selectPlugins(),
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onHideNotification: (id) => { dispatch(hideNotification(id)); },
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdminPage);
|
@ -1,10 +1,10 @@
|
||||
/* Import */
|
||||
@import '../../styles/variables/variables';
|
||||
|
||||
.homePage { /* stylelint-disable */
|
||||
.adminPage { /* stylelint-disable */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.homePageRightWrapper {
|
||||
.adminPageRightWrapper {
|
||||
width: calc(100% - #{$left-menu-width});
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
*
|
||||
* App.react.js
|
||||
* App.js
|
||||
*
|
||||
* This component is the skeleton around the actual pages, and should only
|
||||
* contain code that should be seen on all pages. (e.g. navigation bar)
|
||||
@ -12,14 +12,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import { hideNotification } from 'containers/NotificationProvider/actions';
|
||||
import { selectNotifications } from 'containers/NotificationProvider/selectors';
|
||||
import NotificationsContainer from 'components/NotificationsContainer';
|
||||
import AdminPage from 'containers/AdminPage';
|
||||
import NotFoundPage from 'containers/NotFoundPage';
|
||||
|
||||
import NotificationProvider from 'containers/NotificationProvider';
|
||||
|
||||
import { selectPlugins } from './selectors';
|
||||
import '../../styles/main.scss';
|
||||
import styles from './styles.scss';
|
||||
|
||||
@ -27,9 +26,12 @@ export class App extends React.Component { // eslint-disable-line react/prefer-s
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NotificationsContainer onHideNotification={this.props.onHideNotification} notifications={this.props.notifications}></NotificationsContainer>
|
||||
<NotificationProvider />
|
||||
<div className={styles.container}>
|
||||
{React.Children.toArray(this.props.children)}
|
||||
<Switch>
|
||||
<Route path="/admin" component={AdminPage} />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -40,22 +42,6 @@ App.contextTypes = {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
children: React.PropTypes.node.isRequired,
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
onHideNotification: React.PropTypes.func.isRequired,
|
||||
};
|
||||
App.propTypes = {};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
plugins: selectPlugins(),
|
||||
notifications: selectNotifications(),
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onHideNotification: (id) => { dispatch(hideNotification(id)); },
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
export default App;
|
||||
|
@ -1,22 +1,5 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
// selectLocationState expects a plain JS object for the routing state
|
||||
const selectLocationState = () => {
|
||||
let prevRoutingState;
|
||||
let prevRoutingStateJS;
|
||||
|
||||
return (state) => {
|
||||
const routingState = state.get('route'); // or state.route
|
||||
|
||||
if (!routingState.equals(prevRoutingState)) {
|
||||
prevRoutingState = routingState;
|
||||
prevRoutingStateJS = routingState.toJS();
|
||||
}
|
||||
|
||||
return prevRoutingStateJS;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct selector to the languageToggle state domain
|
||||
*/
|
||||
@ -28,12 +11,10 @@ const selectApp = () => (state) => state.get('app');
|
||||
|
||||
const selectPlugins = () => createSelector(
|
||||
selectApp(),
|
||||
(languageState) => languageState.get('plugins')
|
||||
(appState) => appState.get('plugins')
|
||||
);
|
||||
|
||||
export {
|
||||
selectApp,
|
||||
selectPlugins,
|
||||
selectLocationState,
|
||||
};
|
||||
|
||||
|
@ -1,46 +1,29 @@
|
||||
/*
|
||||
*
|
||||
* HomePage
|
||||
*
|
||||
* This is the first thing users see of our App, at the '/' route
|
||||
*
|
||||
* NOTE: while this component should technically be a stateless functional
|
||||
* component (SFC), hot reloading does not currently support SFCs. If hot
|
||||
* reloading is not a neccessity for you then you can refactor it and remove
|
||||
* the linting exception.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectPlugins } from 'containers/App/selectors';
|
||||
import LeftMenu from 'containers/LeftMenu';
|
||||
import Header from 'components/Header/index';
|
||||
import Content from 'containers/Content';
|
||||
import styles from './syles.scss';
|
||||
import Helmet from 'react-helmet';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import messages from './messages.json';
|
||||
import styles from './styles.scss';
|
||||
|
||||
export class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.homePage}>
|
||||
<LeftMenu plugins={this.props.plugins}></LeftMenu>
|
||||
<div className={styles.homePageRightWrapper}>
|
||||
<Header></Header>
|
||||
<Content {...this.props}>
|
||||
|
||||
</Content>
|
||||
</div>
|
||||
<div className={styles.wrapper}>
|
||||
<Helmet
|
||||
title="Home Page"
|
||||
/>
|
||||
<p><FormattedMessage {...messages.welcome} />.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HomePage.propTypes = {
|
||||
plugins: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
plugins: selectPlugins(),
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
@ -48,4 +31,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
|
||||
export default connect(mapDispatchToProps)(HomePage);
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"welcome": {
|
||||
"id": "app.components.HomePage.welcome",
|
||||
"defaultMessage": "Welcome to the home page"
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.wrapper {
|
||||
padding: 2.3rem;
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './styles.scss';
|
||||
import messages from './messages.json';
|
||||
@ -28,7 +28,7 @@ export default class NotFound extends React.Component { // eslint-disable-line r
|
||||
<h2 className={styles.notFoundDescription}>
|
||||
<FormattedMessage {...messages.description} />
|
||||
</h2>
|
||||
<Link to={'/'}>Back to home page.</Link>
|
||||
<Link to="/admin">Back to home page.</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,27 +6,38 @@
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
|
||||
import NotificationsContainer from 'components/NotificationsContainer';
|
||||
import { selectNotifications } from './selectors';
|
||||
import { hideNotification } from './actions';
|
||||
|
||||
import selectNotificationProvider from './selectors';
|
||||
|
||||
export class NotificationProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{React.Children.only(this.props.children)}
|
||||
</div>
|
||||
<NotificationsContainer
|
||||
onHideNotification={this.props.onHideNotification}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationProvider.propTypes = {
|
||||
children: React.PropTypes.object.isRequired,
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
onHideNotification: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = selectNotificationProvider();
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
notifications: selectNotifications(),
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onHideNotification: (id) => {
|
||||
dispatch(hideNotification(id));
|
||||
},
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Combine all reducers in this file and export the combined reducers.
|
||||
* If we were to do this in store.js, reducers wouldn't be hot reloadable.
|
||||
*/
|
||||
|
||||
import { combineReducers } from 'redux-immutable';
|
||||
import { fromJS } from 'immutable';
|
||||
import { combineReducers } from 'redux-immutable';
|
||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||
import appReducer from 'containers/App/reducer';
|
||||
|
||||
import globalReducer from 'containers/App/reducer';
|
||||
import languageProviderReducer from 'containers/LanguageProvider/reducer';
|
||||
import notificationProviderReducer from 'containers/NotificationProvider/reducer';
|
||||
|
||||
@ -14,13 +14,13 @@ import notificationProviderReducer from 'containers/NotificationProvider/reducer
|
||||
* routeReducer
|
||||
*
|
||||
* The reducer merges route location changes into our immutable state.
|
||||
* The change is necessitated by moving to react-router-redux@4
|
||||
* The change is necessitated by moving to react-router-redux@5
|
||||
*
|
||||
*/
|
||||
|
||||
// Initial routing state
|
||||
const routeInitialState = fromJS({
|
||||
locationBeforeTransitions: null,
|
||||
location: null,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -31,7 +31,7 @@ function routeReducer(state = routeInitialState, action) {
|
||||
/* istanbul ignore next */
|
||||
case LOCATION_CHANGE:
|
||||
return state.merge({
|
||||
locationBeforeTransitions: action.payload,
|
||||
location: action.payload,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
@ -39,14 +39,14 @@ function routeReducer(state = routeInitialState, action) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the main reducer with the asynchronously loaded ones
|
||||
* Creates the main reducer with the dynamically injected ones
|
||||
*/
|
||||
export default function createReducer(asyncReducers) {
|
||||
export default function createReducer(injectedReducers) {
|
||||
return combineReducers({
|
||||
route: routeReducer,
|
||||
app: globalReducer,
|
||||
language: languageProviderReducer,
|
||||
notification: notificationProviderReducer,
|
||||
app: appReducer,
|
||||
...asyncReducers,
|
||||
...injectedReducers,
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Create the store with asynchronously loaded reducers
|
||||
* Create the store with dynamic reducers
|
||||
*/
|
||||
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
@ -9,7 +9,6 @@ import createSagaMiddleware from 'redux-saga';
|
||||
import createReducer from './reducers';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const devtools = window.devToolsExtension || (() => noop => noop);
|
||||
|
||||
export default function configureStore(initialState = {}, history) {
|
||||
// Create the store with two middlewares
|
||||
@ -22,30 +21,40 @@ export default function configureStore(initialState = {}, history) {
|
||||
|
||||
const enhancers = [
|
||||
applyMiddleware(...middlewares),
|
||||
devtools(),
|
||||
];
|
||||
|
||||
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const composeEnhancers =
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
typeof window === 'object' &&
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||
// TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
|
||||
// Prevent recomputing reducers for `replaceReducer`
|
||||
shouldHotReload: false,
|
||||
})
|
||||
: compose;
|
||||
/* eslint-enable */
|
||||
|
||||
const store = createStore(
|
||||
createReducer(),
|
||||
fromJS(initialState),
|
||||
compose(...enhancers)
|
||||
composeEnhancers(...enhancers)
|
||||
);
|
||||
|
||||
// Create hook for async sagas
|
||||
// Extensions
|
||||
store.runSaga = sagaMiddleware.run;
|
||||
store.injectedReducers = {}; // Reducer registry
|
||||
store.injectedSagas = {}; // Saga registry
|
||||
|
||||
// Make reducers hot reloadable, see http://mxs.is/googmo
|
||||
/* istanbul ignore next */
|
||||
if (module.hot) {
|
||||
System.import('./reducers').then((reducerModule) => {
|
||||
const createReducers = reducerModule.default;
|
||||
const nextReducers = createReducers(store.asyncReducers);
|
||||
|
||||
store.replaceReducer(nextReducers);
|
||||
module.hot.accept('./reducers', () => {
|
||||
store.replaceReducer(createReducer(store.injectedReducers));
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize it with no other reducers
|
||||
store.asyncReducers = {};
|
||||
return store;
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
|
||||
import invariant from 'invariant';
|
||||
import warning from 'warning';
|
||||
import createReducer from 'reducers';
|
||||
|
||||
/**
|
||||
* Validate the shape of redux store
|
||||
*/
|
||||
export function checkStore(store) {
|
||||
const shape = {
|
||||
dispatch: isFunction,
|
||||
subscribe: isFunction,
|
||||
getState: isFunction,
|
||||
replaceReducer: isFunction,
|
||||
runSaga: isFunction,
|
||||
asyncReducers: isObject,
|
||||
};
|
||||
invariant(
|
||||
conformsTo(store, shape),
|
||||
'(app/utils...) asyncInjectors: Expected a valid redux store'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an asynchronously loaded reducer
|
||||
*/
|
||||
export function injectAsyncReducer(store, isValid) {
|
||||
return function injectReducer(name, asyncReducer) {
|
||||
if (!isValid) checkStore(store);
|
||||
|
||||
invariant(
|
||||
isString(name) && !isEmpty(name) && isFunction(asyncReducer),
|
||||
'(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
|
||||
);
|
||||
|
||||
store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
|
||||
store.replaceReducer(createReducer(store.asyncReducers));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an asynchronously loaded saga
|
||||
*/
|
||||
export function injectAsyncSagas(store, isValid) {
|
||||
return function injectSagas(sagas) {
|
||||
if (!isValid) checkStore(store);
|
||||
|
||||
invariant(
|
||||
Array.isArray(sagas),
|
||||
'(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
|
||||
);
|
||||
|
||||
warning(
|
||||
!isEmpty(sagas),
|
||||
'(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
|
||||
);
|
||||
|
||||
sagas.map(store.runSaga);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating injectors
|
||||
*/
|
||||
export function getAsyncInjectors(store) {
|
||||
checkStore(store);
|
||||
|
||||
return {
|
||||
injectReducer: injectAsyncReducer(store, true),
|
||||
injectSagas: injectAsyncSagas(store, true),
|
||||
};
|
||||
}
|
25
packages/strapi-admin/files/admin/src/utils/checkStore.js
Normal file
25
packages/strapi-admin/files/admin/src/utils/checkStore.js
Normal file
@ -0,0 +1,25 @@
|
||||
import conformsTo from 'lodash/conformsTo';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import isObject from 'lodash/isObject';
|
||||
import invariant from 'invariant';
|
||||
|
||||
/**
|
||||
* Validate the shape of redux store
|
||||
*/
|
||||
export default function checkStore(store) {
|
||||
console.log(store);
|
||||
|
||||
const shape = {
|
||||
dispatch: isFunction,
|
||||
subscribe: isFunction,
|
||||
getState: isFunction,
|
||||
replaceReducer: isFunction,
|
||||
runSaga: isFunction,
|
||||
injectedReducers: isObject,
|
||||
injectedSagas: isObject,
|
||||
};
|
||||
invariant(
|
||||
conformsTo(store, shape),
|
||||
'(app/utils...) injectors: Expected a valid redux store'
|
||||
);
|
||||
}
|
3
packages/strapi-admin/files/admin/src/utils/constants.js
Normal file
3
packages/strapi-admin/files/admin/src/utils/constants.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
|
||||
export const DAEMON = '@@saga-injector/daemon';
|
||||
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
|
36
packages/strapi-admin/files/admin/src/utils/injectReducer.js
Normal file
36
packages/strapi-admin/files/admin/src/utils/injectReducer.js
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
|
||||
import getInjectors from './reducerInjectors';
|
||||
|
||||
/**
|
||||
* Dynamically injects a reducer
|
||||
*
|
||||
* @param {string} key A key of the reducer
|
||||
* @param {function} reducer A reducer that will be injected
|
||||
*
|
||||
*/
|
||||
export default ({ key, reducer }) => (WrappedComponent) => {
|
||||
class ReducerInjector extends React.Component {
|
||||
static WrappedComponent = WrappedComponent;
|
||||
static contextTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
|
||||
|
||||
componentWillMount() {
|
||||
const { injectReducer } = this.injectors;
|
||||
|
||||
injectReducer(key, reducer);
|
||||
}
|
||||
|
||||
injectors = getInjectors(this.context.store);
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
return hoistNonReactStatics(ReducerInjector, WrappedComponent);
|
||||
};
|
46
packages/strapi-admin/files/admin/src/utils/injectSaga.js
Normal file
46
packages/strapi-admin/files/admin/src/utils/injectSaga.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
|
||||
import getInjectors from './sagaInjectors';
|
||||
|
||||
/**
|
||||
* Dynamically injects a saga, passes component's props as saga arguments
|
||||
*
|
||||
* @param {string} key A key of the saga
|
||||
* @param {function} saga A root saga that will be injected
|
||||
* @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
|
||||
* cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
|
||||
* - constants.DAEMON—starts the saga on component mount and never cancels it or starts again,
|
||||
* - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again.
|
||||
*
|
||||
*/
|
||||
export default ({ key, saga, mode }) => (WrappedComponent) => {
|
||||
class InjectSaga extends React.Component {
|
||||
static WrappedComponent = WrappedComponent;
|
||||
static contextTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
|
||||
|
||||
componentWillMount() {
|
||||
const { injectSaga } = this.injectors;
|
||||
|
||||
injectSaga(key, { saga, mode }, this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { ejectSaga } = this.injectors;
|
||||
|
||||
ejectSaga(key);
|
||||
}
|
||||
|
||||
injectors = getInjectors(this.context.store);
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
return hoistNonReactStatics(InjectSaga, WrappedComponent);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import invariant from 'invariant';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
import createReducer from '../reducers';
|
||||
import checkStore from './checkStore';
|
||||
|
||||
export function injectReducerFactory(store, isValid) {
|
||||
return function injectReducer(key, reducer) {
|
||||
if (!isValid) checkStore(store);
|
||||
|
||||
invariant(
|
||||
isString(key) && !isEmpty(key) && isFunction(reducer),
|
||||
'(app/utils...) injectReducer: Expected `reducer` to be a reducer function'
|
||||
);
|
||||
|
||||
// Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
|
||||
if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return;
|
||||
|
||||
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
|
||||
store.replaceReducer(createReducer(store.injectedReducers));
|
||||
};
|
||||
}
|
||||
|
||||
export default function getInjectors(store) {
|
||||
checkStore(store);
|
||||
|
||||
return {
|
||||
injectReducer: injectReducerFactory(store, true),
|
||||
};
|
||||
}
|
46
packages/strapi-admin/files/admin/src/utils/request.js
Normal file
46
packages/strapi-admin/files/admin/src/utils/request.js
Normal file
@ -0,0 +1,46 @@
|
||||
import 'whatwg-fetch';
|
||||
|
||||
/**
|
||||
* Parses the JSON returned by a network request
|
||||
*
|
||||
* @param {object} response A response from a network request
|
||||
*
|
||||
* @return {object} The parsed JSON from the request
|
||||
*/
|
||||
function parseJSON(response) {
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a network request came back fine, and throws an error if not
|
||||
*
|
||||
* @param {object} response A response from a network request
|
||||
*
|
||||
* @return {object|undefined} Returns either the response, or throws an error
|
||||
*/
|
||||
function checkStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a URL, returning a promise
|
||||
*
|
||||
* @param {string} url The URL we want to request
|
||||
* @param {object} [options] The options we want to pass to "fetch"
|
||||
*
|
||||
* @return {object} The response data
|
||||
*/
|
||||
export default function request(url, options) {
|
||||
return fetch(url, options)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON);
|
||||
}
|
86
packages/strapi-admin/files/admin/src/utils/sagaInjectors.js
Normal file
86
packages/strapi-admin/files/admin/src/utils/sagaInjectors.js
Normal file
@ -0,0 +1,86 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import isString from 'lodash/isString';
|
||||
import invariant from 'invariant';
|
||||
import conformsTo from 'lodash/conformsTo';
|
||||
|
||||
import checkStore from './checkStore';
|
||||
import {
|
||||
DAEMON,
|
||||
ONCE_TILL_UNMOUNT,
|
||||
RESTART_ON_REMOUNT,
|
||||
} from './constants';
|
||||
|
||||
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];
|
||||
|
||||
const checkKey = (key) => invariant(
|
||||
isString(key) && !isEmpty(key),
|
||||
'(app/utils...) injectSaga: Expected `key` to be a non empty string'
|
||||
);
|
||||
|
||||
const checkDescriptor = (descriptor) => {
|
||||
const shape = {
|
||||
saga: isFunction,
|
||||
mode: (mode) => isString(mode) && allowedModes.includes(mode),
|
||||
};
|
||||
invariant(
|
||||
conformsTo(descriptor, shape),
|
||||
'(app/utils...) injectSaga: Expected a valid saga descriptor'
|
||||
);
|
||||
};
|
||||
|
||||
export function injectSagaFactory(store, isValid) {
|
||||
return function injectSaga(key, descriptor = {}, args) {
|
||||
if (!isValid) checkStore(store);
|
||||
|
||||
const newDescriptor = { ...descriptor, mode: descriptor.mode || RESTART_ON_REMOUNT };
|
||||
const { saga, mode } = newDescriptor;
|
||||
|
||||
checkKey(key);
|
||||
checkDescriptor(newDescriptor);
|
||||
|
||||
let hasSaga = Reflect.has(store.injectedSagas, key);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const oldDescriptor = store.injectedSagas[key];
|
||||
// enable hot reloading of daemon and once-till-unmount sagas
|
||||
if (hasSaga && oldDescriptor.saga !== saga) {
|
||||
oldDescriptor.task.cancel();
|
||||
hasSaga = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSaga || (hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)) {
|
||||
store.injectedSagas[key] = { ...newDescriptor, task: store.runSaga(saga, args) }; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function ejectSagaFactory(store, isValid) {
|
||||
return function ejectSaga(key) {
|
||||
if (!isValid) checkStore(store);
|
||||
|
||||
checkKey(key);
|
||||
|
||||
if (Reflect.has(store.injectedSagas, key)) {
|
||||
const descriptor = store.injectedSagas[key];
|
||||
if (descriptor.mode !== DAEMON) {
|
||||
descriptor.task.cancel();
|
||||
// Clean up in production; in development we need `descriptor.saga` for hot reloading
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
|
||||
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function getInjectors(store) {
|
||||
checkStore(store);
|
||||
|
||||
return {
|
||||
injectSaga: injectSagaFactory(store, true),
|
||||
ejectSaga: ejectSagaFactory(store, true),
|
||||
};
|
||||
}
|
@ -66,7 +66,7 @@
|
||||
"express": "^4.15.4",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
"history": "3.0.0",
|
||||
"history": "^4.6.3",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"image-webpack-loader": "^3.3.1",
|
||||
@ -92,9 +92,8 @@
|
||||
"react-helmet": "^5.1.3",
|
||||
"react-intl": "^2.3.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "2.6.1",
|
||||
"react-router-redux": "4.0.5",
|
||||
"react-router-scroll": "0.2.1",
|
||||
"react-router-dom": "^4.1.2",
|
||||
"react-router-redux": "^5.0.0-alpha.6",
|
||||
"redux": "^3.7.2",
|
||||
"redux-immutable": "^4.0.0",
|
||||
"redux-saga": "^0.15.6",
|
||||
@ -112,4 +111,4 @@
|
||||
"devDependencies": {
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user