fix(netlify-cms-core): validate nested fields (#1873)

This commit is contained in:
Bartholomew 2019-02-05 23:27:34 +01:00 committed by Shawn Erquhart
parent ade5809dff
commit 627e600d29
10 changed files with 183 additions and 22 deletions

View File

@ -4,11 +4,14 @@ describe('Editorial Workflow', () => {
const entry1 = { title: 'first title', body: 'first body' } const entry1 = { title: 'first title', body: 'first body' }
const entry2 = { title: 'second title', body: 'second body' } const entry2 = { title: 'second title', body: 'second body' }
const entry3 = { title: 'third title', body: 'third body' } const entry3 = { title: 'third title', body: 'third body' }
const setting1 = { limit: 10, author: 'John Doe' }
const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' }
const notifications = { const notifications = {
saved: 'Entry saved', saved: 'Entry saved',
published: 'Entry published', published: 'Entry published',
updated: 'Entry status updated', updated: 'Entry status updated',
deletedUnpublished: 'Unpublished changes deleted', deletedUnpublished: 'Unpublished changes deleted',
error: { missingField: 'Oops, you\'ve missed a required field. Please complete before saving.' }
} }
describe('Test Backend', () => { describe('Test Backend', () => {
@ -27,6 +30,29 @@ describe('Editorial Workflow', () => {
assertNotification(notifications.saved) assertNotification(notifications.saved)
} }
function validateObjectFields({ limit, author }) {
cy.get('a[href^="#/collections/settings"]').click()
cy.get('a[href^="#/collections/settings/entries/general"]').click()
cy.get('input[type=number]').type(limit);
cy.contains('button', 'Save').click()
assertNotification(notifications.error.missingField)
cy.contains('label', 'Default Author').click()
cy.focused().type(author)
cy.contains('button', 'Save').click()
assertNotification(notifications.saved)
}
function validateListFields({ name, description }) {
cy.get('a[href^="#/collections/settings"]').click()
cy.get('a[href^="#/collections/settings/entries/authors"]').click()
cy.contains('button', 'Add').click()
cy.contains('button', 'Save').click()
assertNotification(notifications.error.missingField)
cy.get('input').eq(2).type(name)
cy.get('[data-slate-editor]').eq(2).type(description)
cy.contains('button', 'Save').click()
}
function exitEditor() { function exitEditor() {
cy.contains('a[href^="#/collections/"]', 'Writing in').click() cy.contains('a[href^="#/collections/"]', 'Writing in').click()
} }
@ -70,6 +96,16 @@ describe('Editorial Workflow', () => {
exitEditor() exitEditor()
} }
function validateObjectFieldsAndExit(setting) {
validateObjectFields(setting)
exitEditor()
}
function validateListFieldsAndExit(setting) {
validateListFields(setting)
exitEditor()
}
function goToWorkflow() { function goToWorkflow() {
cy.contains('a', 'Workflow').click() cy.contains('a', 'Workflow').click()
} }
@ -172,6 +208,16 @@ describe('Editorial Workflow', () => {
createPostAndExit(entry1) createPostAndExit(entry1)
}) })
it('can validate object fields', () => {
login()
validateObjectFieldsAndExit(setting1)
})
it('can validate list fields', () => {
login()
validateListFieldsAndExit(setting2)
})
it('can publish an editorial workflow entry', () => { it('can publish an editorial workflow entry', () => {
login() login()
createPostAndExit(entry1) createPostAndExit(entry1)

View File

@ -62,7 +62,7 @@ collections: # A list of collections the CMS should be able to edit
fields: fields:
- { label: 'Number of posts on frontpage', name: front_limit, widget: number } - { label: 'Number of posts on frontpage', name: front_limit, widget: number }
- { label: 'Default Author', name: author, widget: string } - { label: 'Default Author', name: author, widget: string }
- { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb' } - { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb', required: false }
- name: 'authors' - name: 'authors'
label: 'Authors' label: 'Authors'

View File

@ -29,6 +29,7 @@ export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE'; export const DRAFT_CHANGE = 'DRAFT_CHANGE';
export const DRAFT_CHANGE_FIELD = 'DRAFT_CHANGE_FIELD'; export const DRAFT_CHANGE_FIELD = 'DRAFT_CHANGE_FIELD';
export const DRAFT_VALIDATION_ERRORS = 'DRAFT_VALIDATION_ERRORS'; export const DRAFT_VALIDATION_ERRORS = 'DRAFT_VALIDATION_ERRORS';
export const DRAFT_CLEAR_ERRORS = 'DRAFT_CLEAR_ERRORS';
export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
@ -208,13 +209,17 @@ export function changeDraftField(field, value, metadata) {
}; };
} }
export function changeDraftFieldValidation(field, errors) { export function changeDraftFieldValidation(uniquefieldId, errors) {
return { return {
type: DRAFT_VALIDATION_ERRORS, type: DRAFT_VALIDATION_ERRORS,
payload: { field, errors }, payload: { uniquefieldId, errors },
}; };
} }
export function clearFieldErrors() {
return { type: DRAFT_CLEAR_ERRORS };
}
/* /*
* Exported Thunk Action Creators * Exported Thunk Action Creators
*/ */

View File

@ -7,6 +7,7 @@ import { partial, uniqueId } from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { colors, colorsRaw, transitions, lengths, borders } from 'netlify-cms-ui-default'; import { colors, colorsRaw, transitions, lengths, borders } from 'netlify-cms-ui-default';
import { resolveWidget, getEditorComponents } from 'Lib/registry'; import { resolveWidget, getEditorComponents } from 'Lib/registry';
import { clearFieldErrors } from 'Actions/entries';
import { addAsset } from 'Actions/media'; import { addAsset } from 'Actions/media';
import { query, clearSearch } from 'Actions/search'; import { query, clearSearch } from 'Actions/search';
import { loadEntry } from 'Actions/entries'; import { loadEntry } from 'Actions/entries';
@ -140,10 +141,12 @@ class EditorControl extends React.Component {
removeInsertedMedia: PropTypes.func.isRequired, removeInsertedMedia: PropTypes.func.isRequired,
onValidate: PropTypes.func, onValidate: PropTypes.func,
processControlRef: PropTypes.func, processControlRef: PropTypes.func,
controlRef: PropTypes.func,
query: PropTypes.func.isRequired, query: PropTypes.func.isRequired,
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
clearSearch: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
loadEntry: PropTypes.func.isRequired, loadEntry: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };
@ -170,10 +173,12 @@ class EditorControl extends React.Component {
removeInsertedMedia, removeInsertedMedia,
onValidate, onValidate,
processControlRef, processControlRef,
controlRef,
query, query,
queryHits, queryHits,
isFetching, isFetching,
clearSearch, clearSearch,
clearFieldErrors,
loadEntry, loadEntry,
t, t,
} = this.props; } = this.props;
@ -182,8 +187,9 @@ class EditorControl extends React.Component {
const fieldName = field.get('name'); const fieldName = field.get('name');
const fieldHint = field.get('hint'); const fieldHint = field.get('hint');
const isFieldOptional = field.get('required') === false; const isFieldOptional = field.get('required') === false;
const onValidateObject = onValidate;
const metadata = fieldsMetaData && fieldsMetaData.get(fieldName); const metadata = fieldsMetaData && fieldsMetaData.get(fieldName);
const errors = fieldsErrors && fieldsErrors.get(fieldName); const errors = fieldsErrors && fieldsErrors.get(this.uniqueFieldId);
return ( return (
<ControlContainer> <ControlContainer>
<ControlErrorsList> <ControlErrorsList>
@ -223,7 +229,7 @@ class EditorControl extends React.Component {
mediaPaths={mediaPaths} mediaPaths={mediaPaths}
metadata={metadata} metadata={metadata}
onChange={(newValue, newMetadata) => onChange(fieldName, newValue, newMetadata)} onChange={(newValue, newMetadata) => onChange(fieldName, newValue, newMetadata)}
onValidate={onValidate && partial(onValidate, fieldName)} onValidate={onValidate && partial(onValidate, this.uniqueFieldId)}
onOpenMediaLibrary={openMediaLibrary} onOpenMediaLibrary={openMediaLibrary}
onClearMediaControl={clearMediaControl} onClearMediaControl={clearMediaControl}
onRemoveMediaControl={removeMediaControl} onRemoveMediaControl={removeMediaControl}
@ -235,13 +241,17 @@ class EditorControl extends React.Component {
setInactiveStyle={() => this.setState({ styleActive: false })} setInactiveStyle={() => this.setState({ styleActive: false })}
resolveWidget={resolveWidget} resolveWidget={resolveWidget}
getEditorComponents={getEditorComponents} getEditorComponents={getEditorComponents}
ref={processControlRef && partial(processControlRef, fieldName)} ref={processControlRef && partial(processControlRef, field)}
controlRef={controlRef}
editorControl={ConnectedEditorControl} editorControl={ConnectedEditorControl}
query={query} query={query}
loadEntry={loadEntry} loadEntry={loadEntry}
queryHits={queryHits} queryHits={queryHits}
clearSearch={clearSearch} clearSearch={clearSearch}
clearFieldErrors={clearFieldErrors}
isFetching={isFetching} isFetching={isFetching}
fieldsErrors={fieldsErrors}
onValidateObject={onValidateObject}
t={t} t={t}
/> />
{fieldHint && ( {fieldHint && (
@ -273,6 +283,7 @@ const mapDispatchToProps = {
return loadEntry(collection, slug)(dispatch, getState); return loadEntry(collection, slug)(dispatch, getState);
}, },
clearSearch, clearSearch,
clearFieldErrors,
}; };
const ConnectedEditorControl = connect( const ConnectedEditorControl = connect(

View File

@ -17,10 +17,16 @@ const ControlPaneContainer = styled.div`
export default class ControlPane extends React.Component { export default class ControlPane extends React.Component {
componentValidate = {}; componentValidate = {};
processControlRef = (fieldName, wrappedControl) => { controlRef(field, wrappedControl) {
if (!wrappedControl) return; if (!wrappedControl) return;
this.componentValidate[fieldName] = wrappedControl.validate; const name = field.get('name');
}; const widget = field.get('widget');
if (widget === 'list' || widget === 'object') {
this.componentValidate[name] = wrappedControl.innerWrappedControl.validate;
} else {
this.componentValidate[name] = wrappedControl.validate;
}
}
validate = () => { validate = () => {
this.props.fields.forEach(field => { this.props.fields.forEach(field => {
@ -61,7 +67,8 @@ export default class ControlPane extends React.Component {
fieldsErrors={fieldsErrors} fieldsErrors={fieldsErrors}
onChange={onChange} onChange={onChange}
onValidate={onValidate} onValidate={onValidate}
processControlRef={this.processControlRef} processControlRef={this.controlRef.bind(this)}
controlRef={this.controlRef}
/> />
), ),
)} )}

View File

@ -33,6 +33,7 @@ export default class Widget extends Component {
]), ]),
mediaPaths: ImmutablePropTypes.map.isRequired, mediaPaths: ImmutablePropTypes.map.isRequired,
metadata: ImmutablePropTypes.map, metadata: ImmutablePropTypes.map,
fieldsErrors: ImmutablePropTypes.map,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onValidate: PropTypes.func, onValidate: PropTypes.func,
onOpenMediaLibrary: PropTypes.func.isRequired, onOpenMediaLibrary: PropTypes.func.isRequired,
@ -44,8 +45,10 @@ export default class Widget extends Component {
resolveWidget: PropTypes.func.isRequired, resolveWidget: PropTypes.func.isRequired,
getEditorComponents: PropTypes.func.isRequired, getEditorComponents: PropTypes.func.isRequired,
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
controlRef: PropTypes.func,
query: PropTypes.func.isRequired, query: PropTypes.func.isRequired,
clearSearch: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
editorControl: PropTypes.func.isRequired, editorControl: PropTypes.func.isRequired,
uniqueFieldId: PropTypes.string.isRequired, uniqueFieldId: PropTypes.string.isRequired,
@ -77,16 +80,16 @@ export default class Widget extends Component {
* `getWrappedInstance` method. Note that connected widgets must pass * `getWrappedInstance` method. Note that connected widgets must pass
* `withRef: true` to `connect` in the options object. * `withRef: true` to `connect` in the options object.
*/ */
const wrappedControl = ref.getWrappedInstance ? ref.getWrappedInstance() : ref; this.innerWrappedControl = ref.getWrappedInstance ? ref.getWrappedInstance() : ref;
this.wrappedControlValid = wrappedControl.isValid || truthy; this.wrappedControlValid = this.innerWrappedControl.isValid || truthy;
/** /**
* Get the `shouldComponentUpdate` method from the wrapped control, and * Get the `shouldComponentUpdate` method from the wrapped control, and
* provide the control instance is the `this` binding. * provide the control instance is the `this` binding.
*/ */
const { shouldComponentUpdate: scu } = wrappedControl; const { shouldComponentUpdate: scu } = this.innerWrappedControl;
this.wrappedControlShouldComponentUpdate = scu && scu.bind(wrappedControl); this.wrappedControlShouldComponentUpdate = scu && scu.bind(this.innerWrappedControl);
}; };
validate = (skipWrapped = false) => { validate = (skipWrapped = false) => {
@ -203,6 +206,7 @@ export default class Widget extends Component {
mediaPaths, mediaPaths,
metadata, metadata,
onChange, onChange,
onValidateObject,
onOpenMediaLibrary, onOpenMediaLibrary,
onRemoveMediaControl, onRemoveMediaControl,
onClearMediaControl, onClearMediaControl,
@ -224,8 +228,11 @@ export default class Widget extends Component {
query, query,
queryHits, queryHits,
clearSearch, clearSearch,
clearFieldErrors,
isFetching, isFetching,
loadEntry, loadEntry,
fieldsErrors,
controlRef,
t, t,
} = this.props; } = this.props;
return React.createElement(controlComponent, { return React.createElement(controlComponent, {
@ -235,6 +242,7 @@ export default class Widget extends Component {
metadata, metadata,
onChange, onChange,
onChangeObject: this.onChangeObject, onChangeObject: this.onChangeObject,
onValidateObject,
onOpenMediaLibrary, onOpenMediaLibrary,
onClearMediaControl, onClearMediaControl,
onRemoveMediaControl, onRemoveMediaControl,
@ -257,8 +265,11 @@ export default class Widget extends Component {
query, query,
queryHits, queryHits,
clearSearch, clearSearch,
clearFieldErrors,
isFetching, isFetching,
loadEntry, loadEntry,
fieldsErrors,
controlRef,
t, t,
}); });
} }

View File

@ -5,6 +5,7 @@ import {
DRAFT_DISCARD, DRAFT_DISCARD,
DRAFT_CHANGE_FIELD, DRAFT_CHANGE_FIELD,
DRAFT_VALIDATION_ERRORS, DRAFT_VALIDATION_ERRORS,
DRAFT_CLEAR_ERRORS,
ENTRY_PERSIST_REQUEST, ENTRY_PERSIST_REQUEST,
ENTRY_PERSIST_SUCCESS, ENTRY_PERSIST_SUCCESS,
ENTRY_PERSIST_FAILURE, ENTRY_PERSIST_FAILURE,
@ -61,11 +62,15 @@ const entryDraftReducer = (state = Map(), action) => {
case DRAFT_VALIDATION_ERRORS: case DRAFT_VALIDATION_ERRORS:
if (action.payload.errors.length === 0) { if (action.payload.errors.length === 0) {
return state.deleteIn(['fieldsErrors', action.payload.field]); return state.deleteIn(['fieldsErrors', action.payload.uniquefieldId]);
} else { } else {
return state.setIn(['fieldsErrors', action.payload.field], action.payload.errors); return state.setIn(['fieldsErrors', action.payload.uniquefieldId], action.payload.errors);
} }
case DRAFT_CLEAR_ERRORS: {
return state.set('fieldsErrors', Map());
}
case ENTRY_PERSIST_REQUEST: case ENTRY_PERSIST_REQUEST:
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: { case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
return state.setIn(['entry', 'isPersisting'], true); return state.setIn(['entry', 'isPersisting'], true);

View File

@ -68,13 +68,17 @@ const valueTypes = {
}; };
export default class ListControl extends React.Component { export default class ListControl extends React.Component {
validations = [];
static propTypes = { static propTypes = {
metadata: ImmutablePropTypes.map, metadata: ImmutablePropTypes.map,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onChangeObject: PropTypes.func.isRequired, onChangeObject: PropTypes.func.isRequired,
onValidateObject: PropTypes.func.isRequired,
value: ImmutablePropTypes.list, value: ImmutablePropTypes.list,
field: PropTypes.object, field: PropTypes.object,
forID: PropTypes.string, forID: PropTypes.string,
controlRef: PropTypes.func,
mediaPaths: ImmutablePropTypes.map.isRequired, mediaPaths: ImmutablePropTypes.map.isRequired,
getAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onOpenMediaLibrary: PropTypes.func.isRequired, onOpenMediaLibrary: PropTypes.func.isRequired,
@ -85,6 +89,8 @@ export default class ListControl extends React.Component {
setInactiveStyle: PropTypes.func.isRequired, setInactiveStyle: PropTypes.func.isRequired,
editorControl: PropTypes.func.isRequired, editorControl: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired, resolveWidget: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
fieldsErrors: ImmutablePropTypes.map.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -168,6 +174,17 @@ export default class ListControl extends React.Component {
onChange((value || List()).push(parsedValue)); onChange((value || List()).push(parsedValue));
}; };
processControlRef = ref => {
if (!ref) return;
this.validations.push(ref.validate);
};
validate = () => {
this.validations.forEach(validateListItem => {
validateListItem();
});
};
/** /**
* In case the `onChangeObject` function is frozen by a child widget implementation, * In case the `onChangeObject` function is frozen by a child widget implementation,
* e.g. when debounced, always get the latest object value instead of using * e.g. when debounced, always get the latest object value instead of using
@ -196,16 +213,25 @@ export default class ListControl extends React.Component {
handleRemove = (index, event) => { handleRemove = (index, event) => {
event.preventDefault(); event.preventDefault();
const { itemsCollapsed } = this.state; const { itemsCollapsed } = this.state;
const { value, metadata, onChange, field } = this.props; const { value, metadata, onChange, field, clearFieldErrors } = this.props;
const collectionName = field.get('name'); const collectionName = field.get('name');
const isSingleField = this.getValueType() === valueTypes.SINGLE; const isSingleField = this.getValueType() === valueTypes.SINGLE;
const metadataRemovePath = isSingleField ? value.get(index) : value.get(index).valueSeq(); const metadataRemovePath = isSingleField ? value.get(index) : value.get(index).valueSeq();
const parsedMetadata = metadata && { [collectionName]: metadata.removeIn(metadataRemovePath) }; const parsedMetadata = metadata && { [collectionName]: metadata.removeIn(metadataRemovePath) };
// Removed item object index is the last item in the list
const removedItemIndex = value.count() - 1;
this.setState({ itemsCollapsed: itemsCollapsed.delete(index) }); this.setState({ itemsCollapsed: itemsCollapsed.delete(index) });
onChange(value.remove(index), parsedMetadata); onChange(value.remove(index), parsedMetadata);
clearFieldErrors();
// Remove deleted item object validation
if (this.validations) {
this.validations.splice(removedItemIndex, 1);
}
}; };
handleItemCollapseToggle = (index, event) => { handleItemCollapseToggle = (index, event) => {
@ -253,7 +279,16 @@ export default class ListControl extends React.Component {
}; };
renderItem = (item, index) => { renderItem = (item, index) => {
const { classNameWrapper, editorControl, resolveWidget } = this.props; const {
classNameWrapper,
editorControl,
onValidateObject,
clearFieldErrors,
fieldsErrors,
controlRef,
resolveWidget,
} = this.props;
const { itemsCollapsed } = this.state; const { itemsCollapsed } = this.state;
const collapsed = itemsCollapsed.get(index); const collapsed = itemsCollapsed.get(index);
let field = this.props.field; let field = this.props.field;
@ -286,6 +321,11 @@ export default class ListControl extends React.Component {
editorControl={editorControl} editorControl={editorControl}
resolveWidget={resolveWidget} resolveWidget={resolveWidget}
forList forList
onValidateObject={onValidateObject}
clearFieldErrors={clearFieldErrors}
fieldsErrors={fieldsErrors}
ref={this.processControlRef}
controlRef={controlRef}
/> />
</SortableListItem> </SortableListItem>
); );

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { css, cx } from 'react-emotion'; import { css, cx } from 'react-emotion';
import { Map } from 'immutable'; import { Map, List } from 'immutable';
import { ObjectWidgetTopBar, components } from 'netlify-cms-ui-default'; import { ObjectWidgetTopBar, components } from 'netlify-cms-ui-default';
const styles = { const styles = {
@ -14,15 +15,21 @@ const styles = {
}; };
export default class ObjectControl extends Component { export default class ObjectControl extends Component {
componentValidate = {};
static propTypes = { static propTypes = {
onChangeObject: PropTypes.func.isRequired, onChangeObject: PropTypes.func.isRequired,
onValidateObject: PropTypes.func.isRequired,
value: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.bool]), value: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.bool]),
field: PropTypes.object, field: PropTypes.object,
forID: PropTypes.string, forID: PropTypes.string,
classNameWrapper: PropTypes.string.isRequired, classNameWrapper: PropTypes.string.isRequired,
forList: PropTypes.bool, forList: PropTypes.bool,
controlRef: PropTypes.func,
editorControl: PropTypes.func.isRequired, editorControl: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired, resolveWidget: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
fieldsErrors: ImmutablePropTypes.map.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -46,8 +53,26 @@ export default class ObjectControl extends Component {
return true; return true;
} }
validate = () => {
const { field } = this.props;
let fields = field.get('field') || field.get('fields');
fields = List.isList(fields) ? fields : List([fields]);
fields.forEach(field => {
if (field.get('widget') === 'hidden') return;
this.componentValidate[field.get('name')]();
});
};
controlFor(field, key) { controlFor(field, key) {
const { value, onChangeObject, editorControl: EditorControl } = this.props; const {
value,
onChangeObject,
onValidateObject,
clearFieldErrors,
fieldsErrors,
editorControl: EditorControl,
controlRef,
} = this.props;
if (field.get('widget') === 'hidden') { if (field.get('widget') === 'hidden') {
return null; return null;
@ -55,7 +80,19 @@ export default class ObjectControl extends Component {
const fieldName = field.get('name'); const fieldName = field.get('name');
const fieldValue = value && Map.isMap(value) ? value.get(fieldName) : value; const fieldValue = value && Map.isMap(value) ? value.get(fieldName) : value;
return <EditorControl key={key} field={field} value={fieldValue} onChange={onChangeObject} />; return (
<EditorControl
key={key}
field={field}
value={fieldValue}
onChange={onChangeObject}
clearFieldErrors={clearFieldErrors}
fieldsErrors={fieldsErrors}
onValidate={onValidateObject}
processControlRef={controlRef && controlRef.bind(this)}
controlRef={controlRef}
/>
);
} }
handleCollapseToggle = () => { handleCollapseToggle = () => {

View File

@ -25,6 +25,5 @@ The following options are available on all fields:
widget: "string" widget: "string"
pattern: [".{12,}", "Must have at least 12 characters"] pattern: [".{12,}", "Must have at least 12 characters"]
``` ```
- **Note:** Currently validation doesn't work on nested fields
## Default widgets ## Default widgets