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

@ -9,12 +9,12 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import makeSelectApp from '../App/selectors'; import makeSelectApp from '../App/selectors';
class AppLoader extends React.Component { export class AppLoader extends React.Component {
shouldLoad = () => { shouldLoad = () => {
const { appPlugins, plugins: mountedPlugins } = this.props; const { appPlugins, plugins: mountedPlugins } = this.props;
return appPlugins.length !== Object.keys(mountedPlugins).length; return appPlugins.length !== Object.keys(mountedPlugins).length;
} };
render() { render() {
const { children } = this.props; const { children } = this.props;
@ -31,4 +31,7 @@ AppLoader.propTypes = {
const mapStateToProps = makeSelectApp(); 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 { import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
SHOW_NOTIFICATION,
HIDE_NOTIFICATION,
} from './constants';
let nextNotificationId = 0; let nextNotificationId = 0;
@ -17,7 +14,7 @@ export function showNotification(message, status) {
nextNotificationId++; // eslint-disable-line no-plusplus nextNotificationId++; // eslint-disable-line no-plusplus
// Start timeout to hide the notification // Start timeout to hide the notification
((id) => { (id => {
setTimeout(() => { setTimeout(() => {
dispatch(hideNotification(id)); dispatch(hideNotification(id));
}, 2500); }, 2500);

View File

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

View File

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

View File

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