mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 10:01:40 -05:00
Work on form errors & validation
This commit is contained in:
parent
8d686ae9e1
commit
1d7cef5556
11 changed files with 383 additions and 135 deletions
|
@ -84,6 +84,7 @@ Datatable.propTypes = {
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
newFormOptions: PropTypes.object,
|
newFormOptions: PropTypes.object,
|
||||||
editFormOptions: PropTypes.object,
|
editFormOptions: PropTypes.object,
|
||||||
|
emptyState: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
Datatable.defaultProps = {
|
Datatable.defaultProps = {
|
||||||
|
|
21
packages/vulcan-forms/lib/components/FieldErrors.jsx
Normal file
21
packages/vulcan-forms/lib/components/FieldErrors.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
|
const FieldErrors = ({ errors }) => (
|
||||||
|
<ul className="form-input-errors">
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
{error.message || (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`errors.${error.data.type}`}
|
||||||
|
values={{ ...error.data }}
|
||||||
|
defaultMessage={JSON.stringify(error)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
registerComponent('FieldErrors', FieldErrors);
|
|
@ -164,24 +164,48 @@ class Form extends Component {
|
||||||
Get a field's value in a document // TODO: maybe not needed?
|
Get a field's value in a document // TODO: maybe not needed?
|
||||||
|
|
||||||
*/
|
*/
|
||||||
getValue = (fieldName, document) => {
|
getValue = (fieldName, fieldSchema, document) => {
|
||||||
if (typeof document[fieldName] !== 'undefined' && document[fieldName] !== null) {
|
if (typeof document[fieldName] !== 'undefined' && document[fieldName] !== null) {
|
||||||
return document[fieldName];
|
|
||||||
|
let value = document[fieldName];
|
||||||
|
// convert value type if needed
|
||||||
|
if (fieldSchema.type.definitions[0].type === Number) value = Number(value);
|
||||||
|
|
||||||
|
// if value is an array of objects ({_id: '123'}, {_id: 'abc'}), flatten it into an array of strings (['123', 'abc'])
|
||||||
|
// fallback to item itself if item._id is not defined (ex: item is not an object or item is just {slug: 'xxx'})
|
||||||
|
// if (Array.isArray(field.value)) {
|
||||||
|
// field.value = field.value.map(item => item._id || item);
|
||||||
|
// }
|
||||||
|
// TODO: not needed anymore?
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
Given an array field, get its nested schema
|
||||||
|
|
||||||
|
*/
|
||||||
|
getNestedSchema = (fieldName) => {
|
||||||
|
const arrayItemSchema = this.getSchema()[`${fieldName}.$`];
|
||||||
|
return arrayItemSchema && arrayItemSchema.type.definitions[0].type._schema;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
Given a field's name, its schema, document, and parent, create the
|
Given a field's name, its schema, document, and parent, create the
|
||||||
complete field object to be passed to the component
|
complete field object to be passed to the component
|
||||||
|
|
||||||
*/
|
*/
|
||||||
createField = (fieldName, fieldSchema, document, parentFieldName) => {
|
createField = (fieldName, fieldSchema, document, parentFieldName, parentPath) => {
|
||||||
// console.log('// createField', fieldName)
|
|
||||||
|
const fieldPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
||||||
|
|
||||||
|
// console.log('// createField', fieldPath)
|
||||||
// console.log(fieldSchema)
|
// console.log(fieldSchema)
|
||||||
// console.log(document)
|
// console.log(document)
|
||||||
// console.log('-> nested: ', fieldSchema.type.singleType === Array)
|
// console.log('-> nested: ', !!this.getNestedSchema(fieldName))
|
||||||
|
|
||||||
// store fieldSchema object in this.fieldSchemas
|
// store fieldSchema object in this.fieldSchemas
|
||||||
this.fieldSchemas[fieldName] = fieldSchema;
|
this.fieldSchemas[fieldName] = fieldSchema;
|
||||||
|
@ -191,6 +215,7 @@ class Form extends Component {
|
||||||
// intialize properties
|
// intialize properties
|
||||||
let field = {
|
let field = {
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
|
path: fieldPath,
|
||||||
datatype: fieldSchema.type,
|
datatype: fieldSchema.type,
|
||||||
control: fieldSchema.control,
|
control: fieldSchema.control,
|
||||||
layout: this.props.layout,
|
layout: this.props.layout,
|
||||||
|
@ -205,21 +230,10 @@ class Form extends Component {
|
||||||
field.label = this.getLabel(fieldName);
|
field.label = this.getLabel(fieldName);
|
||||||
|
|
||||||
// note: for nested fields, value will be null here and set by FormNested later
|
// note: for nested fields, value will be null here and set by FormNested later
|
||||||
const fieldValue = this.getValue(fieldName, document);
|
const fieldValue = this.getValue(fieldName, fieldSchema, document);
|
||||||
|
|
||||||
// add value
|
// add value
|
||||||
if (fieldValue) {
|
if (fieldValue) {
|
||||||
field.value = fieldValue;
|
field.value = fieldValue;
|
||||||
|
|
||||||
// convert value type if needed
|
|
||||||
if (fieldSchema.type.definitions[0].type === Number) field.value = Number(field.value);
|
|
||||||
|
|
||||||
// if value is an array of objects ({_id: '123'}, {_id: 'abc'}), flatten it into an array of strings (['123', 'abc'])
|
|
||||||
// fallback to item itself if item._id is not defined (ex: item is not an object or item is just {slug: 'xxx'})
|
|
||||||
// if (Array.isArray(field.value)) {
|
|
||||||
// field.value = field.value.map(item => item._id || item);
|
|
||||||
// }
|
|
||||||
// TODO: not needed anymore?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// backward compatibility from 'autoform' to 'form'
|
// backward compatibility from 'autoform' to 'form'
|
||||||
|
@ -301,25 +315,23 @@ class Form extends Component {
|
||||||
// add document
|
// add document
|
||||||
field.document = this.getDocument();
|
field.document = this.getDocument();
|
||||||
|
|
||||||
// add error state
|
// add any relevant errors
|
||||||
const validationError = _.findWhere(this.state.errors, { name: 'app.validation_error' });
|
// const fieldErrors = _.filter(this.state.errors, error => error.data.name === fieldName);
|
||||||
if (validationError) {
|
// if (fieldErrors) {
|
||||||
const fieldErrors = _.filter(validationError.data.errors, error => error.data.fieldName === fieldName);
|
// field.errors = fieldErrors.map(error => ({ ...error, message: this.getErrorMessage(error) }));
|
||||||
if (fieldErrors) {
|
// }
|
||||||
field.errors = fieldErrors.map(error => ({ ...error, message: this.getErrorMessage(error) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nested fields: set control to "nested"
|
// nested fields: set control to "nested"
|
||||||
if (fieldSchema.type.singleType === Array) {
|
const nestedSchema = this.getNestedSchema(fieldName);
|
||||||
|
|
||||||
|
if (nestedSchema) {
|
||||||
|
field.nestedSchema = nestedSchema;
|
||||||
field.control = 'nested';
|
field.control = 'nested';
|
||||||
// get nested schema
|
// get nested schema
|
||||||
field.nestedSchema = this.getSchema()[`${fieldName}.$`].type.definitions[0].type._schema; // TODO: do this better
|
|
||||||
|
|
||||||
// for each nested field, get field object by calling createField recursively
|
// for each nested field, get field object by calling createField recursively
|
||||||
field.nestedFields = this.getFieldNames(field.nestedSchema).map(subFieldName => {
|
field.nestedFields = this.getFieldNames(field.nestedSchema).map(subFieldName => {
|
||||||
const subFieldSchema = field.nestedSchema[subFieldName];
|
const subFieldSchema = field.nestedSchema[subFieldName];
|
||||||
return this.createField(subFieldName, subFieldSchema, document, fieldName);
|
return this.createField(subFieldName, subFieldSchema, document, fieldName, fieldPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,41 +447,7 @@ class Form extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Render errors
|
|
||||||
|
|
||||||
*/
|
|
||||||
renderErrors = () => {
|
|
||||||
return (
|
|
||||||
<div className="form-errors">
|
|
||||||
{this.state.errors.map((error, index) => {
|
|
||||||
let message;
|
|
||||||
|
|
||||||
if (error.data && error.data.errors) {
|
|
||||||
// this error is a "multi-error" with multiple sub-errors
|
|
||||||
|
|
||||||
message = error.data.errors.map(error => {
|
|
||||||
return {
|
|
||||||
content: this.getErrorMessage(error),
|
|
||||||
data: error.data,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// this is a regular error
|
|
||||||
|
|
||||||
message = {
|
|
||||||
content:
|
|
||||||
error.message ||
|
|
||||||
this.context.intl.formatMessage({ id: error.id, defaultMessage: error.id }, error.data),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Components.FormFlash key={index} message={message} type="error" />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------- //
|
// --------------------------------------------------------------------- //
|
||||||
// ------------------------------- Context ----------------------------- //
|
// ------------------------------- Context ----------------------------- //
|
||||||
|
@ -486,7 +464,7 @@ class Form extends Component {
|
||||||
|
|
||||||
// add error to state
|
// add error to state
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
errors: [...prevState.errors, graphQLError],
|
errors: [...prevState.errors, ...graphQLError.data.errors],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -560,6 +538,7 @@ class Form extends Component {
|
||||||
addToSubmitForm: this.addToSubmitForm,
|
addToSubmitForm: this.addToSubmitForm,
|
||||||
addToSuccessForm: this.addToSuccessForm,
|
addToSuccessForm: this.addToSuccessForm,
|
||||||
addToFailureForm: this.addToFailureForm,
|
addToFailureForm: this.addToFailureForm,
|
||||||
|
errors: this.state.errors,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -584,7 +563,6 @@ class Form extends Component {
|
||||||
this.addToDeletedValues(path);
|
this.addToDeletedValues(path);
|
||||||
} else {
|
} else {
|
||||||
set(prevState.currentValues, path, value);
|
set(prevState.currentValues, path, value);
|
||||||
// dot.str(path, value, prevState.currentValues);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return prevState;
|
return prevState;
|
||||||
|
@ -783,7 +761,7 @@ class Form extends Component {
|
||||||
return (
|
return (
|
||||||
<div className={'document-' + this.getFormType()}>
|
<div className={'document-' + this.getFormType()}>
|
||||||
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
|
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
|
||||||
{this.renderErrors()}
|
<Components.FormErrors errors={this.state.errors}/>
|
||||||
|
|
||||||
{fieldGroups.map(group => (
|
{fieldGroups.map(group => (
|
||||||
<Components.FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} />
|
<Components.FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} />
|
||||||
|
@ -870,6 +848,7 @@ Form.childContextTypes = {
|
||||||
clearForm: PropTypes.func,
|
clearForm: PropTypes.func,
|
||||||
getDocument: PropTypes.func,
|
getDocument: PropTypes.func,
|
||||||
submitForm: PropTypes.func,
|
submitForm: PropTypes.func,
|
||||||
|
errors: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Form;
|
module.exports = Form;
|
||||||
|
|
|
@ -52,7 +52,7 @@ class FormComponent extends PureComponent {
|
||||||
renderComponent() {
|
renderComponent() {
|
||||||
|
|
||||||
// see https://facebook.github.io/react/warnings/unknown-prop.html
|
// see https://facebook.github.io/react/warnings/unknown-prop.html
|
||||||
const { control, group, updateCurrentValues, document, beforeComponent, afterComponent, limit, errors, nestedSchema, nestedFields, datatype, parentFieldName, itemIndex, ...rest } = this.props; // eslint-disable-line
|
const { control, group, updateCurrentValues, document, beforeComponent, afterComponent, limit, errors, nestedSchema, nestedFields, datatype, parentFieldName, itemIndex, path, ...rest } = this.props; // eslint-disable-line
|
||||||
|
|
||||||
const properties = {
|
const properties = {
|
||||||
value: '', // default value, will be overridden by `rest` if real value has been passed down through props
|
value: '', // default value, will be overridden by `rest` if real value has been passed down through props
|
||||||
|
@ -72,7 +72,7 @@ class FormComponent extends PureComponent {
|
||||||
switch (this.props.control) {
|
switch (this.props.control) {
|
||||||
|
|
||||||
case 'nested':
|
case 'nested':
|
||||||
return <Components.FormNested updateCurrentValues={updateCurrentValues} nestedSchema={nestedSchema} nestedFields={nestedFields} {...properties}/>;
|
return <Components.FormNested path={path} updateCurrentValues={updateCurrentValues} nestedSchema={nestedSchema} nestedFields={nestedFields} datatype={datatype} {...properties}/>;
|
||||||
|
|
||||||
case 'number':
|
case 'number':
|
||||||
return <Components.FormComponentNumber {...properties}/>;
|
return <Components.FormComponentNumber {...properties}/>;
|
||||||
|
@ -145,12 +145,9 @@ class FormComponent extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderErrors = () => {
|
getErrors = () => {
|
||||||
return (
|
const fieldErrors = this.context.errors.filter(error => error.data.name === this.props.path);
|
||||||
<ul className='form-input-errors'>
|
return fieldErrors;
|
||||||
{this.props.errors.map((error, index) => <li key={index}>{error.message}</li>)}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showClear = () => {
|
showClear = () => {
|
||||||
|
@ -175,14 +172,14 @@ class FormComponent extends PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const hasErrors = this.props.errors && this.props.errors.length;
|
const hasErrors = this.getErrors() && this.getErrors().length;
|
||||||
const inputClass = classNames('form-input', `input-${this.props.name}`, `form-component-${this.props.control || 'default'}`,{'input-error': hasErrors});
|
const inputClass = classNames('form-input', `input-${this.props.name}`, `form-component-${this.props.control || 'default'}`,{'input-error': hasErrors});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={inputClass}>
|
<div className={inputClass}>
|
||||||
{this.props.beforeComponent ? this.props.beforeComponent : null}
|
{this.props.beforeComponent ? this.props.beforeComponent : null}
|
||||||
{this.renderComponent()}
|
{this.renderComponent()}
|
||||||
{hasErrors ? this.renderErrors() : null}
|
{hasErrors ? <Components.FieldErrors errors={this.getErrors()}/> : null}
|
||||||
{this.showClear() ? this.renderClear() : null}
|
{this.showClear() ? this.renderClear() : null}
|
||||||
{this.props.limit ? <div className={classNames('form-control-limit', {danger: this.state.limit < 10})}>{this.state.limit}</div> : null}
|
{this.props.limit ? <div className={classNames('form-control-limit', {danger: this.state.limit < 10})}>{this.state.limit}</div> : null}
|
||||||
{this.props.afterComponent ? this.props.afterComponent : null}
|
{this.props.afterComponent ? this.props.afterComponent : null}
|
||||||
|
@ -209,6 +206,7 @@ FormComponent.propTypes = {
|
||||||
FormComponent.contextTypes = {
|
FormComponent.contextTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
addToDeletedValues: PropTypes.func,
|
addToDeletedValues: PropTypes.func,
|
||||||
|
errors: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
registerComponent('FormComponent', FormComponent);
|
registerComponent('FormComponent', FormComponent);
|
||||||
|
|
64
packages/vulcan-forms/lib/components/FormErrors.jsx
Normal file
64
packages/vulcan-forms/lib/components/FormErrors.jsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
import Alert from 'react-bootstrap/lib/Alert';
|
||||||
|
|
||||||
|
const FormErrors = ({ errors }) => (
|
||||||
|
<div className="form-errors">
|
||||||
|
{!!errors.length && (
|
||||||
|
<Alert className="flash-message" bsStyle="danger">
|
||||||
|
<ul>
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
{error.message || (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`errors.${error.data.type}`}
|
||||||
|
values={{ ...error.data }}
|
||||||
|
defaultMessage={JSON.stringify(error)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
registerComponent('FormErrors', FormErrors);
|
||||||
|
|
||||||
|
// /*
|
||||||
|
|
||||||
|
// Render errors
|
||||||
|
|
||||||
|
// */
|
||||||
|
// renderErrors = () => {
|
||||||
|
// return (
|
||||||
|
// <div className="form-errors">
|
||||||
|
// {this.state.errors.map((error, index) => {
|
||||||
|
// let message;
|
||||||
|
|
||||||
|
// if (error.data && error.data.errors) {
|
||||||
|
// // this error is a "multi-error" with multiple sub-errors
|
||||||
|
|
||||||
|
// message = error.data.errors.map(error => {
|
||||||
|
// return {
|
||||||
|
// content: this.getErrorMessage(error),
|
||||||
|
// data: error.data,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// // this is a regular error
|
||||||
|
|
||||||
|
// message = {
|
||||||
|
// content:
|
||||||
|
// error.message ||
|
||||||
|
// this.context.intl.formatMessage({ id: error.id, defaultMessage: error.id }, error.data),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return <Components.FormFlash key={index} message={message} type="error" />;
|
||||||
|
// })}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
|
@ -3,14 +3,27 @@ import PropTypes from 'prop-types';
|
||||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
import Button from 'react-bootstrap/lib/Button';
|
import Button from 'react-bootstrap/lib/Button';
|
||||||
|
|
||||||
const FormNestedItem = ({ isDeleted, nestedFields, name, subDocument, removeItem, ...props }) => {
|
const FormNestedItem = (
|
||||||
|
{ isDeleted, nestedFields, name, path, subDocument, removeItem, itemIndex, ...props },
|
||||||
|
{ errors }
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={`form-nested-item ${isDeleted ? 'form-nested-item-deleted' : ''}`}>
|
<div className={`form-nested-item ${isDeleted ? 'form-nested-item-deleted' : ''}`}>
|
||||||
<div className="form-nested-item-inner">
|
<div className="form-nested-item-inner">
|
||||||
{nestedFields.map((field, i) => {
|
{nestedFields.map((field, i) => {
|
||||||
// note: default value to '' to avoid uncontrolled component error
|
// note: default value to '' to avoid uncontrolled component error
|
||||||
const value = subDocument && subDocument[field.name] || '';
|
let value = (subDocument && subDocument[field.name]) || '';
|
||||||
return <Components.FormComponent key={i} {...props} {...field} value={value} />;
|
if (props.control === 'number') value = Number(value);
|
||||||
|
return (
|
||||||
|
<Components.FormComponent
|
||||||
|
key={i}
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
path={`${path}.${field.name}`}
|
||||||
|
value={value}
|
||||||
|
itemIndex={itemIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-nested-item-remove">
|
<div className="form-nested-item-remove">
|
||||||
|
@ -23,21 +36,24 @@ const FormNestedItem = ({ isDeleted, nestedFields, name, subDocument, removeItem
|
||||||
✖️
|
✖️
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-nested-item-deleted-overlay"/>
|
<div className="form-nested-item-deleted-overlay" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FormNestedItem.contextTypes = {
|
||||||
|
errors: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
registerComponent('FormNestedItem', FormNestedItem);
|
registerComponent('FormNestedItem', FormNestedItem);
|
||||||
|
|
||||||
class FormNested extends PureComponent {
|
class FormNested extends PureComponent {
|
||||||
|
|
||||||
addItem = () => {
|
addItem = () => {
|
||||||
this.props.updateCurrentValues({[`${this.props.name}.${this.props.value.length}`] : {}});
|
this.props.updateCurrentValues({ [`${this.props.path}.${this.props.value.length}`]: {} });
|
||||||
};
|
};
|
||||||
|
|
||||||
removeItem = index => {
|
removeItem = index => {
|
||||||
this.props.updateCurrentValues({[`${this.props.name}.${index}`] : null});
|
this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -47,7 +63,7 @@ class FormNested extends PureComponent {
|
||||||
look for the presence of 'addresses.1')
|
look for the presence of 'addresses.1')
|
||||||
*/
|
*/
|
||||||
isDeleted = index => {
|
isDeleted = index => {
|
||||||
return this.context.deletedValues.includes(`${this.props.name}.${index}`);
|
return this.context.deletedValues.includes(`${this.props.path}.${index}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -56,13 +72,22 @@ class FormNested extends PureComponent {
|
||||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||||
<div className="col-sm-9">
|
<div className="col-sm-9">
|
||||||
{this.props.value &&
|
{this.props.value &&
|
||||||
this.props.value.map((subDocument, i) => (
|
this.props.value.map(
|
||||||
!this.isDeleted(i) && <FormNestedItem key={i} itemIndex={i} {...this.props} subDocument={subDocument} removeItem={() => {this.removeItem(i)}} />
|
(subDocument, i) =>
|
||||||
))}
|
!this.isDeleted(i) && (
|
||||||
<Button
|
<FormNestedItem
|
||||||
bsStyle="success"
|
{...this.props}
|
||||||
onClick={this.addItem}
|
key={i}
|
||||||
>
|
itemIndex={i}
|
||||||
|
subDocument={subDocument}
|
||||||
|
path={`${this.props.path}.${i}`}
|
||||||
|
removeItem={() => {
|
||||||
|
this.removeItem(i);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Button bsStyle="success" onClick={this.addItem}>
|
||||||
➕
|
➕
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +97,7 @@ class FormNested extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
FormNested.contextTypes = {
|
FormNested.contextTypes = {
|
||||||
deletedValues: PropTypes.array
|
deletedValues: PropTypes.array,
|
||||||
}
|
};
|
||||||
|
|
||||||
registerComponent('FormNested', FormNested);
|
registerComponent('FormNested', FormNested);
|
||||||
|
|
|
@ -12,6 +12,8 @@ import '../components/bootstrap/Url.jsx';
|
||||||
import '../components/bootstrap/Date.jsx';
|
import '../components/bootstrap/Date.jsx';
|
||||||
|
|
||||||
import '../components/Flash.jsx';
|
import '../components/Flash.jsx';
|
||||||
|
import '../components/FieldErrors.jsx';
|
||||||
|
import '../components/FormErrors.jsx';
|
||||||
import '../components/FormComponent.jsx';
|
import '../components/FormComponent.jsx';
|
||||||
import '../components/FormNested.jsx';
|
import '../components/FormNested.jsx';
|
||||||
import '../components/FormGroup.jsx';
|
import '../components/FormGroup.jsx';
|
||||||
|
|
|
@ -134,4 +134,7 @@ addStrings('en', {
|
||||||
|
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
|
|
||||||
|
"errors.expectedType": `Expected a field of type {dataType}, got “{value}” instead.`,
|
||||||
|
"errors.required": `Field “{name}” is required.`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,10 +6,10 @@ String.prototype.replaceAll = function(search, replacement) {
|
||||||
return target.replace(new RegExp(search, 'g'), replacement);
|
return target.replace(new RegExp(search, 'g'), replacement);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormattedMessage = ({ id, values, defaultMessage, html = false }) => {
|
const FormattedMessage = ({ id, values, defaultMessage = '', html = false }) => {
|
||||||
const messages = Strings[getSetting('locale', 'en')] || {};
|
const messages = Strings[getSetting('locale', 'en')] || {};
|
||||||
let message = messages[id] || defaultMessage;
|
let message = messages[id] || defaultMessage;
|
||||||
if (values) {
|
if (message && values) {
|
||||||
_.forEach(values, (value, key) => {
|
_.forEach(values, (value, key) => {
|
||||||
message = message.replaceAll(`{${key}}`, value);
|
message = message.replaceAll(`{${key}}`, value);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const validateDocument = (document, collection, context) => {
|
export const validateDocument = (document, collection, context) => {
|
||||||
|
|
||||||
const { Users, currentUser } = context;
|
const { Users, currentUser } = context;
|
||||||
const schema = collection.simpleSchema()._schema;
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
|
||||||
|
@ -18,14 +17,13 @@ export const validateDocument = (document, collection, context) => {
|
||||||
|
|
||||||
// Check validity of inserted document
|
// Check validity of inserted document
|
||||||
_.forEach(document, (value, fieldName) => {
|
_.forEach(document, (value, fieldName) => {
|
||||||
|
|
||||||
const fieldSchema = schema[fieldName];
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
// 1. check that the current user has permission to insert each field
|
// 1. check that the current user has permission to insert each field
|
||||||
if (!fieldSchema || !Users.canInsertField (currentUser, fieldSchema)) {
|
if (!fieldSchema || !Users.canInsertField(currentUser, fieldSchema)) {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.disallowed_property_detected',
|
id: 'app.disallowed_property_detected',
|
||||||
fieldName
|
fieldName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,27 +31,24 @@ export const validateDocument = (document, collection, context) => {
|
||||||
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.field_is_too_long',
|
id: 'app.field_is_too_long',
|
||||||
data: {fieldName, limit: fieldSchema.limit}
|
data: { fieldName, limit: fieldSchema.limit },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. check that fields have the proper type
|
// 3. check that fields have the proper type
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. check that required fields have a value
|
// 4. check that required fields have a value
|
||||||
_.keys(schema).forEach(fieldName => {
|
_.keys(schema).forEach(fieldName => {
|
||||||
|
|
||||||
const fieldSchema = schema[fieldName];
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
|
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.required_field_missing',
|
id: 'app.required_field_missing',
|
||||||
data: {fieldName}
|
data: { fieldName },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. still run SS validation for now for backwards compatibility
|
// 5. still run SS validation for now for backwards compatibility
|
||||||
|
@ -64,13 +59,12 @@ export const validateDocument = (document, collection, context) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.schema_validation_error',
|
id: 'app.schema_validation_error',
|
||||||
data: {message: error.message}
|
data: { message: error.message },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return validationErrors;
|
return validationErrors;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -84,7 +78,6 @@ export const validateDocument = (document, collection, context) => {
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const validateModifier = (modifier, document, collection, context) => {
|
export const validateModifier = (modifier, document, collection, context) => {
|
||||||
|
|
||||||
const { Users, currentUser } = context;
|
const { Users, currentUser } = context;
|
||||||
const schema = collection.simpleSchema()._schema;
|
const schema = collection.simpleSchema()._schema;
|
||||||
const set = modifier.$set;
|
const set = modifier.$set;
|
||||||
|
@ -94,61 +87,223 @@ export const validateModifier = (modifier, document, collection, context) => {
|
||||||
|
|
||||||
// 1. check that the current user has permission to edit each field
|
// 1. check that the current user has permission to edit each field
|
||||||
const modifiedProperties = _.keys(set).concat(_.keys(unset));
|
const modifiedProperties = _.keys(set).concat(_.keys(unset));
|
||||||
modifiedProperties.forEach(function (fieldName) {
|
modifiedProperties.forEach(function(fieldName) {
|
||||||
var field = schema[fieldName];
|
var field = schema[fieldName];
|
||||||
if (!field || !Users.canEditField(currentUser, field, document)) {
|
if (!field || !Users.canEditField(currentUser, field, document)) {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.disallowed_property_detected',
|
id: 'app.disallowed_property_detected',
|
||||||
fieldName
|
data: {name: fieldName},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check validity of set modifier
|
// Check validity of set modifier
|
||||||
_.forEach(set, (value, fieldName) => {
|
_.forEach(set, (value, fieldName) => {
|
||||||
|
|
||||||
const fieldSchema = schema[fieldName];
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
// 2. check field lengths
|
// 2. check field lengths
|
||||||
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.field_is_too_long',
|
id: 'app.field_is_too_long',
|
||||||
data: {fieldName, limit: fieldSchema.limit}
|
data: { name: fieldName, limit: fieldSchema.limit },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. check that fields have the proper type
|
// 3. check that fields have the proper type
|
||||||
// TODO
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// // 4. check that required fields have a value
|
||||||
|
// // when editing, we only want to require fields that are actually part of the form
|
||||||
|
// // so we make sure required keys are present in the $unset object
|
||||||
|
// _.keys(schema).forEach(fieldName => {
|
||||||
|
// const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
|
// if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
|
||||||
|
// validationErrors.push({
|
||||||
|
// id: 'app.required_field_missing',
|
||||||
|
// data: { name: fieldName },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 5. still run SS validation for now for backwards compatibility
|
||||||
|
const validationContext = collection.simpleSchema().newContext();
|
||||||
|
validationContext.validate({ $set: set, $unset: unset }, { modifier: true });
|
||||||
|
|
||||||
|
if (!validationContext.isValid()) {
|
||||||
|
const errors = validationContext.validationErrors();
|
||||||
|
console.log('// validationContext');
|
||||||
|
console.log(validationContext.isValid());
|
||||||
|
console.log(errors);
|
||||||
|
errors.forEach(error => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log(error);
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.schema_validation_error',
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
The following versions were written to be more SimpleSchema-agnostic, but
|
||||||
|
are not currently used
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
If document is not trusted, run validation steps:
|
||||||
|
|
||||||
|
1. Check that the current user has permission to edit each field
|
||||||
|
2. Check field lengths
|
||||||
|
3. Check field types
|
||||||
|
4. Check for missing fields
|
||||||
|
5. Run SimpleSchema validation step (for now)
|
||||||
|
|
||||||
|
*/
|
||||||
|
export const validateDocumentNotUsed = (document, collection, context) => {
|
||||||
|
const { Users, currentUser } = context;
|
||||||
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
|
||||||
|
let validationErrors = [];
|
||||||
|
|
||||||
|
// Check validity of inserted document
|
||||||
|
_.forEach(document, (value, fieldName) => {
|
||||||
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
|
// 1. check that the current user has permission to insert each field
|
||||||
|
if (!fieldSchema || !Users.canInsertField(currentUser, fieldSchema)) {
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.disallowed_property_detected',
|
||||||
|
fieldName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. check field lengths
|
||||||
|
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.field_is_too_long',
|
||||||
|
data: { fieldName, limit: fieldSchema.limit },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. check that fields have the proper type
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. check that required fields have a value
|
||||||
|
_.keys(schema).forEach(fieldName => {
|
||||||
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
|
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.required_field_missing',
|
||||||
|
data: { fieldName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. still run SS validation for now for backwards compatibility
|
||||||
|
try {
|
||||||
|
collection.simpleSchema().validate(document);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.schema_validation_error',
|
||||||
|
data: { message: error.message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
If document is not trusted, run validation steps:
|
||||||
|
|
||||||
|
1. Check that the current user has permission to insert each field
|
||||||
|
2. Check field lengths
|
||||||
|
3. Check field types
|
||||||
|
4. Check for missing fields
|
||||||
|
5. Run SimpleSchema validation step (for now)
|
||||||
|
|
||||||
|
*/
|
||||||
|
export const validateModifierNotUsed = (modifier, document, collection, context) => {
|
||||||
|
const { Users, currentUser } = context;
|
||||||
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
const set = modifier.$set;
|
||||||
|
const unset = modifier.$unset;
|
||||||
|
|
||||||
|
let validationErrors = [];
|
||||||
|
|
||||||
|
// 1. check that the current user has permission to edit each field
|
||||||
|
const modifiedProperties = _.keys(set).concat(_.keys(unset));
|
||||||
|
modifiedProperties.forEach(function(fieldName) {
|
||||||
|
var field = schema[fieldName];
|
||||||
|
if (!field || !Users.canEditField(currentUser, field, document)) {
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.disallowed_property_detected',
|
||||||
|
data: {name: fieldName},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check validity of set modifier
|
||||||
|
_.forEach(set, (value, fieldName) => {
|
||||||
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
|
// 2. check field lengths
|
||||||
|
if (fieldSchema.limit && value.length > fieldSchema.limit) {
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.field_is_too_long',
|
||||||
|
data: { name: fieldName, limit: fieldSchema.limit },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. check that fields have the proper type
|
||||||
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. check that required fields have a value
|
// 4. check that required fields have a value
|
||||||
// when editing, we only want to require fields that are actually part of the form
|
// when editing, we only want to require fields that are actually part of the form
|
||||||
// so we make sure required keys are present in the $unset object
|
// so we make sure required keys are present in the $unset object
|
||||||
_.keys(schema).forEach(fieldName => {
|
_.keys(schema).forEach(fieldName => {
|
||||||
|
|
||||||
const fieldSchema = schema[fieldName];
|
const fieldSchema = schema[fieldName];
|
||||||
|
|
||||||
if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
|
if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
|
||||||
validationErrors.push({
|
validationErrors.push({
|
||||||
id: 'app.required_field_missing',
|
id: 'app.required_field_missing',
|
||||||
data: {fieldName}
|
data: { name: fieldName },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. still run SS validation for now for backwards compatibility
|
// 5. still run SS validation for now for backwards compatibility
|
||||||
try {
|
const validationContext = collection.simpleSchema().newContext();
|
||||||
collection.simpleSchema().validate({$set: set, $unset: unset}, { modifier: true });
|
validationContext.validate({ $set: set, $unset: unset }, { modifier: true });
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
if (!validationContext.isValid()) {
|
||||||
console.log(error);
|
const errors = validationContext.validationErrors();
|
||||||
validationErrors.push({
|
console.log('// validationContext');
|
||||||
id: 'app.schema_validation_error',
|
console.log(validationContext.isValid());
|
||||||
data: {message: error.message}
|
console.log(errors);
|
||||||
|
errors.forEach(error => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log(error);
|
||||||
|
validationErrors.push({
|
||||||
|
id: 'app.schema_validation_error',
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return validationErrors;
|
return validationErrors;
|
||||||
}
|
};
|
||||||
|
|
|
@ -130,9 +130,9 @@ export const editMutation = async ({ collection, documentId, set = {}, unset = {
|
||||||
debug('// editMutation');
|
debug('// editMutation');
|
||||||
debug('// collectionName: ', collection._name);
|
debug('// collectionName: ', collection._name);
|
||||||
debug('// documentId: ', documentId);
|
debug('// documentId: ', documentId);
|
||||||
// debug('// set: ', set);
|
debug('// set: ', set);
|
||||||
// debug('// unset: ', unset);
|
debug('// unset: ', unset);
|
||||||
// debug('// document: ', document);
|
debug('// document: ', document);
|
||||||
|
|
||||||
if (validate) {
|
if (validate) {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue