Merge branch 'master' into doc-poly-update

This commit is contained in:
Jim LAURIE 2018-06-21 11:33:42 +02:00 committed by GitHub
commit b2d545edac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 692 additions and 274 deletions

View File

@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { isFunction, isObject } from 'lodash';
import cn from 'classnames';
import LoadingBar from 'components/LoadingBar';
import styles from './styles.scss';
@ -27,6 +28,8 @@ function Sub({ bordered, content, link, name, style, title, underline }) {
return (
<a className={cn(styles.subWrapper, bordered && styles.subBordered, styles.link)} href={`https://blog.strapi.io/${link}`} target="_blank">
<span>{title}</span>
{title === '' && <LoadingBar />}
{content === '' && <LoadingBar style={{ width: '40%' }} />}
<p style={style}>
{isFunction(content) ? content() : content}
</p>

View File

@ -6,8 +6,8 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import SupportUsTitle from 'components/SupportUsTitle/Loadable';
import SupportUsCta from 'components/SupportUsCta/Loadable';
import SupportUsTitle from 'components/SupportUsTitle';
import SupportUsCta from 'components/SupportUsCta';
import styles from './styles.scss';

View File

@ -185,6 +185,10 @@ export class AdminPage extends React.Component {
const header = this.showLeftMenu() ? <Header /> : '';
const style = this.showLeftMenu() ? {} : { width: '100%' };
if (adminPage.isLoading) {
return <div />;
}
return (
<div className={styles.adminPage}>
{this.showLeftMenu() && (

View File

@ -16,6 +16,7 @@ import {
const initialState = fromJS({
allowGa: true,
currentEnvironment: 'development',
isLoading: true,
layout: Map({}),
strapiVersion: '3',
});
@ -23,7 +24,9 @@ const initialState = fromJS({
function adminPageReducer(state = initialState, action) {
switch (action.type) {
case GET_CURR_ENV_SUCCEEDED:
return state.update('currentEnvironment', () => action.currentEnvironment);
return state
.update('isLoading', () => false)
.update('currentEnvironment', () => action.currentEnvironment);
case GET_GA_STATUS_SUCCEEDED:
return state.update('allowGa', () => action.allowGa);
case GET_LAYOUT_SUCCEEDED:

View File

@ -3,9 +3,9 @@
*/
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
loading: LoadingIndicatorPage,
});

View File

@ -36,15 +36,27 @@ function getSrc(name) {
}
}
function SocialLink({ link, name }) {
return (
<div className={cn(styles.socialLink, 'col-md-6 col-lg-6')}>
<a href={link} target="_blank">
<div><img src={getSrc(name)} /></div>
<span>{name}</span>
</a>
</div>
);
class SocialLink extends React.PureComponent {
state = { imgLoaded: false };
handleImgLoaded = () => this.setState({ imgLoaded: true });
render() {
const { link, name } = this.props;
const { imgLoaded } = this.state;
return (
<div className={cn(styles.socialLink, 'col-md-6 col-lg-6')}>
<a href={link} target="_blank">
<div>
{!imgLoaded && <div className={styles.spinner}><div /></div>}
<img src={getSrc(name)} onLoad={this.handleImgLoaded} />
</div>
<span>{name}</span>
</a>
</div>
);
}
}
SocialLink.propTypes = {

View File

@ -14,12 +14,12 @@ import PropTypes from 'prop-types';
import { get, isEmpty, upperFirst } from 'lodash';
import cn from 'classnames';
import Block from 'components/HomePageBlock/Loadable';
import Block from 'components/HomePageBlock';
import Button from 'components/Button';
import Sub from 'components/Sub/Loadable';
import Sub from 'components/Sub';
import Input from 'components/InputText';
import SupportUsCta from 'components/SupportUsCta/Loadable';
import SupportUsTitle from 'components/SupportUsTitle/Loadable';
import SupportUsCta from 'components/SupportUsCta';
import SupportUsTitle from 'components/SupportUsTitle';
import { selectPlugins } from 'containers/App/selectors';

View File

@ -8,7 +8,10 @@ import { fromJS, List, Map } from 'immutable';
import { GET_ARTICLES_SUCCEEDED, ON_CHANGE, SUBMIT_SUCCEEDED } from './constants';
const initialState = fromJS({
articles: List([]),
articles: List([
{content: '', title: '', link: ''},
{content: '', title: '', link: ''},
]),
body: Map({
email: '',
}),

View File

@ -267,3 +267,23 @@
max-width: 55rem !important;
margin-bottom: 31px;
}
.spinner {
display: flex;
justify-content: space-around;
width: 100%;
margin: auto;
> div {
border: 2px solid #f3f3f3; /* Light grey */
border-top: 2px solid #3498db; /* Blue */
border-radius: 50%;
width: 10px;
height: 10px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -1,8 +1,8 @@
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
loading: LoadingIndicatorPage,
});

View File

@ -1,64 +0,0 @@
{
"availablePlugins": [
{
"description": "content-manager.plugin.description",
"id": "content-manager",
"icon": "plug",
"isCompatible": true,
"name": "Content Manager",
"price": 0,
"ratings": 1,
"longDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"downloads_nb": 3,
"logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png"
},
{
"description": "content-type-builder.plugin.description",
"id": "content-type-builder",
"icon": "paint-brush",
"isCompatible": true,
"name": "content type builder",
"price": 0,
"ratings": 4.4,
"longDescription": "lorem",
"downloads_nb": 3,
"logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png"
},
{
"description": "settings-manager.plugin.description",
"id": "settings-manager",
"icon": "wrench",
"isCompatible": true,
"name": "settings manager",
"price": 0,
"ratings": 2.6,
"longDescription": "lorem",
"downloads_nb": 3,
"logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png"
},
{
"description": "users-permissions.plugin.description",
"id": "users-permissions",
"icon": "users",
"isCompatible": true,
"name": "Auth & Permissions",
"price": 0,
"ratings": 1,
"longDescription": "lorem",
"downloads_nb": 3,
"logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png"
},
{
"description": "users-permissions.plugin.description",
"id": "test",
"icon": "users",
"isCompatible": true,
"name": "test",
"price": 0,
"ratings": 1,
"longDescription": "lorem",
"downloads_nb": 3,
"logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png"
}
]
}

View File

@ -24,7 +24,8 @@ import DownloadInfo from 'components/DownloadInfo';
import OverlayBlocker from 'components/OverlayBlocker';
import PluginCard from 'components/PluginCard';
import PluginHeader from 'components/PluginHeader';
import SupportUsBanner from 'components/SupportUsBanner/Loadable';
import SupportUsBanner from 'components/SupportUsBanner';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
@ -64,6 +65,10 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line
}
render() {
if (!this.props.didFetchPlugins) {
return <LoadingIndicatorPage />;
}
return (
<div>
<OverlayBlocker isOpen={this.props.blockApp}>

View File

@ -1,8 +1,8 @@
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
loading: LoadingIndicatorPage,
});

View File

@ -15,10 +15,11 @@ import cn from 'classnames';
import PluginHeader from 'components/PluginHeader';
import ListPlugins from 'components/ListPlugins';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import { makeSelectCurrentEnv, makeSelectPluginDeleteAction, makeSelectPlugins } from './selectors';
import { makeSelectCurrentEnv, makeSelectPluginDeleteAction, makeSelectPlugins, makeSelectIsLoading } from './selectors';
import { getPlugins, onDeletePluginClick, onDeletePluginConfirm } from './actions';
import reducer from './reducer';
import saga from './saga';
@ -36,6 +37,10 @@ export class ListPluginsPage extends React.Component { // eslint-disable-line re
}
render() {
if (this.props.isLoading) {
return <LoadingIndicatorPage />;
}
return (
<div>
<FormattedMessage id="app.components.ListPluginsPage.helmet.title">
@ -79,6 +84,7 @@ ListPluginsPage.propTypes = {
currentEnvironment: PropTypes.string.isRequired,
getPlugins: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
isLoading: PropTypes.bool.isRequired,
onDeletePluginClick: PropTypes.func.isRequired,
onDeletePluginConfirm: PropTypes.func.isRequired,
pluginActionSucceeded: PropTypes.bool.isRequired,
@ -87,6 +93,7 @@ ListPluginsPage.propTypes = {
const mapStateToProps = createStructuredSelector({
currentEnvironment: makeSelectCurrentEnv(),
isLoading: makeSelectIsLoading(),
pluginActionSucceeded: makeSelectPluginDeleteAction(),
plugins: makeSelectPlugins(),
});

View File

@ -15,6 +15,7 @@ import {
const initialState = fromJS({
currentEnvironment: 'development',
deleteActionSucceeded: false,
isLoading: true,
plugins: Map({}),
pluginToDelete: '',
});
@ -35,7 +36,8 @@ function listPluginsPageReducer(state = initialState, action) {
return state.update('currentEnvironment', () => action.currentEnvironment);
case GET_PLUGINS_SUCCEEDED:
return state
.set('plugins', Map(action.plugins));
.set('plugins', Map(action.plugins))
.update('isLoading', () => false);
case ON_DELETE_PLUGIN_CLICK:
return state
.set('pluginToDelete', action.pluginToDelete);

View File

@ -39,9 +39,15 @@ const makeSelectCurrentEnv = () => createSelector(
(substate) => substate.get('currentEnvironment'),
);
const makeSelectIsLoading = () => createSelector(
selectListPluginsPageDomain(),
(substate) => substate.get('isLoading'),
);
export default makeSelectListPluginsPage;
export {
makeSelectCurrentEnv,
makeSelectIsLoading,
selectListPluginsPageDomain,
makeSelectPluginToDelete,
makeSelectPluginDeleteAction,

View File

@ -1,8 +1,8 @@
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
loading: LoadingIndicatorPage,
});

View File

@ -10,13 +10,18 @@
import './public-path.js'; // eslint-disable-line import/extensions
import React from 'react';
import Loadable from 'react-loadable';
import { Provider } from 'react-redux';
import App from 'containers/App'; // eslint-disable-line
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import configureStore from './store';
import { translationMessages } from './i18n';
const LoadableApp = Loadable({
loader: () => import('containers/App'),
loading: LoadingIndicatorPage,
});
const tryRequireRoot = (source) => {
try {
return require('../../../../admin/src/' + source + '.js').default; // eslint-disable-line prefer-template
@ -62,7 +67,7 @@ const store = configureStore({}, strapi.router, pluginName);
function Comp(props) {
return (
<Provider store={store}>
<App {...props} />
<LoadableApp {...props} />
</Provider>
);
}

View File

@ -7,7 +7,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isObject, merge } from 'lodash';
import Loadable from 'react-loadable';
// Design
import InputAddonWithErrors from 'components/InputAddonWithErrors';
import InputCheckboxWithErrors from 'components/InputCheckboxWithErrors';
@ -21,13 +21,8 @@ import InputPasswordWithErrors from 'components/InputPasswordWithErrors';
import InputTextAreaWithErrors from 'components/InputTextAreaWithErrors';
import InputTextWithErrors from 'components/InputTextWithErrors';
import InputToggleWithErrors from 'components/InputToggleWithErrors';
import WysiwygWithErrors from 'components/WysiwygWithErrors';
import InputJSONWithErrors from 'components/InputJSONWithErrors';
// import WysiwygWithErrors from 'components/WysiwygWithErrors';
const Loading = () => <div>Loading ...</div>;
const LoadableWysiwyg = Loadable({
loader: () => import('components/WysiwygWithErrors'),
loading: Loading,
});
const DefaultInputError = ({ type }) => <div>Your input type: <b>{type}</b> does not exist</div>;
@ -46,7 +41,7 @@ const inputs = {
text: InputTextWithErrors,
textarea: InputTextAreaWithErrors,
toggle: InputToggleWithErrors,
wysiwyg: LoadableWysiwyg,
wysiwyg: WysiwygWithErrors,
};
function InputsIndex(props) {

View File

@ -0,0 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.scss';
const LoadingBar = ({ style }) => <div className={styles.loaderBar} style={style} />;
LoadingBar.defaultProps = {
style: {},
};
LoadingBar.propTypes = {
style: PropTypes.object,
};
export default LoadingBar;

View File

@ -0,0 +1,28 @@
.loaderBar {
height: 6px;
width: 20%;
margin-top: 10px;
position: relative;
overflow: hidden;
background-color: #F3F3F4;
border-radius: 2px;
&:before {
display: block;
position: absolute;
content: '';
left: -200px;
width: 200px;
height: 6px;
background-color: rgb(227, 227, 231);
animation: loading 2s linear infinite;
}
}
@keyframes loading {
from {left: -200px; width: 30%;}
50% {width: 30%;}
70% {width: 70%;}
80% { left: 50%;}
95% {left: 120%;}
to {left: 100%;}
}

View File

@ -1,5 +0,0 @@
import styled from 'styled-components';
const Div = styled.div``;
export default Div;

View File

@ -6,8 +6,12 @@
import React from 'react';
import Div from './Div';
import styles from './styles.scss';
const LoadingIndicator = () => <Div>Loading....</Div>;
const LoadingIndicator = () => {
return (
<div className={styles.loader}><div /></div>
);
};
export default LoadingIndicator;

View File

@ -0,0 +1,18 @@
.loader {
display: flex;
justify-content: space-around;
width: 100%;
> div {
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #555555 !important; /* Blue */
border-radius: 50%;
width: 26px;
height: 26px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -0,0 +1,17 @@
/**
*
* LoadingIndicatorPage
*
*/
import React from 'react';
import styles from './styles.scss';
const LoadingIndicatorPage = () => {
return (
<div className={styles.loaderPage}><div /></div>
);
};
export default LoadingIndicatorPage;

View File

@ -0,0 +1,21 @@
.loaderPage {
display: flex;
flex-direction: column;
justify-content: space-around;
width: 100%;
height: 100vh;
> div {
margin: auto;
border: 6px solid #f3f3f3; /* Light grey */
border-top: 6px solid #1C91E7; /* Blue */
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -0,0 +1,8 @@
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
});

View File

@ -13,13 +13,14 @@ import PluginHeaderActions from 'components/PluginHeaderActions';
import styles from './styles.scss';
function PluginHeader({ actions, description, overrideRendering, subActions, title }) {
function PluginHeader({ actions, description, overrideRendering, subActions, title, withDescriptionAnim }) {
return (
<div className={cn(styles.pluginHeader, 'row')}>
<div className="col-lg-7">
<PluginHeaderTitle
title={title}
description={description}
withDescriptionAnim={withDescriptionAnim}
/>
</div>
<div className="col-lg-2 justify-content-end">
@ -43,6 +44,7 @@ PluginHeader.defaultProps = {
overrideRendering: false,
subActions: [],
title: '',
withDescriptionAnim: false,
};
PluginHeader.propTypes = {
@ -68,6 +70,7 @@ PluginHeader.propTypes = {
values: PropTypes.object,
}),
]),
withDescriptionAnim: PropTypes.bool,
};
export default PluginHeader;

View File

@ -9,9 +9,11 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { isFunction, isObject } from 'lodash';
import LoadingBar from 'components/LoadingBar';
import styles from './styles.scss';
function PluginHeaderTitle({ description, title }) {
function PluginHeaderTitle({ description, title, withDescriptionAnim }) {
const contentTitle = formatData(title);
const contentDescription = formatData(description);
@ -20,9 +22,15 @@ function PluginHeaderTitle({ description, title }) {
<h1 className={styles.pluginHeaderTitleName}>
{contentTitle}
</h1>
<p className={styles.pluginHeaderTitleDescription}>
{contentDescription}&nbsp;
</p>
{ withDescriptionAnim ?
(
<LoadingBar />
) : (
<p className={styles.pluginHeaderTitleDescription}>
{contentDescription}&nbsp;
</p>
)
}
</div>
);
}
@ -43,6 +51,7 @@ const formatData = (data) => {
PluginHeaderTitle.defaultProps = {
description: '',
title: '',
withDescriptionAnim: false,
};
PluginHeaderTitle.propTypes = {
@ -62,6 +71,7 @@ PluginHeaderTitle.propTypes = {
values: PropTypes.object,
}),
]),
withDescriptionAnim: PropTypes.bool,
};
export default PluginHeaderTitle;

View File

@ -0,0 +1,7 @@
import Loadable from 'react-loadable';
import Loader from './Loader';
export default Loadable({
loader: () => import('./index'),
loading: Loader,
});

View File

@ -0,0 +1,11 @@
import React from 'react';
import styles from './styles.scss';
const Loader = () => (
<div className="col-md-12">
<div className={styles.wysLoader}><div /></div>
</div>
);
export default Loader;

View File

@ -3,3 +3,22 @@
font-size: 1.3rem;
font-family: 'Lato';
}
.wysLoader {
display: flex;
justify-content: space-around;
width: 100%;
> div {
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -12,22 +12,29 @@ import PropTypes from 'prop-types';
import Button from 'components/CustomButton';
import Logo from '../../assets/images/icon_filter.png';
const imgStyle = {
marginTop: '-3px',
marginRight: '10px',
height: '7px',
fontSize: '12px',
};
import styles from './styles.scss';
function AddFilterCTA({ onClick, showHideText }) {
const id = showHideText ? 'hide' : 'add';
class AddFilterCTA extends React.Component {
state = { imgLoaded: false };
return (
<Button type="button" onClick={onClick} marginTop>
<img src={Logo} alt="filter_logo" style={imgStyle} />
<FormattedMessage id={`content-manager.components.AddFilterCTA.${id}`} />
</Button>
);
handleImgLoaded = () => this.setState({ imgLoaded: true });
render() {
const { onClick, showHideText } = this.props;
const { imgLoaded } = this.state;
const id = showHideText ? 'hide' : 'add';
return (
<Button type="button" onClick={onClick} marginTop>
<div className={styles.ctaWrapper}>
{!imgLoaded && <div className={styles.spinner}><div /></div>}
<img src={Logo} onLoad={this.handleImgLoaded} alt="filter_logo" className={styles.imgCta} />
<FormattedMessage id={`content-manager.components.AddFilterCTA.${id}`} />
</div>
</Button>
);
}
}
AddFilterCTA.defaultProps = {

View File

@ -0,0 +1,33 @@
.spinner {
display: flex;
justify-content: space-around;
width: 100%;
margin: auto;
> div {
border: 2px solid #f3f3f3; /* Light grey */
border-top: 2px solid #3498db; /* Blue */
border-radius: 50%;
width: 10px;
height: 10px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.imgCta {
height: 7px;
margin: auto;
margin-right: 0px;
font-size: 12px;
}
.ctaWrapper {
display: flex;
> span {
margin-left: 10px;
}
}

View File

@ -17,6 +17,9 @@ const CustomButton = styled.button`
&:hover {
background: #F7F8F8;
}
&:focus, &:active {
outline:0;
}
`;
export default CustomButton;

View File

@ -12,6 +12,7 @@ import TableDelete from 'components/TableDelete';
import TableHeader from 'components/TableHeader';
import TableRow from 'components/TableRow';
import TableEmpty from 'components/TableEmpty';
import TableLoading from 'components/TableLoading';
import styles from './styles.scss';
@ -61,7 +62,7 @@ class Table extends React.Component {
onToggleDeleteAll={this.props.onToggleDeleteAll}
/>
)}
{rows}
{this.props.showLoader ? <TableLoading colspan={this.props.headers.length + 1} /> : rows}
</tbody>
</table>
);
@ -76,6 +77,7 @@ Table.defaultProps = {
entriesToDelete: [],
handleDelete: () => {},
search: '',
showLoader: false,
};
Table.propTypes = {
@ -98,6 +100,7 @@ Table.propTypes = {
route: PropTypes.object.isRequired,
routeParams: PropTypes.object.isRequired,
search: PropTypes.string,
showLoader: PropTypes.bool,
sort: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,26 @@
/**
*
* TableLoading
*/
import React from 'react';
import PropTypes from 'prop-types';
import LoadingIndicator from 'components/LoadingIndicator';
import styles from './styles.scss';
function TableLoading({ colspan }) {
return (
<tr className={styles.tableLoading}>
<td colSpan={colspan + 1}>
<LoadingIndicator />
</td>
</tr>
);
}
TableLoading.propTypes = {
colspan: PropTypes.number.isRequired,
};
export default TableLoading;

View File

@ -0,0 +1,17 @@
.tableLoading { /* stylelint-disable */
width: 100%;
height: 108px;
background: #ffffff;
td{
height: 106px;
vertical-align: middle;
line-height: 106px;
font-size: 1.3rem;
font-weight: 400;
color: #333740;
text-align: center;
border-collapse: collapse;
border-top: 1px solid #F1F1F2 !important;
}
}

View File

@ -19,10 +19,10 @@ import getQueryParameters from 'utils/getQueryParameters';
import Home from 'containers/Home';
import EditPage from 'containers/EditPage';
import ListPage from 'containers/ListPage';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import EmptyAttributesView from 'components/EmptyAttributesView';
import {
emptyStore,
loadModels,
} from './actions';
import { makeSelectLoading, makeSelectModels, makeSelectModelEntries } from './selectors';
@ -34,13 +34,9 @@ class App extends React.Component {
this.props.loadModels();
}
componentWillUnmount() {
this.props.emptyStore();
}
render() {
if (this.props.loading) {
return <div />;
return <LoadingIndicatorPage />;
}
const currentModelName = this.props.location.pathname.split('/')[3];
@ -69,7 +65,6 @@ App.contextTypes = {
};
App.propTypes = {
emptyStore: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired,
loadModels: PropTypes.func.isRequired,
@ -84,8 +79,6 @@ App.propTypes = {
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
emptyStore,
// getModelEntries,
loadModels,
},
dispatch,

View File

@ -19,7 +19,7 @@ const initialState = fromJS({
function appReducer(state = initialState, action) {
switch (action.type) {
case EMPTY_STORE:
return initialState;
return state;
case GET_MODEL_ENTRIES_SUCCEEDED:
return state.set('modelEntries', action.count);
case LOAD_MODELS:

View File

@ -17,6 +17,7 @@ import cn from 'classnames';
// ./node_modules/strapi-helper-plugin/lib/src
// or strapi/packages/strapi-helper-plugin/lib/src
import BackHeader from 'components/BackHeader';
import LoadingIndicator from 'components/LoadingIndicator';
import PluginHeader from 'components/PluginHeader';
// Plugin's components
@ -108,7 +109,7 @@ export class EditPage extends React.Component {
* @return {[type]} [description]
*/
getLayout = () => (
bindLayout.call(this, this.props.editPage.layout)
bindLayout.call(this, get(this.props.editPage, ['layout', this.getModelName()], {}))
)
/**
@ -150,6 +151,14 @@ export class EditPage extends React.Component {
get(this.props.schema, ['plugins', this.getSource(), this.getModelName()])
: get(this.props.schema, [this.getModelName()]);
getPluginHeaderTitle = () => {
if (this.isCreating()) {
return toString(this.props.editPage.pluginHeaderTitle);
}
return this.props.match.params.id;
}
/**
* Retrieve the model's source
* @return {String}
@ -236,6 +245,7 @@ export class EditPage extends React.Component {
kind: 'secondary',
onClick: this.props.onCancel,
type: 'button',
disabled: this.showLoaders(),
},
{
kind: 'primary',
@ -244,10 +254,17 @@ export class EditPage extends React.Component {
type: 'submit',
loader: this.props.editPage.showLoader,
style: this.props.editPage.showLoader ? { marginRight: '18px' } : {},
disabled: this.showLoaders(),
},
]
);
showLoaders = () => {
const { editPage: { isLoading, layout } } = this.props;
return isLoading && !this.isCreating() || isLoading && get(layout, this.getModelName()) === undefined;
}
render() {
const { editPage } = this.props;
@ -258,24 +275,28 @@ export class EditPage extends React.Component {
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
actions={this.pluginHeaderActions()}
title={{ id: toString(editPage.pluginHeaderTitle) }}
title={{ id: this.getPluginHeaderTitle() }}
/>
<div className="row">
<div className={this.isRelationComponentNull() ? 'col-lg-12' : 'col-lg-9'}>
<div className={styles.main_wrapper}>
<Edit
attributes={this.getModelAttributes()}
didCheckErrors={editPage.didCheckErrors}
formValidations={editPage.formValidations}
formErrors={editPage.formErrors}
layout={this.getLayout()}
modelName={this.getModelName()}
onBlur={this.handleBlur}
onChange={this.handleChange}
record={editPage.record}
resetProps={editPage.resetProps}
schema={this.getSchema()}
/>
{this.showLoaders() ? (
<LoadingIndicator />
) : (
<Edit
attributes={this.getModelAttributes()}
didCheckErrors={editPage.didCheckErrors}
formValidations={editPage.formValidations}
formErrors={editPage.formErrors}
layout={this.getLayout()}
modelName={this.getModelName()}
onBlur={this.handleBlur}
onChange={this.handleChange}
record={editPage.record}
resetProps={editPage.resetProps}
schema={this.getSchema()}
/>
)}
</div>
</div>
{!this.isRelationComponentNull() && (
@ -360,3 +381,5 @@ export default compose(
withSaga,
withConnect,
)(EditPage);

View File

@ -27,7 +27,8 @@ const initialState = fromJS({
isCreating: false,
id: '',
initialRecord: Map({}),
layout: Map({}),
isLoading: true,
layout: fromJS({}),
modelName: '',
pluginHeaderTitle: 'New Entry',
record: Map({}),
@ -44,11 +45,14 @@ function editPageReducer(state = initialState, action) {
case GET_DATA_SUCCEEDED:
return state
.update('id', () => action.id)
.update('isLoading', () => false)
.update('initialRecord', () => Map(action.data))
.update('pluginHeaderTitle', () => action.pluginHeaderTitle)
.update('record', () => Map(action.data));
case GET_LAYOUT_SUCCEEDED:
return state.update('layout', () => Map(action.layout));
return state
.update('isLoading', () => false)
.updateIn(['layout', state.get('modelName')], () => Map(action.layout));
case INIT_MODEL_PROPS:
return state
.update('formValidations', () => List(action.formValidations))
@ -63,7 +67,7 @@ function editPageReducer(state = initialState, action) {
.update('record', () => state.get('initialRecord'))
.update('resetProps', (v) => v = !v);
case RESET_PROPS:
return initialState;
return initialState.update('layout', () => state.get('layout'));
case SET_FILE_RELATIONS:
return state.set('fileRelations', List(action.fileRelations));
case SET_FORM_ERRORS:

View File

@ -72,10 +72,11 @@ export function deleteSeveralDataSuccess() {
};
}
export function getData(currentModel, source) {
export function getData(currentModel, source, setUpdatingParams = false) {
return {
type: GET_DATA,
currentModel,
setUpdatingParams,
source,
};
}

View File

@ -87,7 +87,7 @@ export class ListPage extends React.Component {
}
if (search !== this.props.location.search) {
this.getData(this.props);
this.getData(this.props, true);
}
if (prevProps.listPage.filtersUpdated !== filtersUpdated) {
@ -120,7 +120,7 @@ export class ListPage extends React.Component {
* Function to fetch data
* @param {Object} props
*/
getData = props => {
getData = (props, setUpdatingParams = false) => {
const source = getQueryParameters(props.location.search, 'source');
const _limit = toInteger(getQueryParameters(props.location.search, '_limit')) || 10;
const _page = toInteger(getQueryParameters(props.location.search, '_page')) || 1;
@ -130,7 +130,7 @@ export class ListPage extends React.Component {
const filters = generateFiltersFromSearch(props.location.search);
this.props.setParams(params, filters);
this.props.getData(props.match.params.slug, source);
this.props.getData(props.match.params.slug, source, setUpdatingParams);
};
/**
@ -157,8 +157,9 @@ export class ListPage extends React.Component {
* Generate the redirect URI when editing an entry
* @type {String}
*/
generateRedirectURI = () =>
`?redirectUrl=/plugins/content-manager/${this.getCurrentModelName().toLowerCase()}${this.generateSearch()}`;
generateRedirectURI = () => (
`?redirectUrl=/plugins/content-manager/${this.getCurrentModelName().toLowerCase()}${this.generateSearch()}`
);
generateSearch = () => {
const {
@ -194,7 +195,7 @@ export class ListPage extends React.Component {
areAllEntriesSelected = () => {
const { listPage: { entriesToDelete, records } } = this.props;
return entriesToDelete.length === records.length && records.length > 0;
return entriesToDelete.length === get(records, this.getCurrentModelName(), []).length && get(records, this.getCurrentModelName(), []).length > 0;
};
/**
@ -304,6 +305,12 @@ export class ListPage extends React.Component {
};
showLoaders = () => {
const { listPage: { isLoading, records, updatingParams } } = this.props;
return updatingParams || isLoading && get(records, this.getCurrentModelName()) === undefined;
}
render() {
const {
addFilter,
@ -311,9 +318,11 @@ export class ListPage extends React.Component {
listPage,
listPage: {
appliedFilters,
count,
entriesToDelete,
filters,
filterToFocus,
records,
params,
showFilter,
showWarningDeleteAll,
@ -356,16 +365,17 @@ export class ListPage extends React.Component {
actions={pluginHeaderActions}
description={{
id:
listPage.count > 1
get(count, this.getCurrentModelName(), 0) > 1
? 'content-manager.containers.List.pluginHeaderDescription'
: 'content-manager.containers.List.pluginHeaderDescription.singular',
values: {
label: listPage.count,
label: get(count, this.getCurrentModelName(), 0),
},
}}
title={{
id: this.getCurrentModelName() || 'Content Manager',
}}
withDescriptionAnim={this.showLoaders()}
/>
<div className={cn(styles.wrapper)}>
<FiltersPickWrapper
@ -416,11 +426,12 @@ export class ListPage extends React.Component {
onClickSelect={onClickSelect}
onToggleDeleteAll={onToggleDeleteAll}
primaryKey={this.getCurrentModel().primaryKey || 'id'}
records={listPage.records}
records={get(records, this.getCurrentModelName(), [])}
redirectUrl={this.generateRedirectURI()}
route={this.props.match}
routeParams={this.props.match.params}
search={params._q}
showLoader={this.showLoaders()}
sort={params._sort}
/>
<PopUpWarning
@ -450,7 +461,7 @@ export class ListPage extends React.Component {
}}
/>
<PageFooter
count={listPage.count}
count={get(count, this.getCurrentModelName(), 0)}
onChangeParams={this.handleChangeParams}
params={listPage.params}
style={{ marginTop: '2.9rem', padding: '0 15px 0 15px' }}

View File

@ -13,6 +13,7 @@ import {
CHANGE_PARAMS,
DELETE_DATA_SUCCESS,
DELETE_SEVERAL_DATA_SUCCESS,
GET_DATA,
GET_DATA_SUCCEEDED,
ON_CHANGE,
ON_CLICK_REMOVE,
@ -29,20 +30,23 @@ import {
const initialState = fromJS({
appliedFilters: List([]),
count: 0,
count: fromJS({}),
currentModel: '',
entriesToDelete: List([]),
filters: List([]),
filtersUpdated: false,
filterToFocus: null,
isLoading: true,
params: Map({
_limit: 10,
_page: 1,
_sort: '',
_q: '',
}),
records: List([]),
records: fromJS({}),
showFilter: false,
showWarningDeleteAll: false,
updatingParams: false,
});
function listPageReducer(state = initialState, action) {
@ -51,7 +55,7 @@ function listPageReducer(state = initialState, action) {
return state.update('appliedFilters', list => list.push(Map(action.filter)));
case DELETE_DATA_SUCCESS:
return state
.update('records', (list) => (
.updateIn(['records', state.get('currentModel')], (list) => (
list.filter(obj => {
if (obj._id) {
return obj._id !== action.id;
@ -60,7 +64,7 @@ function listPageReducer(state = initialState, action) {
return obj.id !== parseInt(action.id, 10);
})
))
.update('count', (v) => v = v - 1);
.updateIn(['count', state.get('currentModel')], v => v = v - 1);
case DELETE_SEVERAL_DATA_SUCCESS:
return state
.update('showWarningDeleteAll', () => false)
@ -81,13 +85,26 @@ function listPageReducer(state = initialState, action) {
return !v;
}
return v;
})
.update('updatingParams', () => true);
case GET_DATA:
return state
.update('isLoading', () => true)
.update('currentModel', () => action.currentModel)
.update('updatingParams', v => {
if (action.setUpdatingParams) {
return true;
}
return v;
});
case GET_DATA_SUCCEEDED:
return state
.update('entriesToDelete', () => List([]))
.update('count', () => action.data[0].count)
.update('records', () => List(action.data[1]));
.updateIn(['count', state.get('currentModel')], () => action.data[0].count)
.update('isLoading', () => false)
.updateIn(['records', state.get('currentModel')], () => List(action.data[1]))
.update('updatingParams', () => false);
case ON_CHANGE:
return state.updateIn(['appliedFilters', action.index, action.key], () => action.value);
case ON_CLICK_REMOVE:
@ -109,7 +126,7 @@ function listPageReducer(state = initialState, action) {
return state.update('entriesToDelete', () => {
if (state.get('entriesToDelete').size === 0) {
return state
.get('records')
.getIn(['records', state.get('currentModel')])
.reduce((acc, current) => acc.concat(List([toString(current.id)])), List([]));
}

View File

@ -99,6 +99,7 @@ export function* dataDelete({ id, modelName, source }) {
export function* dataDeleteAll({ entriesToDelete, model, source }) {
try {
const params = Object.assign(entriesToDelete, source !== undefined ? { source } : {});
yield call(request, `/content-manager/explorer/deleteAll/${model}`, {
method: 'DELETE',
params,

View File

@ -5,7 +5,7 @@ export default function checkAttributeValidations(errors) {
const attributeIndex = split(this.props.hash, '::')[3];
const sameAttributes = filter(this.props.contentTypeData.attributes, (attr) => attr.name === this.props.modifiedDataAttribute.name);
const sameParamsKey = filter(this.props.contentTypeData.attributes, (attr) =>
attr.params.key === this.props.modifiedDataAttribute.params.key && attr.params.target === this.props.modifiedDataAttribute.params.target);
attr.params.key !== '-' && attr.params.key === this.props.modifiedDataAttribute.params.key && attr.params.target === this.props.modifiedDataAttribute.params.target);
const sameParamsKeyAndName = filter(this.props.contentTypeData.attributes, (attr) => attr.name === this.props.modifiedDataAttribute.params.key);
const formErrors = concat(errors, hasNestedValue(this.props.modifiedDataAttribute));
const isEditingParamsKey = this.props.modifiedDataAttribute.params.key !== get(this.props.contentTypeData.attributes, [attributeIndex, 'params', 'key']);

View File

@ -7,7 +7,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import cn from 'classnames';
import LoadingIndicator from 'components/LoadingIndicator';
import Input from 'components/InputsIndex';
import styles from './styles.scss';
@ -25,6 +27,14 @@ class EditForm extends React.Component { // eslint-disable-line react/prefer-sta
)
render() {
if (this.props.showLoaders) {
return (
<div className={cn(styles.editForm, this.props.showLoaders && styles.loadIndicatorContainer)}>
<LoadingIndicator />
</div>
);
}
return (
<div className={styles.editForm}>
<div className="row">
@ -94,6 +104,7 @@ class EditForm extends React.Component { // eslint-disable-line react/prefer-sta
EditForm.propTypes = {
onChange: PropTypes.func.isRequired,
showLoaders: PropTypes.bool.isRequired,
values: PropTypes.object.isRequired,
};

View File

@ -13,3 +13,11 @@
margin-bottom: 2.1rem;
background: #F6F6F6;
}
.loadIndicatorContainer {
display: flex;
flex-direction: column;
justify-content: space-around;
min-height: 209px;
padding-top: 88px;
}

View File

@ -8,6 +8,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { map, omitBy, size } from 'lodash';
import cn from 'classnames';
// Components from strapi-helper-plugin
import LoadingBar from 'components/LoadingBar';
import LoadingIndicator from 'components/LoadingIndicator';
// Design
import Button from 'components/Button';
@ -48,14 +53,14 @@ const generateListTitle = (data, settingType) => {
}
};
function List({ data, deleteData, noButton, onButtonClick, settingType, values }) {
function List({ data, deleteData, noButton, onButtonClick, settingType, showLoaders, values }) {
const object = omitBy(data, (v) => v.name === 'server'); // Remove the server key when displaying providers
return (
<div className={styles.list}>
<div className={styles.flex}>
<div className={styles.titleContainer}>
{generateListTitle(data, settingType)}
{showLoaders ? <LoadingBar style={{ marginTop: '0' }} /> : generateListTitle(data, settingType)}
</div>
<div className={styles.buttonContainer}>
{noButton ? (
@ -67,18 +72,20 @@ function List({ data, deleteData, noButton, onButtonClick, settingType, values }
)}
</div>
</div>
<div className={styles.ulContainer}>
<ul className={noButton ? styles.listPadded : ''}>
{map(object, item => (
<ListRow
deleteData={deleteData}
item={item}
key={item.name}
settingType={settingType}
values={values}
/>
))}
</ul>
<div className={cn(styles.ulContainer, showLoaders && styles.loadingContainer, showLoaders && settingType === 'roles' && styles.loadingContainerRole )}>
{showLoaders ? <LoadingIndicator /> : (
<ul className={noButton ? styles.listPadded : ''}>
{map(object, item => (
<ListRow
deleteData={deleteData}
item={item}
key={item.name}
settingType={settingType}
values={values}
/>
))}
</ul>
)}
</div>
</div>
);
@ -87,6 +94,7 @@ function List({ data, deleteData, noButton, onButtonClick, settingType, values }
List.defaultProps = {
noButton: false,
onButtonClick: () => {},
showLoaders: true,
};
List.propTypes = {
@ -95,6 +103,7 @@ List.propTypes = {
noButton: PropTypes.bool,
onButtonClick: PropTypes.func,
settingType: PropTypes.string.isRequired,
showLoaders: PropTypes.bool,
values: PropTypes.object.isRequired,
};

View File

@ -11,6 +11,8 @@
}
.titleContainer {
flex: 2;
width: 20%;
color: #333740;
font-family: Lato;
font-size: 1.8rem;
@ -33,3 +35,16 @@
.listPadded {
padding-top: 3px !important;
}
.loadingContainer {
display: flex;
flex-direction: column;
justify-content: space-around;
min-height: 162px;
padding-top: 20px;
}
.loadingContainerRole {
min-height: 142px !important;
padding-top: 0;
}

View File

@ -46,7 +46,7 @@ class Plugin extends React.Component { // eslint-disable-line react/prefer-state
render() {
const divStyle = this.state.collapse ? { marginBottom: '.4rem' } : {};
const icon = get(this.context.plugins.toJS(), [this.props.name, 'icon']);
const icon = get(this.props.plugin, ['information', 'logo']);
const emptyApplication = !isEmpty(get(this.props.plugin, 'controllers'));
if (!emptyApplication) {
@ -57,11 +57,11 @@ class Plugin extends React.Component { // eslint-disable-line react/prefer-state
<div className={styles.plugin} style={divStyle}>
<div className={styles.banner} onClick={this.handleClick}>
<div>
{ icon ? (
{this.props.name !== 'application' && (
<div className={styles.iconContainer}>
<img src={this.props.plugin.information.logo} alt="icon" />
{icon && <img src={icon} alt="icon" />}
</div>
) : ''}
)}
<div className={styles.name}>{this.props.name}</div>
&nbsp;&nbsp;
<div className={styles.description}>

View File

@ -104,7 +104,7 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
<Input
inputDescription={{ id: 'users-permissions.PopUpForm.Providers.enabled.description' }}
label={{ id: 'users-permissions.PopUpForm.Providers.enabled.label' }}
name={`${dataToEdit}.enabled`}
name={`${settingType}.${dataToEdit}.enabled`}
onChange={this.handleChange}
type="toggle"
validations={{}}
@ -121,7 +121,7 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
errors={get(this.props.formErrors, [findIndex(this.props.formErrors, ['name', value]), 'errors'], [])}
key={value}
label={{ id: `users-permissions.PopUpForm.Providers.${ includes(value, 'callback') || includes(value, 'redirect_uri') ? 'redirectURL.front-end' : value}.label` }}
name={`${dataToEdit}.${value}`}
name={`${settingType}.${dataToEdit}.${value}`}
onFocus={includes(value, 'callback') || includes(value, 'redirect_uri') ? this.handleFocus : () => {}}
onBlur={includes(value, 'callback') || includes(value, 'redirect_uri') ? this.handleBlur : false}
onChange={this.props.onChange}
@ -163,7 +163,7 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
didCheckErrors={this.props.didCheckErrors}
errors={get(this.props.formErrors, [findIndex(this.props.formErrors, ['name', value]), 'errors'], [])}
label={{ id: `users-permissions.PopUpForm.Email.${value}.label` }}
name={`${dataToEdit}.${value}`}
name={`${settingType}.${dataToEdit}.${value}`}
onChange={this.props.onChange}
placeholder={`users-permissions.PopUpForm.Email.${value}.placeholder`}
type={includes(value, 'email') ? 'email' : 'text'}
@ -179,7 +179,7 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
didCheckErrors={this.props.didCheckErrors}
errors={get(this.props.formErrors, [findIndex(this.props.formErrors, ['name', value]), 'errors'], [])}
label={{ id: `users-permissions.PopUpForm.Email.${value}.label` }}
name={`${dataToEdit}.${value}`}
name={`${settingType}.${dataToEdit}.${value}`}
inputDescription={{
id: includes(value, 'object') ? 'users-permissions.PopUpForm.Email.email_templates.inputDescription' : '',
params,

View File

@ -20,6 +20,7 @@ import {
ON_CHANGE_INPUT,
ON_CLICK_ADD,
ON_CLICK_DELETE,
RESET_PROPS,
RESET_SHOULD_DISPLAY_POLICIES_HINT,
SELECT_ALL_ACTIONS,
SET_ACTION_TYPE,
@ -147,6 +148,10 @@ export function onClickDelete(itemToDelete) {
};
}
export const resetProps = () => ({
type: RESET_PROPS,
});
export function resetShouldDisplayPoliciesHint() {
return {
type: RESET_SHOULD_DISPLAY_POLICIES_HINT,

View File

@ -18,6 +18,7 @@ export const ON_CANCEL = 'UsersPermissions/EditPage/ON_CANCEL';
export const ON_CHANGE_INPUT = 'UsersPermissions/EditPage/ON_CHANGE_INPUT';
export const ON_CLICK_ADD = 'UsersPermissions/EditPage/ON_CLICK_ADD';
export const ON_CLICK_DELETE = 'UsersPermissions/EditPage/ON_CLICK_DELETE';
export const RESET_PROPS = 'UsersPermissions/EditPage/RESET_PROPS';
export const RESET_SHOULD_DISPLAY_POLICIES_HINT = 'UsersPermissions/EditPage/RESET_SHOULD_DISPLAY_POLICIES_HINT';
export const SELECT_ALL_ACTIONS = 'UsersPermissions/EditPage/SELECT_ALL_ACTIONS';
export const SET_ACTION_TYPE = 'UsersPermissions/EditPage/SET_ACTION_TYPE';

View File

@ -17,6 +17,8 @@ import cn from 'classnames';
import BackHeader from 'components/BackHeader';
import Input from 'components/InputsIndex';
import InputSearch from 'components/InputSearchContainer';
import LoadingIndicator from 'components/LoadingIndicator';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import PluginHeader from 'components/PluginHeader';
import Plugins from 'components/Plugins';
import Policies from 'components/Policies';
@ -43,6 +45,7 @@ import {
setRoleId,
setShouldDisplayPolicieshint,
submit,
resetProps,
resetShouldDisplayPoliciesHint,
} from './actions';
@ -90,6 +93,8 @@ export class EditPage extends React.Component { // eslint-disable-line react/pre
componentWillUnmount() {
// Empty formErrors
this.props.setErrors([]);
// Empty modifiedData so prev values aren't displayed when loading
this.props.resetProps();
this.props.resetShouldDisplayPoliciesHint();
}
@ -102,6 +107,73 @@ export class EditPage extends React.Component { // eslint-disable-line react/pre
this.props.submit();
}
showLoaderForm = () => {
const { editPage: { modifiedData }, match: { params: { actionType } } } = this.props;
return actionType !== 'create' && get(modifiedData, ['name'], '') === '';
}
showLoaderPermissions = () => {
const { editPage: { modifiedData } } = this.props;
return isEmpty(get(modifiedData, ['permissions']));
}
renderFirstBlock = () => (
<React.Fragment>
<div className="col-md-6">
<div className="row">
<Input
autoFocus
customBootstrapClass="col-md-12"
errors={get(this.props.editPage, ['formErrors', findIndex(this.props.editPage.formErrors, ['name', 'name']), 'errors'])}
didCheckErrors={this.props.editPage.didCheckErrors}
label={{ id: 'users-permissions.EditPage.form.roles.label.name' }}
name="name"
onChange={this.props.onChangeInput}
type="text"
validations={{ required: true }}
value={get(this.props.editPage, ['modifiedData', 'name'])}
/>
</div>
<div className="row">
<Input
customBootstrapClass="col-md-12"
label={{ id: 'users-permissions.EditPage.form.roles.label.description' }}
name="description"
onChange={this.props.onChangeInput}
type="textarea"
validations={{ required: true }}
value={get(this.props.editPage, ['modifiedData', 'description'])}
/>
</div>
</div>
<InputSearch
addUser={this.props.addUser}
didDeleteUser={this.props.editPage.didDeleteUser}
didFetchUsers={this.props.editPage.didFetchUsers}
didGetUsers={this.props.editPage.didGetUsers}
getUser={this.props.getUser}
label={{
id: 'users-permissions.EditPage.form.roles.label.users',
params: {
number: size(get(this.props.editPage, ['modifiedData', 'users'])),
},
}}
onClickAdd={this.props.onClickAdd}
onClickDelete={this.props.onClickDelete}
name="users"
type="text"
users={get(this.props.editPage, 'users')}
validations={{ required: true }}
values={get(this.props.editPage, ['modifiedData', 'users'])}
/>
<div className="col-md-12">
<div className={styles.separator} />
</div>
</React.Fragment>
)
render() {
const pluginHeaderTitle = this.props.match.params.actionType === 'create' ?
'users-permissions.EditPage.header.title.create'
@ -124,6 +196,10 @@ export class EditPage extends React.Component { // eslint-disable-line react/pre
disabled: isEqual(this.props.editPage.modifiedData, this.props.editPage.initialData),
},
];
if (this.showLoaderForm()) {
return <LoadingIndicatorPage />;
}
return (
<div>
@ -152,61 +228,21 @@ export class EditPage extends React.Component { // eslint-disable-line react/pre
</div>
<form className={styles.form}>
<div className="row">
<div className="col-md-6">
<div className="row">
<Input
autoFocus
customBootstrapClass="col-md-12"
errors={get(this.props.editPage, ['formErrors', findIndex(this.props.editPage.formErrors, ['name', 'name']), 'errors'])}
didCheckErrors={this.props.editPage.didCheckErrors}
label={{ id: 'users-permissions.EditPage.form.roles.label.name' }}
name="name"
onChange={this.props.onChangeInput}
type="text"
validations={{ required: true }}
value={get(this.props.editPage, ['modifiedData', 'name'])}
/>
</div>
<div className="row">
<Input
customBootstrapClass="col-md-12"
label={{ id: 'users-permissions.EditPage.form.roles.label.description' }}
name="description"
onChange={this.props.onChangeInput}
type="textarea"
validations={{ required: true }}
value={get(this.props.editPage, ['modifiedData', 'description'])}
/>
</div>
</div>
<InputSearch
addUser={this.props.addUser}
didDeleteUser={this.props.editPage.didDeleteUser}
didFetchUsers={this.props.editPage.didFetchUsers}
didGetUsers={this.props.editPage.didGetUsers}
getUser={this.props.getUser}
label={{
id: 'users-permissions.EditPage.form.roles.label.users',
params: {
number: size(get(this.props.editPage, ['modifiedData', 'users'])),
},
}}
onClickAdd={this.props.onClickAdd}
onClickDelete={this.props.onClickDelete}
name="users"
type="text"
users={get(this.props.editPage, 'users')}
validations={{ required: true }}
values={get(this.props.editPage, ['modifiedData', 'users'])}
/>
<div className="col-md-12">
<div className={styles.separator} />
</div>
{this.showLoaderForm() ? (
<div className={styles.loaderWrapper}><LoadingIndicator /></div>
) : this.renderFirstBlock()}
</div>
<div className="row" style={{ marginRight: '-30px'}}>
<Plugins
plugins={get(this.props.editPage, ['modifiedData', 'permissions'])}
/>
{this.showLoaderPermissions() && (
<div className={styles.loaderWrapper} style={{ minHeight: '400px' }}>
<LoadingIndicator />
</div>
)}
{!this.showLoaderPermissions() && (
<Plugins
plugins={get(this.props.editPage, ['modifiedData', 'permissions'])}
/>
)}
<Policies
shouldDisplayPoliciesHint={this.props.editPage.shouldDisplayPoliciesHint}
inputSelectName={this.props.editPage.inputPoliciesPath}
@ -246,6 +282,7 @@ EditPage.propTypes = {
onChangeInput: PropTypes.func.isRequired,
onClickAdd: PropTypes.func.isRequired,
onClickDelete: PropTypes.func.isRequired,
resetProps: PropTypes.func.isRequired,
resetShouldDisplayPoliciesHint: PropTypes.func.isRequired,
selectAllActions: PropTypes.func.isRequired,
setActionType: PropTypes.func.isRequired,
@ -281,6 +318,7 @@ function mapDispatchToProps(dispatch) {
setRoleId,
setShouldDisplayPolicieshint,
submit,
resetProps,
resetShouldDisplayPoliciesHint,
},
dispatch,

View File

@ -17,6 +17,7 @@ import {
ON_CHANGE_INPUT,
ON_CLICK_ADD,
ON_CLICK_DELETE,
RESET_PROPS,
RESET_SHOULD_DISPLAY_POLICIES_HINT,
SELECT_ALL_ACTIONS,
SET_ACTION_TYPE,
@ -85,6 +86,11 @@ function editPageReducer(state = initialState, action) {
return state
.set('didDeleteUser', !state.get('didDeleteUser'))
.updateIn(['modifiedData', 'users'], list => list.filter(o => o[o.id ? 'id' : '_id'] !== action.itemToDelete[o.id ? 'id' : '_id']));
case RESET_PROPS:
return state
.updateIn(['modifiedData'], () => Map({}))
.update('initialData', () => Map({}))
.update('users', () => List([]));
case RESET_SHOULD_DISPLAY_POLICIES_HINT:
return state.set('shouldDisplayPoliciesHint', true);
case SELECT_ALL_ACTIONS: {

View File

@ -30,3 +30,11 @@
font-weight: bold;
line-height: 18px;
}
.loaderWrapper {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 260px;
margin: auto;
}

View File

@ -96,6 +96,8 @@ export class HomePage extends React.Component {
this.props.resetProps();
}
getEndPoint = () => this.props.match.params.settingType;
handleKeyBoardShortCut = (e) => {
if (includes(keyBoardShortCuts, e.keyCode)) {
const mapKey = clone(this.state.mapKey);
@ -122,8 +124,8 @@ export class HomePage extends React.Component {
handleSubmit = (e) => {
e.preventDefault();
const modifiedObject = get(this.props.modifiedData, this.props.dataToEdit);
const initObject = get(this.props.initialData, this.props.dataToEdit);
const modifiedObject = get(this.props.modifiedData, [this.getEndPoint(), this.props.dataToEdit]);
const initObject = get(this.props.initialData, [this.getEndPoint(), this.props.dataToEdit]);
const formErrors = checkFormValidity(this.props.match.params.settingType, modifiedObject, this.props.dataToEdit);
if (isEqual(initObject, modifiedObject)) {
@ -153,23 +155,31 @@ export class HomePage extends React.Component {
},
];
showLoaders = () => {
const { data, isLoading, modifiedData } = this.props;
const isAdvanded = this.getEndPoint() === 'advanced';
return isLoading && get(data, this.getEndPoint()) === undefined && !isAdvanded || isLoading && isAdvanded && get(modifiedData, this.getEndPoint()) === undefined;
}
render() {
const { didCheckErrors, formErrors, modifiedData, initialData, match, dataToEdit } = this.props;
const { data, didCheckErrors, formErrors, modifiedData, initialData, match, dataToEdit } = this.props;
const headerActions = match.params.settingType === 'advanced' && !isEqual(modifiedData, initialData) ?
this.pluginHeaderActions : [];
const noButtonList = match.params.settingType === 'email-templates' || match.params.settingType === 'providers';
const component = match.params.settingType === 'advanced' ?
<EditForm onChange={this.props.onChange} values={modifiedData} /> : (
<EditForm onChange={this.props.onChange} values={get(modifiedData, this.getEndPoint(), {})} showLoaders={this.showLoaders()} /> : (
<List
data={this.props.data}
data={get(data, this.getEndPoint(), [])}
deleteData={this.props.deleteData}
noButton={noButtonList}
onButtonClick={this.handleButtonClick}
settingType={match.params.settingType}
values={modifiedData}
showLoaders={this.showLoaders()}
values={get(modifiedData, this.getEndPoint(), {})}
/>
);
return (
<div>
<form onSubmit={(e) => e.preventDefault()}>
@ -191,7 +201,7 @@ export class HomePage extends React.Component {
onChange={this.props.onChange}
onSubmit={this.handleSubmit}
settingType={match.params.settingType}
values={modifiedData[dataToEdit] || {}}
values={get(modifiedData,[this.getEndPoint(), dataToEdit], {})}
/>
</form>
</div>
@ -208,7 +218,7 @@ HomePage.defaultProps = {};
HomePage.propTypes = {
cancelChanges: PropTypes.func.isRequired,
data: PropTypes.array.isRequired,
data: PropTypes.object.isRequired,
dataToEdit: PropTypes.string.isRequired,
deleteData: PropTypes.func.isRequired,
didCheckErrors: PropTypes.bool.isRequired,
@ -217,6 +227,7 @@ HomePage.propTypes = {
formErrors: PropTypes.array.isRequired,
history: PropTypes.object.isRequired,
initialData: PropTypes.object.isRequired,
isLoading: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
modifiedData: PropTypes.object.isRequired,

View File

@ -10,6 +10,7 @@ import {
CANCEL_CHANGES,
DELETE_DATA,
DELETE_DATA_SUCCEEDED,
FETCH_DATA,
FETCH_DATA_SUCCEEDED,
ON_CHANGE,
RESET_PROPS,
@ -21,16 +22,18 @@ import {
} from './constants';
const initialState = fromJS({
data: List([]),
data: fromJS({}),
dataToDelete: Map({}),
dataToEdit: '',
deleteEndPoint: '',
didCheckErrors: false,
formErrors: List([]),
initialData: Map({}),
isLoading: true,
modifiedData: Map({}),
showButtons: false,
didDeleteData: false,
endPoint: 'roles',
});
function homePageReducer(state = initialState, action) {
@ -45,27 +48,36 @@ function homePageReducer(state = initialState, action) {
.set('deleteEndPoint', action.deleteEndPoint);
case DELETE_DATA_SUCCEEDED:
return state
.update('data', list => list.splice(action.indexDataToDelete, 1))
.updateIn(['data', state.get('endPoint')], list => list.splice(action.indexDataToDelete, 1))
.set('deleteEndPoint', '')
.set('dataToDelete', Map({}))
.update('didDeleteData', (v) => !v);
case FETCH_DATA:
return state
.update('endPoint', () => action.endPoint)
.update('isLoading', () => true);
case FETCH_DATA_SUCCEEDED:
return state
.set('data', List(action.data))
.set('initialData', action.modifiedData)
.set('modifiedData', action.modifiedData);
.updateIn(['data', state.get('endPoint')], () => List(action.data))
.updateIn(['initialData', state.get('endPoint')], () => action.modifiedData)
.update('isLoading', () => false)
.updateIn(['modifiedData', state.get('endPoint')], () => action.modifiedData);
case ON_CHANGE:
return state
.updateIn(action.keys, () => action.value);
case RESET_PROPS:
return initialState;
return initialState
.update('data', () => state.get('data'))
.update('initialData', () => state.get('initialData'))
.update('modifiedData', () => state.get('modifiedData'))
.update('endPoint', () => 'roles');
case SET_DATA_TO_EDIT:
return state.update('dataToEdit', () => action.dataToEdit);
case SET_FORM:
return state
.set('formErrors', List([]))
.set('initialData', action.form)
.set('modifiedData', action.form);
.updateIn(['initialData', state.get('endPoint')], () => action.form)
.updateIn(['modifiedData', state.get('endPoint')], () => action.form);
case SET_FORM_ERRORS:
return state
.update('didCheckErrors', (v) => v = !v)

View File

@ -17,9 +17,6 @@ import {
SUBMIT,
} from './constants';
// TODO uncomment to test design providers and so on...
// import data from './data.json';
import {
makeSelectAllData,
makeSelectDataToDelete,
@ -31,8 +28,8 @@ export function* dataDelete() {
try {
const allData = yield select(makeSelectAllData());
const dataToDelete = yield select(makeSelectDataToDelete());
const indexDataToDelete = findIndex(allData, ['name', dataToDelete.name]);
const endPointAPI = yield select(makeSelectDeleteEndPoint());
const indexDataToDelete = findIndex(allData[endPointAPI], ['name', dataToDelete.name]);
if (indexDataToDelete !== -1) {
const id = dataToDelete.id;