diff --git a/packages/strapi-helper-plugin/lib/src/components/Input/index.js b/packages/strapi-helper-plugin/lib/src/components/Input/index.js index aa7f7b4d57..47d76e9913 100644 --- a/packages/strapi-helper-plugin/lib/src/components/Input/index.js +++ b/packages/strapi-helper-plugin/lib/src/components/Input/index.js @@ -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, ]), }; diff --git a/packages/strapi-plugin-content-manager/admin/src/components/EditForm/index.js b/packages/strapi-plugin-content-manager/admin/src/components/EditForm/index.js index 74c1aea7aa..e492b4c368 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/EditForm/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/EditForm/index.js @@ -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 ( ); }); @@ -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, diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/App/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/App/reducer.js index 304a862440..84a657f400 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/App/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/App/reducer.js @@ -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) { diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/actions.js b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/actions.js index 95032c226d..2514f3ee04 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/actions.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/actions.js @@ -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, - }; -} diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/constants.js b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/constants.js index 03d8ba6a0c..99d3bac53f 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/constants.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/constants.js @@ -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'; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/index.js index fa13014d13..d7dcb53648 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/index.js @@ -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()} /> @@ -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 ); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/reducer.js index 39f782d8c1..ede9298ce0 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/reducer.js @@ -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; } diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/selectors.js b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/selectors.js index 349294f2e6..664b65e0ef 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Edit/selectors.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Edit/selectors.js @@ -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, }; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/List/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/List/index.js index edba698981..44e94b8556 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/List/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/List/index.js @@ -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 { { + + // 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; +}