Merge branch 'plugin/content-manager-dev' of github.com:strapi/strapi into plugin/content-manager-dev

This commit is contained in:
Aurelsicoko 2017-09-19 13:57:17 +02:00
commit 4d2ec1f5b0
12 changed files with 351 additions and 101 deletions

View File

@ -6,6 +6,7 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { get, isEmpty, map, mapKeys, isObject, reject, includes } from 'lodash';
import { FormattedMessage } from 'react-intl';
import DateTime from 'react-datetime';
@ -67,37 +68,37 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
validate = (value) => {
let errors = [];
// handle i18n
const requiredError = { id: 'error.validation.required' };
const requiredError = { id: `${this.props.pluginID}.error.validation.required` };
mapKeys(this.props.validations, (validationValue, validationKey) => {
switch (validationKey) {
case 'max':
if (parseInt(value, 10) > validationValue) {
errors.push({ id: 'error.validation.max' });
errors.push({ id: `${this.props.pluginID}.error.validation.max` });
}
break;
case 'maxLength':
if (value.length > validationValue) {
errors.push({ id: 'error.validation.maxLength' });
errors.push({ id: `${this.props.pluginID}.error.validation.maxLength` });
}
break;
case 'min':
if (parseInt(value, 10) < validationValue) {
errors.push({ id: 'error.validation.min' });
errors.push({ id: `${this.props.pluginID}.error.validation.min` });
}
break;
case 'minLength':
if (value.length < validationValue) {
errors.push({ id: 'error.validation.minLength' });
errors.push({ id: `${this.props.pluginID}.error.validation.minLength` });
}
break;
case 'required':
if (value.length === 0) {
errors.push({ id: 'error.validation.required' });
errors.push({ id: `${this.props.pluginID}.error.validation.required` });
}
break;
case 'regex':
if (!new RegExp(validationValue).test(value)) {
errors.push({ id: 'error.validation.regex' });
errors.push({ id: `${this.props.pluginID}.error.validation.regex` });
}
break;
default:
@ -367,36 +368,37 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
}
Input.propTypes = {
addon: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.string,
addon: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
]),
addRequiredInputDesign: React.PropTypes.bool,
customBootstrapClass: React.PropTypes.string,
deactivateErrorHighlight: React.PropTypes.bool,
didCheckErrors: React.PropTypes.bool,
disabled: React.PropTypes.bool,
errors: React.PropTypes.array,
handleBlur: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.bool,
addRequiredInputDesign: PropTypes.bool,
customBootstrapClass: PropTypes.string,
deactivateErrorHighlight: PropTypes.bool,
didCheckErrors: PropTypes.bool,
disabled: PropTypes.bool,
errors: PropTypes.array,
handleBlur: PropTypes.oneOfType([
PropTypes.func,
PropTypes.bool,
]),
handleChange: React.PropTypes.func.isRequired,
handleFocus: React.PropTypes.func,
inputDescription: React.PropTypes.string,
label: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
noErrorsDescription: React.PropTypes.bool,
placeholder: React.PropTypes.string,
selectOptions: React.PropTypes.array,
selectOptionsFetchSucceeded: React.PropTypes.bool,
title: React.PropTypes.string,
type: React.PropTypes.string.isRequired,
validations: React.PropTypes.object.isRequired,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.bool,
React.PropTypes.number,
handleChange: PropTypes.func.isRequired,
handleFocus: PropTypes.func,
inputDescription: PropTypes.string,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
noErrorsDescription: PropTypes.bool,
placeholder: PropTypes.string,
pluginID: PropTypes.string,
selectOptions: PropTypes.array,
selectOptionsFetchSucceeded: PropTypes.bool,
title: PropTypes.string,
type: PropTypes.string.isRequired,
validations: PropTypes.object.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.number,
]),
};

View File

@ -7,7 +7,7 @@
// Dependencies.
import React from 'react';
import PropTypes from 'prop-types';
import { omit } from 'lodash';
import { findIndex, get, omit } from 'lodash';
// Components.
import Input from 'components/Input';
@ -43,6 +43,10 @@ class EditForm extends React.Component {
// List fields inputs
const fields = Object.keys(displayedFields).map(attr => {
const details = displayedFields[attr];
const errorIndex = findIndex(this.props.formErrors, ['name', attr]);
const errors = errorIndex !== -1 ? this.props.formErrors[errorIndex].errors : [];
const validationsIndex = findIndex(this.props.formValidations, ['name', attr]);
const validations = get(this.props.formValidations[validationsIndex], 'validations') || {};
return (
<Input
@ -53,7 +57,10 @@ class EditForm extends React.Component {
value={this.props.record.get(attr) || ''}
placeholder={details.placeholder || details.label || attr || ''}
handleChange={this.props.handleChange}
validations={{}}
validations={validations}
errors={errors}
didCheckErrors={this.props.didCheckErrors}
pluginID="content-manager"
/>
);
});
@ -70,6 +77,9 @@ class EditForm extends React.Component {
EditForm.propTypes = {
currentModelName: PropTypes.string.isRequired,
didCheckErrors: PropTypes.bool.isRequired,
formErrors: PropTypes.array.isRequired,
formValidations: PropTypes.array.isRequired,
handleChange: PropTypes.func.isRequired,
record: PropTypes.oneOfType([
PropTypes.object,

View File

@ -4,7 +4,7 @@
*
*/
import { fromJS } from 'immutable';
import { fromJS, List } from 'immutable';
import { LOAD_MODELS, LOADED_MODELS, UPDATE_SCHEMA } from './constants';
@ -12,6 +12,7 @@ const initialState = fromJS({
loading: true,
models: false,
schema: false,
formValidations: List(),
});
function appReducer(state = initialState, action) {

View File

@ -3,40 +3,46 @@
* Edit actions
*
*/
import { get } from 'lodash';
import { getValidationsFromForm } from '../../utils/formValidations';
import {
SET_INITIAL_STATE,
CANCEL_CHANGES,
DELETE_RECORD,
DELETE_RECORD_ERROR,
DELETE_RECORD_SUCCESS,
EDIT_RECORD,
EDIT_RECORD_ERROR,
EDIT_RECORD_SUCCESS,
SET_CURRENT_MODEL_NAME,
SET_IS_CREATING,
SET_INITIAL_STATE,
LOAD_RECORD,
LOAD_RECORD_SUCCESS,
SET_RECORD_ATTRIBUTE,
EDIT_RECORD,
EDIT_RECORD_SUCCESS,
EDIT_RECORD_ERROR,
DELETE_RECORD,
DELETE_RECORD_SUCCESS,
DELETE_RECORD_ERROR,
TOGGLE_NULL,
CANCEL_CHANGES,
SET_FORM_VALIDATIONS,
SET_FORM,
SET_FORM_ERRORS,
} from './constants';
export function setInitialState() {
export function cancelChanges() {
return {
type: SET_INITIAL_STATE,
type: CANCEL_CHANGES,
};
}
export function setCurrentModelName(currentModelName) {
export function deleteRecord(id, modelName) {
return {
type: SET_CURRENT_MODEL_NAME,
currentModelName,
type: DELETE_RECORD,
id,
modelName,
};
}
export function setIsCreating() {
export function editRecord() {
return {
type: SET_IS_CREATING,
type: EDIT_RECORD,
};
}
@ -47,24 +53,17 @@ export function loadRecord(id) {
};
}
export function recordLoaded(record) {
export function recordDeleted(id) {
return {
type: LOAD_RECORD_SUCCESS,
record,
type: DELETE_RECORD_SUCCESS,
id,
};
}
export function setRecordAttribute(key, value) {
export function recordDeleteError() {
return {
type: SET_RECORD_ATTRIBUTE,
key,
value,
};
}
export function editRecord() {
return {
type: EDIT_RECORD,
type: DELETE_RECORD_ERROR,
};
}
@ -80,24 +79,71 @@ export function recordEditError() {
};
}
export function deleteRecord(id, modelName) {
export function recordLoaded(record) {
return {
type: DELETE_RECORD,
id,
modelName,
type: LOAD_RECORD_SUCCESS,
record,
};
}
export function recordDeleted(id) {
export function setCurrentModelName(currentModelName) {
return {
type: DELETE_RECORD_SUCCESS,
id,
type: SET_CURRENT_MODEL_NAME,
currentModelName,
};
}
export function recordDeleteError() {
export function setForm(data) {
const form = [];
Object.keys(data).map(attr => {
form.push([attr, '']);
});
return {
type: DELETE_RECORD_ERROR,
type: SET_FORM,
form,
}
}
export function setFormErrors(formErrors) {
return {
type: SET_FORM_ERRORS,
formErrors,
};
}
export function setFormValidations(data) {
const form = Object.keys(data).map(attr => {
return { name: attr, validations: get(data[attr], ['params']) || {} }
});
const formValidations = getValidationsFromForm(form, []);
return {
type: SET_FORM_VALIDATIONS,
formValidations,
}
}
export function setInitialState() {
return {
type: SET_INITIAL_STATE,
};
}
export function setIsCreating() {
return {
type: SET_IS_CREATING,
};
}
export function setRecordAttribute(key, value) {
return {
type: SET_RECORD_ATTRIBUTE,
key,
value,
};
}
@ -106,9 +152,3 @@ export function toggleNull() {
type: TOGGLE_NULL,
};
}
export function cancelChanges() {
return {
type: CANCEL_CHANGES,
};
}

View File

@ -7,6 +7,9 @@
export const SET_INITIAL_STATE = 'app/Edit/SET_INITIAL_STATE';
export const SET_CURRENT_MODEL_NAME = 'app/Edit/SET_CURRENT_MODEL_NAME';
export const SET_IS_CREATING = 'app/Edit/SET_IS_CREATING';
export const SET_FORM_VALIDATIONS = 'app/Edit/SET_FORM_VALIDATIONS';
export const SET_FORM = 'app/Edit/SET_FORM';
export const SET_FORM_ERRORS = 'app/Edit/SET_FORM_ERRORS';
export const LOAD_RECORD = 'app/Edit/LOAD_RECORD';
export const LOAD_RECORD_SUCCESS = 'app/Edit/LOAD_RECORD_SUCCESS';

View File

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
import { get, isObject } from 'lodash';
import { map, get, isObject, isEmpty } from 'lodash';
import { router } from 'app';
// Components.
@ -26,6 +26,7 @@ import { makeSelectModels, makeSelectSchema } from 'containers/App/selectors';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import templateObject from 'utils/templateObject';
import { checkFormValidity } from '../../utils/formValidations';
// Styles.
import styles from './styles.scss';
@ -40,6 +41,9 @@ import {
editRecord,
toggleNull,
cancelChanges,
setFormValidations,
setForm,
setFormErrors,
} from './actions';
// Selectors.
@ -52,6 +56,10 @@ import {
makeSelectDeleting,
makeSelectIsCreating,
makeSelectIsRelationComponentNull,
makeSelectForm,
makeSelectFormValidations,
makeSelectFormErrors,
makeSelectDidCheckErrors,
} from './selectors';
import reducer from './reducer';
@ -74,7 +82,7 @@ export class Edit extends React.Component {
buttonBackground: 'primary',
buttonSize: 'buttonLg',
label: this.props.editing ? 'content-manager.containers.Edit.editing' : 'content-manager.containers.Edit.submit',
onClick: this.props.editRecord,
onClick: this.handleSubmit,
disabled: this.props.editing,
type: 'submit',
},
@ -94,7 +102,8 @@ export class Edit extends React.Component {
componentDidMount() {
this.props.setInitialState();
this.props.setCurrentModelName(this.props.match.params.slug.toLowerCase());
this.props.setFormValidations(this.props.models[this.props.match.params.slug.toLowerCase()].attributes);
this.props.setForm(this.props.models[this.props.match.params.slug.toLowerCase()].attributes);
// Detect that the current route is the `create` route or not
if (this.props.match.params.id === 'create') {
this.props.setIsCreating();
@ -118,12 +127,20 @@ export class Edit extends React.Component {
}
handleSubmit = () => {
this.props.editRecord();
const form = this.props.form.toJS();
map(this.props.record.toJS(), (value, key) => form[key] = value);
const formErrors = checkFormValidity(form, this.props.formValidations.toJS());
if (isEmpty(formErrors)) {
this.props.editRecord();
} else {
this.props.setFormErrors(formErrors);
}
}
handleSubmitOnEnterPress = (e) => {
if (e.keyCode === 13) {
this.props.editRecord();
this.handleSubmit();
}
}
@ -164,6 +181,9 @@ export class Edit extends React.Component {
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
editing={this.props.editing}
formErrors={this.props.formErrors.toJS()}
didCheckErrors={this.props.didCheckErrors}
formValidations={this.props.formValidations.toJS()}
/>
</div>
</div>
@ -185,17 +205,25 @@ export class Edit extends React.Component {
);
}
}
/* eslint-disable react/require-default-props */
Edit.propTypes = {
cancelChanges: PropTypes.func.isRequired,
currentModelName: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
]).isRequired,
// deleteRecord: PropTypes.func.isRequired,
// deleting: PropTypes.bool.isRequired,
didCheckErrors: PropTypes.bool.isRequired,
editing: PropTypes.bool.isRequired,
editRecord: PropTypes.func.isRequired,
form: PropTypes.object.isRequired,
formErrors: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]),
formValidations: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]),
isCreating: PropTypes.bool.isRequired,
isRelationComponentNull: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
@ -219,6 +247,9 @@ Edit.propTypes = {
PropTypes.bool,
]).isRequired,
setCurrentModelName: PropTypes.func.isRequired,
setForm: PropTypes.func.isRequired,
setFormErrors: PropTypes.func.isRequired,
setFormValidations: PropTypes.func.isRequired,
setInitialState: PropTypes.func.isRequired,
setIsCreating: PropTypes.func.isRequired,
setRecordAttribute: PropTypes.func.isRequired,
@ -235,6 +266,10 @@ const mapStateToProps = createStructuredSelector({
schema: makeSelectSchema(),
models: makeSelectModels(),
isRelationComponentNull: makeSelectIsRelationComponentNull(),
form: makeSelectForm(),
formValidations: makeSelectFormValidations(),
formErrors: makeSelectFormErrors(),
didCheckErrors: makeSelectDidCheckErrors(),
});
function mapDispatchToProps(dispatch) {
@ -248,6 +283,9 @@ function mapDispatchToProps(dispatch) {
editRecord,
toggleNull,
cancelChanges,
setFormValidations,
setForm,
setFormErrors,
},
dispatch
);

View File

@ -4,7 +4,7 @@
*
*/
import { fromJS, Map } from 'immutable';
import { fromJS, Map, List } from 'immutable';
import {
SET_INITIAL_STATE,
@ -21,6 +21,9 @@ import {
DELETE_RECORD_ERROR,
TOGGLE_NULL,
CANCEL_CHANGES,
SET_FORM_VALIDATIONS,
SET_FORM,
SET_FORM_ERRORS,
} from './constants';
const initialState = fromJS({
@ -31,6 +34,10 @@ const initialState = fromJS({
deleting: false,
isCreating: false,
isRelationComponentNull: false,
formValidations: List(),
formErrors: List(),
form: Map({}),
didCheckErrors: false,
});
function editReducer(state = initialState, action) {
@ -50,9 +57,12 @@ function editReducer(state = initialState, action) {
.set('model', action.model)
.set('id', action.id);
case LOAD_RECORD_SUCCESS:
return state.set('loading', false).set('record', fromJS(action.record));
return state
.set('loading', false)
.set('record', fromJS(action.record));
case SET_RECORD_ATTRIBUTE:
return state.setIn(['record', action.key], fromJS(action.value));
return state
.setIn(['record', action.key], fromJS(action.value));
case EDIT_RECORD:
return state.set('editing', true);
case EDIT_RECORD_SUCCESS:
@ -69,6 +79,15 @@ function editReducer(state = initialState, action) {
return state.set('isRelationComponentNull', !state.get('isRelationComponentNull'));
case CANCEL_CHANGES:
return state.set('record', Map({}));
case SET_FORM_VALIDATIONS:
return state
.set('formValidations', List(action.formValidations));
case SET_FORM:
return state.set('form', Map(action.form));
case SET_FORM_ERRORS:
return state
.set('formErrors', List(action.formErrors))
.set('didCheckErrors', !state.didCheckErrors);
default:
return state;
}

View File

@ -36,6 +36,18 @@ const makeSelectIsCreating = () =>
const makeSelectIsRelationComponentNull = () =>
createSelector(selectEditDomain(), substate => substate.get('isRelationComponentNull'));
const makeSelectForm = () =>
createSelector(selectEditDomain(), substate => substate.get('form'));
const makeSelectFormValidations = () =>
createSelector(selectEditDomain(), substate => substate.get('formValidations'));
const makeSelectFormErrors = () =>
createSelector(selectEditDomain(), substate => substate.get('formErrors'));
const makeSelectDidCheckErrors = () =>
createSelector(selectEditDomain(), substate => substate.get('didCheckErrors'));
export default selectEditDomain;
export {
makeSelectRecord,
@ -45,4 +57,8 @@ export {
makeSelectDeleting,
makeSelectIsCreating,
makeSelectIsRelationComponentNull,
makeSelectForm,
makeSelectFormValidations,
makeSelectFormErrors,
makeSelectDidCheckErrors,
};

View File

@ -87,14 +87,6 @@ export class List extends React.Component {
}
changePage = (page) => {
router.push({
pathname: this.props.location.pathname,
search: `?page=${page}&limit=${this.props.limit}&sort=${this.props.sort}`,
});
this.props.changePage(page);
}
init(props) {
const slug = props.match.params.slug;
// Set current model name
@ -132,6 +124,14 @@ export class List extends React.Component {
});
}
handleChangePage = (page) => {
router.push({
pathname: this.props.location.pathname,
search: `?page=${page}&limit=${this.props.limit}&sort=${this.props.sort}`,
});
this.props.changePage(page);
}
handleChangeSort = (sort) => {
router.push({
pathname: this.props.location.pathname,
@ -248,7 +248,7 @@ export class List extends React.Component {
<TableFooter
limit={this.props.limit}
currentPage={this.props.currentPage}
changePage={this.changePage}
changePage={this.handleChangePage}
count={this.props.count}
className="push-lg-right"
handleChangeLimit={this.handleChangeLimit}

View File

@ -12,6 +12,19 @@
"containers.List.pluginHeaderDescription": "Manage your {label}",
"components.LimitSelect.itemsPerPage": "Items per page",
"containers.List.errorFetchRecords": "Error",
"error.validation.required": "This value input is required.",
"error.validation.regex": "The value not match the regex.",
"error.validation.max": "The value is too high.",
"error.validation.min": "The value is too low.",
"error.validation.maxLength": "The value is too long.",
"error.validation.minLength": "The value is too shot.",
"error.contentTypeName.taken": "This name already exists",
"error.attribute.taken": "This field name already exists",
"error.attribute.key.taken": "This value already exists",
"error.attribute.sameKeyAndName": "Can't be equals",
"error.validation.minSupMax": "Can't be superior",
"pageNotFound": "Page not found",
"popUpWarning.button.cancel": "Cancel",

View File

@ -13,6 +13,18 @@
"components.LimitSelect.itemsPerPage": "Éléments par page",
"containers.List.errorFetchRecords": "Erreur",
"error.validation.required": "Ce champ est obligatoire.",
"error.validation.regex": "La valeur ne correspond pas au format attendu.",
"error.validation.max": "La valeur est trop grande.",
"error.validation.min": "La valeur est trop basse.",
"error.validation.maxLength": "La valeur est trop longue.",
"error.validation.minLength": "La valeur est trop courte.",
"error.contentTypeName.taken": "Ce nom existe déjà",
"error.attribute.taken": "Ce champ existe déjà",
"error.attribute.key.taken": "Cette valeur existe déjà",
"error.attribute.sameKeyAndName": "Ne peuvent pas être égaux",
"error.validation.minSupMax": "Ne peut pas être plus grand",
"popUpWarning.button.cancel": "Annuler",
"popUpWarning.button.confirm": "Confirmer",
"popUpWarning.title": "Confirmation requise",

View File

@ -0,0 +1,96 @@
import { forEach, isObject, isArray, map, mapKeys, includes, reject, isEmpty, findIndex, isUndefined } from 'lodash';
/* eslint-disable consistent-return */
export function getValidationsFromForm(form, formValidations) {
map(form, (value, key) => {
// Check if the object
if (isObject(value) && !isArray(value)) {
forEach(value, (subValue) => {
// Check if it has nestedInputs
if (isArray(subValue) && value.type !== 'select') {
return getValidationsFromForm(subValue, formValidations);
}
});
}
if (isArray(value) && value.type !== 'select') {
return getValidationsFromForm(form[key], formValidations);
}
// Push the target and the validation
if (value.name) {
formValidations.push({ name: value.name, validations: value.validations });
}
});
return formValidations;
}
export function checkFormValidity(formData, formValidations) {
const errors = [];
forEach(formData, (value, key) => {
const validationValue = formValidations[findIndex(formValidations, ['name', key])];
if (!isUndefined(validationValue)) {
const inputErrors = validate(value, validationValue.validations);
if (!isEmpty(inputErrors)) {
errors.push({ name: key, errors: inputErrors });
}
}
});
return errors;
}
function validate(value, validations) {
let errors = [];
// Handle i18n
const requiredError = { id: 'content-manager.error.validation.required' };
mapKeys(validations, (validationValue, validationKey) => {
switch (validationKey) {
case 'max':
if (parseInt(value, 10) > validationValue) {
errors.push({ id: 'content-manager.error.validation.max' });
}
break;
case 'min':
if (parseInt(value, 10) < validationValue) {
errors.push({ id: 'content-manager.error.validation.min' });
}
break;
case 'maxLength':
if (value.length > validationValue) {
errors.push({ id: 'content-manager.error.validation.maxLength' });
}
break;
case 'minLength':
if (value.length < validationValue) {
errors.push({ id: 'content-manager.error.validation.minLength' });
}
break;
case 'required':
if (value.length === 0) {
errors.push({ id: 'content-manager.error.validation.required' });
}
break;
case 'regex':
if (!new RegExp(validationValue).test(value)) {
errors.push({ id: 'content-manager.error.validation.regex' });
}
break;
default:
errors = [];
}
});
if (includes(errors, requiredError)) {
errors = reject(errors, (error) => error !== requiredError);
}
return errors;
}