Created Admin and PluginDispatcher containers, update the history dependency to remove the error log in the console

This commit is contained in:
soupette 2019-04-02 21:23:42 +02:00
parent 0f554234d8
commit 661a89642b
19 changed files with 343 additions and 64 deletions

View File

@ -0,0 +1,15 @@
/*
*
* Admin actions
*
*/
import {
DEFAULT_ACTION,
} from './constants';
export function defaultAction() {
return {
type: DEFAULT_ACTION,
};
}

View File

@ -0,0 +1,7 @@
/*
*
* Admin constants
*
*/
export const DEFAULT_ACTION = 'StrapiAdmin/Admin/DEFAULT_ACTION';

View File

@ -0,0 +1,53 @@
/**
*
* Admin
*
*/
import React from 'react';
// import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { bindActionCreators, compose } from 'redux';
import makeSelectAdmin from './selectors';
import reducer from './reducer';
import saga from './saga';
export class Admin extends React.Component {
// eslint-disable-line react/prefer-stateless-function
render() {
return <div />;
}
}
Admin.propTypes = {};
const mapStateToProps = createStructuredSelector({
admin: makeSelectAdmin(),
});
function mapDispatchToProps(dispatch) {
return bindActionCreators({}, dispatch);
}
const withConnect = connect(
mapStateToProps,
mapDispatchToProps,
);
/* Remove this line if the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withReducer = strapi.injectReducer({ key: 'admin', reducer });
/* Remove the line below the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withSaga = strapi.injectSaga({ key: 'admin', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(Admin);

View File

@ -0,0 +1,23 @@
/*
*
* Admin reducer
*
*/
import { fromJS } from 'immutable';
import {
DEFAULT_ACTION,
} from './constants';
const initialState = fromJS({});
function adminReducer(state = initialState, action) {
switch (action.type) {
case DEFAULT_ACTION:
return state;
default:
return state;
}
}
export default adminReducer;

View File

@ -0,0 +1,6 @@
// import { take, call, put, select } from 'redux-saga/effects';
// Individual exports for testing
export default function* defaultSaga() {
// See example in containers/HomePage/saga.js
}

View File

@ -0,0 +1,23 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the admin state domain
*/
const selectAdminDomain = () => state => state.get('admin');
/**
* Other specific selectors
*/
/**
* Default selector used by Admin
*/
const makeSelectAdmin = () =>
createSelector(
selectAdminDomain(),
substate => substate.toJS(),
);
export default makeSelectAdmin;
export { selectAdminDomain };

View File

@ -0,0 +1,18 @@
import {
defaultAction,
} from '../actions';
import {
DEFAULT_ACTION,
} from '../constants';
describe('Admin actions', () => {
describe('Default Action', () => {
it('has a type of DEFAULT_ACTION', () => {
const expected = {
type: DEFAULT_ACTION,
};
expect(defaultAction()).toEqual(expected);
});
});
});

View File

@ -0,0 +1,21 @@
// import React from 'react';
// import { shallow } from 'enzyme';
// import mountWithIntl from 'testUtils/mountWithIntl';
// import formatMessagesWithPluginId from 'testUtils/formatMessages';
// This part is needed if you need to test the lifecycle of a container that contains FormattedMessages
// import pluginId from '../../../pluginId';
// import pluginTradsEn from '../../../translations/en.json';
// import { Admin } from '../index';
// const messages = formatMessagesWithPluginId(pluginId, pluginTradsEn);
// const renderComponent = (props = {}) => mountWithIntl(<Admin {...props} />, messages);
describe('<Admin />', () => {
it('should not crash', () => {
// shallow(<Admin />);
// renderComponent({});
});
});

View File

@ -0,0 +1,9 @@
import { fromJS } from 'immutable';
import adminReducer from '../reducer';
describe('adminReducer', () => {
it('returns the initial state', () => {
expect(adminReducer(undefined, {})).toEqual(fromJS({}));
});
});

View File

@ -0,0 +1,15 @@
/**
* Test sagas
*/
/* eslint-disable redux-saga/yield-effects */
// import { take, call, put, select } from 'redux-saga/effects';
// import { defaultSaga } from '../saga';
// const generator = defaultSaga();
describe('defaultSaga Saga', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -0,0 +1,10 @@
// import { fromJS } from 'immutable';
// import { makeSelectAdminDomain } from '../selectors';
// const selector = makeSelectAdminDomain();
describe('makeSelectAdminDomain', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -12,7 +12,6 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
// From strapi-helper-plugin
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
@ -25,36 +24,30 @@ import NotificationProvider from '../NotificationProvider';
import AppLoader from '../AppLoader';
import styles from './styles.scss';
export class App extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div>
<NotificationProvider />
<AppLoader>
{({ shouldLoad }) => {
if (shouldLoad) {
return <LoadingIndicatorPage />;
}
function App() {
return (
<div>
<NotificationProvider />
<AppLoader>
{({ shouldLoad }) => {
if (shouldLoad) {
return <LoadingIndicatorPage />;
}
return (
<div className={styles.container}>
<Switch>
<Route path="/" component={AdminPage} />
<Route path="" component={NotFoundPage} />
</Switch>
</div>
);
}}
</AppLoader>
</div>
);
}
return (
<div className={styles.container}>
<Switch>
<Route path='/' component={AdminPage} />
<Route path='' component={NotFoundPage} />
</Switch>
</div>
);
}}
</AppLoader>
</div>
);
}
App.contextTypes = {
router: PropTypes.object.isRequired,
};
App.propTypes = {};
export default App;

View File

@ -0,0 +1,31 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Route } from 'react-router-dom';
import AppLoader from '../../AppLoader';
import App from '../index';
describe('<App />', () => {
it('should render the <AppLoader />', () => {
const renderedComponent = shallow(<App />);
expect(renderedComponent.find(AppLoader)).toHaveLength(1);
});
it('Should render the <Switch /> if the app is loading', () => {
const topComp = shallow(<App />);
const insideAppLoaderNotLoading = shallow(
topComp.find(AppLoader).prop('children')({ shouldLoad: false }),
);
expect(insideAppLoaderNotLoading.find(Route).length).toBe(2);
});
it('should not render the <Switch /> if the app is loading', () => {
const topComp = shallow(<App />);
const insideAppLoaderLoading = shallow(
topComp.find(AppLoader).prop('children')({ shouldLoad: true }),
);
expect(insideAppLoaderLoading.find(Route).length).toBe(0);
});
});

View File

@ -1,7 +1,7 @@
/**
*
*
* AppLoader
*
*
*/
import React from 'react';
@ -9,16 +9,16 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import makeSelectApp from '../App/selectors';
class AppLoader extends React.Component {
export class AppLoader extends React.Component {
shouldLoad = () => {
const { appPlugins, plugins: mountedPlugins } = this.props;
return appPlugins.length !== Object.keys(mountedPlugins).length;
}
};
render() {
const { children } = this.props;
return children({ shouldLoad: this.shouldLoad() });
}
}
@ -31,4 +31,7 @@ AppLoader.propTypes = {
const mapStateToProps = makeSelectApp();
export default connect(mapStateToProps, null)(AppLoader);
export default connect(
mapStateToProps,
null,
)(AppLoader);

View File

@ -0,0 +1,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AppLoader } from '../index';
describe('<AppLoader />', () => {
let props;
beforeEach(() => {
props = {
appPlugins: [],
plugins: {},
};
});
it('should not crash', () => {
shallow(<AppLoader {...props}>{() => null}</AppLoader>);
});
});

View File

@ -4,12 +4,9 @@
*
*/
import { dispatch } from 'app';
import { dispatch } from '../../app';
import {
SHOW_NOTIFICATION,
HIDE_NOTIFICATION,
} from './constants';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
let nextNotificationId = 0;
@ -17,7 +14,7 @@ export function showNotification(message, status) {
nextNotificationId++; // eslint-disable-line no-plusplus
// Start timeout to hide the notification
((id) => {
(id => {
setTimeout(() => {
dispatch(hideNotification(id));
}, 2500);

View File

@ -10,12 +10,19 @@ import cn from 'classnames';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { FormattedMessage } from 'react-intl';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import injectSaga from '../../utils/injectSaga';
import injectReducer from '../../utils/injectReducer';
import OnboardingVideo from 'components/OnboardingVideo';
import OnboardingVideo from '../../components/OnboardingVideo';
import { getVideos, onClick, removeVideos, setVideoDuration, setVideoEnd, updateVideoStartTime } from './actions';
import {
getVideos,
onClick,
removeVideos,
setVideoDuration,
setVideoEnd,
updateVideoStartTime,
} from './actions';
import makeSelectOnboarding from './selectors';
import reducer from './reducer';
import saga from './saga';
@ -43,17 +50,17 @@ export class Onboarding extends React.Component {
setVideoEnd = () => {
this.setVideoEnd();
}
};
didPlayVideo = (index, currTime) => {
const eventName = `didPlay${index}GetStartedVideo`;
this.context.emitEvent(eventName, {timestamp: currTime});
}
this.context.emitEvent(eventName, { timestamp: currTime });
};
didStopVideo = (index, currTime) => {
const eventName = `didStop${index}Video`;
this.context.emitEvent(eventName, {timestamp: currTime});
}
this.context.emitEvent(eventName, { timestamp: currTime });
};
handleOpenModal = () => this.setState({ showVideos: true });
@ -61,16 +68,17 @@ export class Onboarding extends React.Component {
this.setState(prevState => ({ showVideos: !prevState.showVideos }));
const { showVideos } = this.state;
const eventName = showVideos ? 'didOpenGetStartedVideoContainer' : 'didCloseGetStartedVideoContainer';
const eventName = showVideos
? 'didOpenGetStartedVideoContainer'
: 'didCloseGetStartedVideoContainer';
this.context.emitEvent(eventName);
};
updateCurrentTime = (index, current, duration) => {
this.props.updateVideoStartTime(index, current);
const percent = current * 100 / duration;
const percent = (current * 100) / duration;
const video = this.props.videos[index];
if (percent >= 80) {
@ -80,7 +88,7 @@ export class Onboarding extends React.Component {
}
};
updateEnd = (index) => {
updateEnd = index => {
this.props.setVideoEnd(index, true);
};
@ -89,12 +97,29 @@ export class Onboarding extends React.Component {
const { videos, onClick, setVideoDuration } = this.props;
return (
<div className={cn(styles.videosWrapper, videos.length > 0 ? styles.visible : styles.hidden)}>
<div className={cn(styles.videosContent, this.state.showVideos ? styles.shown : styles.hide)}>
<div
className={cn(
styles.videosWrapper,
videos.length > 0 ? styles.visible : styles.hidden,
)}
>
<div
className={cn(
styles.videosContent,
this.state.showVideos ? styles.shown : styles.hide,
)}
>
<div className={styles.videosHeader}>
<p><FormattedMessage id="app.components.Onboarding.title" /></p>
<p>
<FormattedMessage id='app.components.Onboarding.title' />
</p>
{videos.length && (
<p>{Math.floor((videos.filter(v => v.end).length)*100/videos.length)}<FormattedMessage id="app.components.Onboarding.label.completed" /></p>
<p>
{Math.floor(
(videos.filter(v => v.end).length * 100) / videos.length,
)}
<FormattedMessage id='app.components.Onboarding.label.completed' />
</p>
)}
</div>
<ul className={styles.onboardingList}>
@ -120,8 +145,8 @@ export class Onboarding extends React.Component {
onClick={this.handleVideosToggle}
className={this.state.showVideos ? styles.active : ''}
>
<i className="fa fa-question" />
<i className="fa fa-times" />
<i className='fa fa-question' />
<i className='fa fa-times' />
<span />
</button>
</div>
@ -157,7 +182,17 @@ Onboarding.propTypes = {
const mapStateToProps = makeSelectOnboarding();
function mapDispatchToProps(dispatch) {
return bindActionCreators({ getVideos, onClick, setVideoDuration, updateVideoStartTime, setVideoEnd, removeVideos }, dispatch);
return bindActionCreators(
{
getVideos,
onClick,
setVideoDuration,
updateVideoStartTime,
setVideoEnd,
removeVideos,
},
dispatch,
);
}
const withConnect = connect(

View File

@ -2,12 +2,13 @@
* Common configuration for the app in both dev an prod mode
*/
import createHistory from 'history/createBrowserHistory';
import { createBrowserHistory } from 'history';
import './public-path';
import configureStore from './configureStore';
const basename = strapi.remoteURL.replace(window.location.origin, '');
const history = createHistory({
const history = createBrowserHistory({
basename,
});
const store = configureStore({}, history);

View File

@ -83,7 +83,7 @@
"babel-polyfill": "6.26.0",
"bootstrap": "^4.0.0-alpha.6",
"classnames": "^2.2.5",
"history": "^4.6.3",
"history": "^4.9.0",
"immutable": "^3.8.2",
"imports-loader": "^0.7.1",
"invariant": "2.2.1",