Update recursively schema on change

This commit is contained in:
cyril lopez 2018-06-28 17:54:24 +02:00
parent c0c99d443a
commit cc49c29f99
20 changed files with 403 additions and 24 deletions

View File

@ -7,7 +7,7 @@ const bootstrap = (plugin) => new Promise((resolve, reject) => {
.then(models => {
const menu = [{
name: 'Content Types',
links: map(omit(models.models, 'plugins'), (model, key) => ({
links: map(omit(models.models.models, 'plugins'), (model, key) => ({
label: model.labelPlural || model.label || key,
destination: key,
})),

View File

@ -0,0 +1,39 @@
/**
*
* Block
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
const Block = ({ children, description, title }) => (
<div className="col-md-12">
<div className={styles.ctmBlock}>
<div className={styles.ctmBlockTitle}>
<FormattedMessage id={title} />
<FormattedMessage id={description}>
{msg => <p>{msg}</p>}
</FormattedMessage>
</div>
{children}
</div>
</div>
);
Block.defaultProps = {
children: null,
description: 'app.utils.defaultMessage',
title: 'app.utils.defaultMessage',
};
Block.propTypes = {
children: PropTypes.any,
description: PropTypes.string,
title: PropTypes.string,
};
export default Block;

View File

@ -0,0 +1,21 @@
.ctmBlock{
background: #ffffff;
padding: 22px 28px 0px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
webkit-font-smoothing: antialiased;
}
.ctmBlockTitle {
padding-top: 0px;
line-height: 18px;
font-weight: 400;
> span {
color: #333740;
font-size: 18px;
}
> p {
color: #787E8F;
font-size: 13px;
}
}

View File

@ -4,12 +4,14 @@
*
*/
import { includes } from 'lodash';
import {
EMPTY_STORE,
GET_MODEL_ENTRIES,
GET_MODEL_ENTRIES_SUCCEEDED,
LOAD_MODELS,
LOADED_MODELS,
ON_CHANGE,
} from './constants';
export function emptyStore() {
@ -45,3 +47,13 @@ export function loadedModels(models) {
models,
};
}
export function onChange({ target }) {
const value = includes(target.name, 'pageEntries') ? parseInt(target.value, 10) : target.value;
return {
type: ON_CHANGE,
keys: target.name.split('.'),
value,
};
}

View File

@ -9,3 +9,4 @@ export const GET_MODEL_ENTRIES = 'contentManager/App/GET_MODEL_ENTRIES';
export const GET_MODEL_ENTRIES_SUCCEEDED = 'contentManager/App/GET_MODEL_ENTRIES_SUCCEEDED';
export const LOAD_MODELS = 'contentManager/App/LOAD_MODELS';
export const LOADED_MODELS = 'contentManager/App/LOADED_MODELS';
export const ON_CHANGE = 'contentManager/App/ON_CHANGE';

View File

@ -16,9 +16,9 @@ import { Switch, Route } from 'react-router-dom';
import injectSaga from 'utils/injectSaga';
import getQueryParameters from 'utils/getQueryParameters';
import Home from 'containers/Home';
import EditPage from 'containers/EditPage';
import ListPage from 'containers/ListPage';
import SettingsPage from 'containers/SettingsPage';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import EmptyAttributesView from 'components/EmptyAttributesView';
@ -41,7 +41,7 @@ class App extends React.Component {
const currentModelName = this.props.location.pathname.split('/')[3];
const source = getQueryParameters(this.props.location.search, 'source');
const attrPath = source === 'content-manager' ? [currentModelName, 'fields'] : ['plugins', source, currentModelName, 'fields'];
const attrPath = source === 'content-manager' ? ['models', currentModelName, 'fields'] : ['models', 'plugins', source, currentModelName, 'fields'];
if (currentModelName && source && isEmpty(get(this.props.schema, attrPath))) {
return <EmptyAttributesView currentModelName={currentModelName} history={this.props.history} modelEntries={this.props.modelEntries} />;
@ -50,7 +50,7 @@ class App extends React.Component {
return (
<div className="content-manager">
<Switch>
<Route path="/plugins/content-manager/ctm-configurations" component={Home} />
<Route path="/plugins/content-manager/ctm-configurations" component={SettingsPage} />
<Route path="/plugins/content-manager/:slug/:id" component={EditPage} />
<Route path="/plugins/content-manager/:slug" component={ListPage} />
</Switch>

View File

@ -5,8 +5,13 @@
*/
import { fromJS, List } from 'immutable';
import { EMPTY_STORE, GET_MODEL_ENTRIES_SUCCEEDED, LOAD_MODELS, LOADED_MODELS } from './constants';
import {
EMPTY_STORE,
GET_MODEL_ENTRIES_SUCCEEDED,
LOAD_MODELS,
LOADED_MODELS,
ON_CHANGE,
} from './constants';
const initialState = fromJS({
modelEntries: 0,
@ -27,6 +32,32 @@ function appReducer(state = initialState, action) {
return state
.update('schema', () => fromJS(action.models.models))
.set('loading', false);
case ON_CHANGE:
return state
.updateIn(['schema'].concat(action.keys), () => action.value)
.updateIn(['schema', 'models'], models => {
return models
.keySeq()
.reduce((acc, current) => {
if (current !== 'plugins') {
return acc.setIn([current, action.keys[1]], action.value);
}
return acc
.get(current)
.keySeq()
.reduce((acc1, curr) => {
return acc1
.getIn([current, curr])
.keySeq()
.reduce((acc2, curr1) => {
return acc2.setIn([ current, curr, curr1, action.keys[1]], action.value);
}, acc1);
}, acc);
}, models);
});
default:
return state;
}

View File

@ -100,11 +100,6 @@ export class EditPage extends React.Component {
}
}
componentDidCatch(error, info) {
console.log('err', err);
console.log('info', info);
}
componentWillUnmount() {
this.props.resetProps();
}
@ -128,7 +123,7 @@ export class EditPage extends React.Component {
* Retrieve the model
* @type {Object}
*/
getModel = () => get(this.props.schema, [this.getModelName()]) || get(this.props.schema, ['plugins', this.getSource(), this.getModelName()]);
getModel = () => get(this.props.schema, ['models', this.getModelName()]) || get(this.props.schema, ['models', 'plugins', this.getSource(), this.getModelName()]);
/**
* Retrieve specific attribute
@ -153,8 +148,8 @@ export class EditPage extends React.Component {
* @return {Object}
*/
getSchema = () => this.getSource() !== 'content-manager' ?
get(this.props.schema, ['plugins', this.getSource(), this.getModelName()])
: get(this.props.schema, [this.getModelName()]);
get(this.props.schema, ['models', 'plugins', this.getSource(), this.getModelName()])
: get(this.props.schema, ['models', this.getModelName()]);
getPluginHeaderTitle = () => {
if (this.isCreating()) {

View File

@ -107,8 +107,8 @@ export class ListPage extends React.Component {
* @return {Object} the current model
*/
getCurrentModel = () => (
get(this.props.schema, [this.getCurrentModelName()]) ||
get(this.props.schema, ['plugins', this.getSource(), this.getCurrentModelName()])
get(this.props.schema, ['models', this.getCurrentModelName()]) ||
get(this.props.schema, ['models', 'plugins', this.getSource(), this.getCurrentModelName()])
);
/**

View File

@ -0,0 +1,42 @@
{
"inputs": [
{
"label": { "id": "content-manager.form.Input.search" },
"customBootstrapClass": "col-md-5",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.search",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.filters" },
"customBootstrapClass": "col-md-5",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.filters",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.bulkActions" },
"customBootstrapClass": "col-md-2",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.bulkActions",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.pageEntries" },
"customBootstrapClass": "col-md-3",
"didCheckErrors": false,
"errors": [],
"inputDescription": { "id": "content-manager.form.Input.pageEntries.inputDescription" },
"name": "generalSettings.pageEntries",
"selectOptions": ["10", "20", "50", "100"],
"type": "select",
"validations": {}
}
]
}

View File

@ -0,0 +1,125 @@
/**
*
* SettingsPage
*/
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import cn from 'classnames';
import { get } from 'lodash';
import PropTypes from 'prop-types';
import { onChange } from 'containers/App/actions';
import { makeSelectSchema } from 'containers/App/selectors';
import PluginHeader from 'components/PluginHeader';
import Input from 'components/InputsIndex';
import Block from 'components/Block';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import reducer from './reducer';
import saga from './saga';
import makeSelectSettingsPage from './selectors';
import styles from './styles.scss';
import forms from './forms.json';
class SettingsPage extends React.PureComponent {
getPluginHeaderActions = () => (
[
{
label: 'content-manager.popUpWarning.button.cancel',
kind: 'secondary',
onClick: () => {},
type: 'button',
},
{
kind: 'primary',
label: 'content-manager.containers.Edit.submit',
onClick: () => {},
type: 'submit',
},
]
);
getValue = (input) => {
const { schema: { generalSettings } } = this.props;
const value = get(generalSettings, input.name.split('.')[1], input.type === 'toggle' ? false : 10);
return input.type === 'toggle' ? value : value.toString();
}
render() {
return (
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
actions={this.getPluginHeaderActions()}
title="Content Manager"
description={{ id: 'content-manager.containers.SettingsPage.pluginHeaderDescription' }}
/>
<div className={cn('row', styles.container)}>
<Block
description="content-manager.containers.SettingsPage.Block.generalSettings.description"
title="content-manager.containers.SettingsPage.Block.generalSettings.title"
>
<form onSubmit={(e) => e.preventDefault()} className={styles.ctmForm}>
<div className="row">
<div className="col-md-10">
<div className="row">
{forms.inputs.map(input => {
return (
<Input
key={input.name}
onChange={this.props.onChange}
value={this.getValue(input)}
{...input}
/>
);
})}
</div>
</div>
</div>
</form>
</Block>
</div>
</div>
);
}
}
SettingsPage.defaultProps = {};
SettingsPage.propTypes = {
onChange: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
};
const mapDispatchToProps = (dispatch) => (
bindActionCreators(
{
onChange,
},
dispatch,
)
);
const mapStateToProps = createStructuredSelector({
schema: makeSelectSchema(),
settingsPage: makeSelectSettingsPage(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'settingsPage', reducer });
const withSaga = injectSaga({ key: 'settingsPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(SettingsPage);

View File

@ -0,0 +1,19 @@
/**
*
* SettingsPage reducer
*/
import { fromJS } from 'immutable';
const initialState = fromJS({
foo: 'bar',
});
function settingsPageReducer(state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
export default settingsPageReducer;

View File

@ -0,0 +1,14 @@
// import { LOCATION_CHANGE } from 'react-router-redux'
// import {
// call,
// cancel,
// fork,
// put,
// select,
// take,
// takeLatest,
// } from 'redux-saga/effects';
function* defaultSaga() {}
export default defaultSaga;

View File

@ -0,0 +1,24 @@
/**
*
* SettingsPage selectors
*/
import { createSelector } from 'reselect';
/**
* Direct selector to the settingsPage state domain
*/
const selectSettingsPageDomain = () => state => state.get('settingsPage');
/**
* Default selector used by EditPage
*/
const makeSelectSettingsPage = () => createSelector(
selectSettingsPageDomain(),
(substate) => substate.toJS()
);
export default makeSelectSettingsPage;

View File

@ -0,0 +1,21 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
}
}
.container {
padding-top: 18px;
}
.main_wrapper{
background: #ffffff;
padding: 22px 28px 0px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
}
.ctmForm {
padding-top: 2.6rem;
}

View File

@ -14,6 +14,9 @@
"containers.List.pluginHeaderDescription.singular": "{label} entry found",
"components.LimitSelect.itemsPerPage": "Items per page",
"containers.List.errorFetchRecords": "Error",
"containers.SettingsPage.pluginHeaderDescription": "Configure the default settings for all your content types",
"containers.SettingsPage.Block.generalSettings.description": "Configure the default options for your content types",
"containers.SettingsPage.Block.generalSettings.title": "General",
"components.AddFilterCTA.add": "Filters",
"components.AddFilterCTA.hide": "Filters",
@ -71,6 +74,12 @@
"error.validation.minSupMax": "Can't be superior",
"error.validation.json": "This is not a JSON",
"form.Input.search": "Enable search",
"form.Input.filters": "Enable filters",
"form.Input.bulkActions": "Enable bulk actions",
"form.Input.pageEntries": "Entries per page",
"form.Input.pageEntries.inputDescription": "Note: You can override this value in the Content Type settings page.",
"notification.error.relationship.fetch": "An error occurred during relationship fetch.",
"success.record.delete": "Deleted",

View File

@ -15,6 +15,9 @@
"containers.List.pluginHeaderDescription.singular": "{label} entrée trouvée",
"components.LimitSelect.itemsPerPage": "Éléments par page",
"containers.List.errorFetchRecords": "Erreur",
"containers.SettingsPage.pluginHeaderDescription": "Configurez les paramètres par défaut de vos modèles",
"containers.SettingsPage.Block.generalSettings.description": "Configurez les options par défault de vos modèles",
"containers.SettingsPage.Block.generalSettings.title": "Général",
"components.AddFilterCTA.add": "Filtres",
"components.AddFilterCTA.hide": "Filtres",
@ -71,6 +74,12 @@
"error.validation.minSupMax": "Ne peut pas être plus grand",
"error.validation.json": "Le format JSON n'est pas respecté",
"form.Input.search": "Autoriser la search",
"form.Input.filters": "Autoriser les filtres",
"form.Input.bulkActions": "Autoriser les bulk actions",
"form.Input.pageEntries": "Nombre d'entrées par page",
"form.Input.pageEntries.inputDescription": "Note: Vous pouvez les modifier ces valeurs par modèle",
"notification.error.relationship.fetch": "Une erreur est survenue en récupérant les relations.",
"success.record.delete": "Supprimé",

View File

@ -27,7 +27,17 @@ module.exports = async cb => {
}, {});
// Init schema
const schema = { plugins: {} };
const schema = {
generalSettings: {
search: true,
filters: true,
bulkActions: true,
pageEntries: 10,
},
models: {
plugins: {},
},
};
const buildSchema = (model, name, plugin = false) => {
// Model data
@ -38,7 +48,7 @@ module.exports = async cb => {
search: true,
filters: true,
bulkActions: true,
pageEntries: 20,
pageEntries: 10,
defaultSort: 'id'
}, model);
@ -56,7 +66,12 @@ module.exports = async cb => {
schemaModel.listDisplay = Object.keys(schemaModel.fields)
// Construct Array of attr ex { type: 'string', label: 'Foo', name: 'Foo', description: '' }
// NOTE: Do we allow sort on boolean?
.map(attr => Object.assign(schemaModel.fields[attr], { name: attr, sortable: true, searchable: true }))
.map(attr => {
const attrType = schemaModel.fields[attr].type;
const sortable = attrType !== 'json' && attrType !== 'array';
return Object.assign(schemaModel.fields[attr], { name: attr, sortable, searchable: sortable });
})
// Retrieve only the fourth first items
.slice(0, 4);
@ -64,6 +79,8 @@ module.exports = async cb => {
name: model.primaryKey || 'id',
label: 'Id',
type: 'string',
sortable: true,
searchable: true,
});
if (model.associations) {
@ -88,11 +105,11 @@ module.exports = async cb => {
}
if (plugin) {
return _.set(schema.plugins, `${plugin}.${name}`, schemaModel);
return _.set(schema.models.plugins, `${plugin}.${name}`, schemaModel);
}
// Set the formatted model to the schema
schema[name] = schemaModel;
schema.models[name] = schemaModel;
};
_.forEach(pluginsModel, (plugin, pluginName) => {
@ -130,11 +147,11 @@ module.exports = async cb => {
if (!prevSchema) {
pluginStore.set({ key: 'schema', value: schema });
cb();
return cb();
}
const prevSchemaKeys = buildSchemaKeys(prevSchema);
const schemaKeys = buildSchemaKeys(schema);
const prevSchemaKeys = buildSchemaKeys(prevSchema.models);
const schemaKeys = buildSchemaKeys(schema.models);
// Update the store with the new created APIs
if (!_.isEqual(prevSchemaKeys, schemaKeys)) {