mirror of
https://github.com/strapi/strapi.git
synced 2025-10-22 13:35:53 +00:00
Update recursively schema on change
This commit is contained in:
parent
c0c99d443a
commit
cc49c29f99
@ -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,
|
||||
})),
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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()])
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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",
|
||||
|
@ -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é",
|
||||
|
@ -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)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user