Add queries system and schema based fields

This commit is contained in:
Pierre Burgy 2017-06-17 17:01:50 +02:00
parent b48ff5dec3
commit 43809dcb00
15 changed files with 281 additions and 88 deletions

View File

@ -250,6 +250,9 @@ module.exports = function(strapi) {
model: global[globalName]
});
// Expose ORM functions through the `strapi.models` object.
strapi.models[model] = _.assign(global[globalName], strapi.models[model]);
// Push model to strapi global variables.
strapi.bookshelf.collections[globalName.toLowerCase()] = global[
globalName

View File

@ -20,6 +20,7 @@ class EditForm extends React.Component {
generateField(attributeValue, attributeKey) {
let input;
const value = this.props.record && this.props.record.get(attributeKey);
// Generate fields according to attribute type
switch (attributeValue.type) {
@ -30,15 +31,14 @@ class EditForm extends React.Component {
onChange={e =>
this.props.setRecordAttribute(
attributeKey,
e.target.value === 'true'
e.target.value === 'null'
? null
: e.target.value === 'true'
)}
defaultValue={
this.props.record && this.props.record.get(attributeKey)
}
>
<option disabled>Select an option</option>
<option value>True</option>
<option value={false}>False</option>
<option value={'null'} selected={value !== true && value !== false}>Select an option</option>
<option value selected={value === true}>True</option>
<option value={false} selected={value === false}>False</option>
</select>
);
break;
@ -48,10 +48,8 @@ class EditForm extends React.Component {
type="number"
className="form-control"
id={attributeKey}
placeholder={attributeKey}
value={
(this.props.record && this.props.record.get(attributeKey)) || ''
}
placeholder={attributeValue.placeholder || attributeValue.label || attributeKey}
value={value}
onChange={e =>
this.props.setRecordAttribute(attributeKey, e.target.value)}
/>
@ -63,10 +61,21 @@ class EditForm extends React.Component {
type="text"
className="form-control"
id={attributeKey}
placeholder={attributeKey}
value={
(this.props.record && this.props.record.get(attributeKey)) || ''
}
placeholder={attributeValue.placeholder || attributeValue.label || attributeKey}
value={value}
onChange={e =>
this.props.setRecordAttribute(attributeKey, e.target.value)}
/>
);
break;
case 'url':
input = (
<input
type="url"
className="form-control"
id={attributeKey}
placeholder={attributeValue.placeholder || attributeValue.label || attributeKey}
value={value}
onChange={e =>
this.props.setRecordAttribute(attributeKey, e.target.value)}
/>
@ -78,19 +87,22 @@ class EditForm extends React.Component {
type="text"
className="form-control"
id={attributeKey}
placeholder={attributeKey}
value={
(this.props.record && this.props.record.get(attributeKey)) || ''
}
placeholder={attributeValue.placeholder || attributeValue.label || attributeKey}
value={value}
onChange={e =>
this.props.setRecordAttribute(attributeKey, e.target.value)}
/>
);
}
const description = attributeValue.description
? <p>{attributeValue.description}</p>
: '';
return (
<div key={attributeKey} className="form-group">
<label htmlFor={attributeKey}>{attributeKey}</label>
<label htmlFor={attributeKey}>{attributeValue.label || attributeKey}</label>
{description}
{input}
</div>
);
@ -98,7 +110,7 @@ class EditForm extends React.Component {
render() {
const fields = _.map(
this.props.currentModel.attributes,
this.props.schema[this.props.currentModelName].fields,
(attributeValue, attributeKey) =>
this.generateField(attributeValue, attributeKey)
);
@ -115,12 +127,13 @@ class EditForm extends React.Component {
}
EditForm.propTypes = {
currentModel: React.PropTypes.object.isRequired,
currentModelName: React.PropTypes.string.isRequired,
editRecord: React.PropTypes.func.isRequired,
record: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.bool,
]),
schema: React.PropTypes.object.isRequired,
setRecordAttribute: React.PropTypes.func.isRequired,
};

View File

@ -27,9 +27,9 @@ class TableRow extends React.Component {
getDisplayedValue(type, value) {
switch (type) {
case 'string':
return !_.isEmpty(value) ? value.toString() : '-';
return !_.isEmpty(value.toString()) ? value.toString() : '-';
case 'integer':
return !_.isEmpty(value) ? Number(value) : '-';
return !_.isEmpty(value.toString()) ? value.toString() : '-';
default:
return '-';
}

View File

@ -4,7 +4,7 @@
*
*/
import { LOAD_MODELS, LOADED_MODELS } from './constants';
import { LOAD_MODELS, LOADED_MODELS, UPDATE_SCHEMA } from './constants';
export function loadModels() {
return {
@ -18,3 +18,10 @@ export function loadedModels(models) {
models,
};
}
export function updateSchema(schema) {
return {
type: UPDATE_SCHEMA,
schema,
};
}

View File

@ -6,3 +6,4 @@
export const LOAD_MODELS = 'contentManager/App/LOAD_MODELS';
export const LOADED_MODELS = 'contentManager/App/LOADED_MODELS';
export const UPDATE_SCHEMA = 'contentManager/App/UPDATE_SCHEMA';

View File

@ -9,12 +9,14 @@ import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { loadModels } from './actions';
import config from '../../../../config/admin.json';
import { loadModels, updateSchema } from './actions';
import { makeSelectModels, makeSelectLoading } from './selectors';
class App extends React.Component {
componentWillMount() {
this.props.loadModels();
this.props.updateSchema(config.admin.schema);
}
render() {
@ -49,11 +51,13 @@ App.propTypes = {
React.PropTypes.object,
React.PropTypes.bool,
]),
updateSchema: React.PropTypes.func.isRequired,
};
export function mapDispatchToProps(dispatch) {
return {
loadModels: () => dispatch(loadModels()),
updateSchema: (schema) => dispatch(updateSchema(schema)),
dispatch,
};
}

View File

@ -6,11 +6,12 @@
import { fromJS } from 'immutable';
import { LOAD_MODELS, LOADED_MODELS } from './constants';
import { LOAD_MODELS, LOADED_MODELS, UPDATE_SCHEMA } from './constants';
const initialState = fromJS({
loading: false,
models: false,
schema: false,
});
function appReducer(state = initialState, action) {
@ -19,6 +20,8 @@ function appReducer(state = initialState, action) {
return state.set('loading', true);
case LOADED_MODELS:
return state.set('loading', false).set('models', action.models);
case UPDATE_SCHEMA:
return state.set('schema', action.schema);
default:
return state;
}

View File

@ -3,7 +3,7 @@ import { takeLatest } from 'redux-saga';
import { fork, put } from 'redux-saga/effects';
import { loadedModels } from './actions';
import { LOAD_MODELS } from './constants';
import { LOAD_MODELS, UPDATE_SCHEMA } from './constants';
export function* getModels() {
try {
@ -19,14 +19,6 @@ export function* getModels() {
const data = yield response.json();
yield put(loadedModels(data));
const leftMenuLinks = _.map(data, (model, key) => ({
label: model.globalId,
to: key,
}));
// Update the admin left menu links
window.Strapi.refresh('content-manager').leftMenuLinks(leftMenuLinks);
} catch (err) {
window.Strapi.notification.error(
'An error occurred during models config fetch.'
@ -34,10 +26,20 @@ export function* getModels() {
}
}
export function* schemaUpdated(action) {
const leftMenuLinks = _.map(action.schema, (model, key) => ({
label: model.plural || model.label || key,
to: key,
}));
// Update the admin left menu links
window.Strapi.refresh('content-manager').leftMenuLinks(leftMenuLinks);
}
// Individual exports for testing
export function* defaultSaga() {
// yield takeLatest(LOAD_MODELS, getModels);
yield fork(takeLatest, LOAD_MODELS, getModels);
yield fork(takeLatest, UPDATE_SCHEMA, schemaUpdated);
}
// All sagas to be loaded

View File

@ -37,9 +37,13 @@ const makeSelectModels = () =>
const makeSelectLoading = () =>
createSelector(selectGlobalDomain(), substate => substate.get('loading'));
const makeSelectSchema = () =>
createSelector(selectGlobalDomain(), substate => substate.get('schema'));
export {
selectGlobalDomain,
selectLocationState,
makeSelectLoading,
makeSelectModels,
makeSelectSchema,
};

View File

@ -5,13 +5,12 @@
*/
import React from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import Container from 'components/Container';
import EditForm from 'components/EditForm';
import { makeSelectModels } from 'containers/App/selectors';
import { makeSelectModels, makeSelectSchema } from 'containers/App/selectors';
import {
setCurrentModelName,
@ -49,11 +48,12 @@ export class Edit extends React.Component {
const PluginHeader = this.props.exposedComponents.PluginHeader;
let content = <p>Loading...</p>;
if (currentModel && currentModel.attributes) {
if (this.props.schema && currentModel && currentModel.attributes) {
content = (
<EditForm
record={this.props.record}
currentModel={currentModel}
currentModelName={this.props.currentModelName}
schema={this.props.schema}
setRecordAttribute={this.props.setRecordAttribute}
editRecord={this.props.editRecord}
editing={this.props.editing}
@ -86,8 +86,7 @@ export class Edit extends React.Component {
}
// Plugin header config
const pluginHeaderTitle =
_.upperFirst(this.props.routeParams.slug) || 'Content Manager';
const pluginHeaderTitle = this.props.schema[this.props.currentModelName].label || 'Content Manager';
const pluginHeaderDescription = this.props.isCreating
? 'New entry'
: `#${this.props.record.get('id')}`;
@ -141,6 +140,10 @@ Edit.propTypes = {
React.PropTypes.bool,
]),
routeParams: React.PropTypes.object.isRequired,
schema: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.bool,
]),
setCurrentModelName: React.PropTypes.func.isRequired,
setIsCreating: React.PropTypes.func.isRequired,
setRecordAttribute: React.PropTypes.func.isRequired,
@ -154,6 +157,7 @@ const mapStateToProps = createStructuredSelector({
editing: makeSelectEditing(),
deleting: makeSelectDeleting(),
isCreating: makeSelectIsCreating(),
schema: makeSelectSchema(),
});
function mapDispatchToProps(dispatch) {

View File

@ -10,7 +10,7 @@ import { createStructuredSelector } from 'reselect';
import { injectIntl } from 'react-intl';
import _ from 'lodash';
import { makeSelectModels } from 'containers/App/selectors';
import { makeSelectModels, makeSelectSchema } from 'containers/App/selectors';
import Container from 'components/Container';
import Table from 'components/Table';
import TableFooter from 'components/TableFooter';
@ -86,26 +86,13 @@ export class List extends React.Component {
// Detect current model structure from models list
const currentModel = this.props.models[this.props.currentModelName];
// Hide non displayed attributes
const displayedAttributes = _.pickBy(
currentModel.attributes,
attr => !attr.admin || attr.admin.displayed !== false
);
// Define table headers
const tableHeaders = _.map(displayedAttributes, (value, key) => ({
name: key,
label: key,
type: value.type,
const tableHeaders = _.map(this.props.schema[this.props.currentModelName].list, (value) => ({
name: value,
label: this.props.schema[this.props.currentModelName].fields[value].label,
type: this.props.schema[this.props.currentModelName].fields[value].type,
}));
// Add the primary key column
tableHeaders.unshift({
name: currentModel.primaryKey,
label: 'ID',
type: 'string',
});
content = (
<Table
records={this.props.records}
@ -130,9 +117,8 @@ export class List extends React.Component {
];
// Plugin header config
const pluginHeaderTitle =
_.upperFirst(this.props.currentModelNamePluralized) || 'Content Manager';
const pluginHeaderDescription = `Manage your ${this.props.currentModelNamePluralized}`;
const pluginHeaderTitle = this.props.schema[this.props.currentModelName].label || 'Content Manager';
const pluginHeaderDescription = `Manage your ${this.props.schema[this.props.currentModelName].labelPlural.toLowerCase()}`;
return (
<div>
@ -180,10 +166,6 @@ List.propTypes = {
React.PropTypes.string,
React.PropTypes.bool,
]),
currentModelNamePluralized: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.bool,
]),
currentPage: React.PropTypes.number.isRequired,
exposedComponents: React.PropTypes.object.isRequired,
history: React.PropTypes.object.isRequired,
@ -203,6 +185,10 @@ List.propTypes = {
]),
route: React.PropTypes.object.isRequired,
routeParams: React.PropTypes.object.isRequired,
schema: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.bool,
]),
setCurrentModelName: React.PropTypes.func.isRequired,
sort: React.PropTypes.string.isRequired,
};
@ -243,6 +229,7 @@ const mapStateToProps = createStructuredSelector({
sort: makeSelectSort(),
currentModelName: makeSelectCurrentModelName(),
currentModelNamePluralized: makeSelectCurrentModelNamePluralized(),
schema: makeSelectSchema(),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(List));

View File

@ -0,0 +1,64 @@
module.exports = {
find: async (params) => {
const entries = await params.model
.forge()
.query({
limit: Number(params.limit),
orderBy: params.sort,
offset: Number(params.skip),
})
.fetchAll();
return entries;
},
count: async (params) => {
const count = await params.model
.forge()
.count();
return Number(count);
},
findOne: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.forge(where)
.fetch();
return entry;
},
create: async (params) => {
const entry = await params.model
.forge()
.save(params.values);
return entry;
},
update: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.forge(where)
.save(params.values, {patch: true});
return entry;
},
delete: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.forge(where)
.destroy();
return entry;
}
};

View File

@ -0,0 +1,56 @@
module.exports = {
find: async (params) => {
const entries = params.model
.find()
.limit(Number(params.limit))
.sort(params.sort)
.skip(Number(params.skip));
return entries;
},
count: async (params) => {
const count = await params.model
.count();
return Number(count);
},
findOne: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.findOne(where);
return entry;
},
create: async (params) => {
const entry = await params.model
.create(params.values);
return entry;
},
update: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.update(where, params.values);
return entry;
},
delete: async (params) => {
const where = {};
where[params.primaryKey] = params.id;
const entry = await params.model
.destroy(where);
return entry;
}
};

View File

@ -23,67 +23,105 @@ module.exports = {
find: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const primaryKey = model.primaryKey;
const { limit = 10, skip = 0, sort = primaryKey } = ctx.request.query;
const entries = await model
.find()
.limit(Number(limit))
.sort(sort)
.skip(Number(skip));
// Find entries using `queries` system
const entries = await queries
.find({
model,
limit,
skip,
sort
});
ctx.body = entries;
},
count: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const count = await model.count();
// Count using `queries` system
const count = await queries.count({ model });
ctx.body = {
count: Number(count),
count,
};
},
findOne: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const primaryKey = model.primaryKey;
const params = {};
params[primaryKey] = ctx.params.id;
const id = ctx.params.id;
const entry = await model.findOne(params);
// Find an entry using `queries` system
const entry = await queries.findOne({
model,
primaryKey,
id
});
// Entry not found
if (!entry) {
return (ctx.notFound('Entry not found'));
}
ctx.body = entry;
},
create: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const values = ctx.request.body;
const entryCreated = await model.create(ctx.request.body);
// Create an entry using `queries` system
const entryCreated = await queries.create({
model,
values
});
ctx.body = entryCreated;
},
update: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const primaryKey = model.primaryKey;
const params = {};
const id = ctx.params.id;
const values = ctx.request.body;
params[primaryKey] = ctx.params.id;
const entryUpdated = await model.update(params, ctx.request.body);
// Update an entry using `queries` system
const entryUpdated = await queries.update({
model,
primaryKey,
id,
values
});
ctx.body = entryUpdated;
},
delete: async ctx => {
const model = strapi.models[ctx.params.model];
const orm = _.get(strapi.plugins, ['content-manager', 'config', 'admin', 'schema', ctx.params.model, 'orm']);
const queries = _.get(strapi.plugins, ['content-manager', 'config', 'queries', orm]);
const primaryKey = model.primaryKey;
const params = {};
params[primaryKey] = ctx.params.id;
const id = ctx.params.id;
const entryDeleted = await model.remove(params);
// Delete an entry using `queries` system
const entryDeleted = await queries.delete({
model,
primaryKey,
id
});
ctx.body = entryDeleted;
},

View File

@ -105,13 +105,20 @@ module.exports = strapi => {
filter: /(.+)\.(js|json)$/,
depth: 1
}, cb);
},
queries: cb => {
dictionary.optional({
dirname: path.resolve(strapi.config.appPath, strapi.config.paths.plugins, plugin, strapi.config.paths.config, 'queries'),
filter: /(.+)\.js$/,
depth: 2
}, cb);
}
}, (err, config) => {
if (err) {
return cb(err);
}
return cb(null, _.merge(config.common, config.specific));
return cb(null, _.merge(config.common, config.specific, { queries: config.queries}));
});
}
},