Validation (#216)
* Field config options: 'required' and 'pattern' * Widget controls can implement it's own isValid * Validation errors store in redux & displayed * Support for returned Promises in isValid * Allow widget controls to return either a boolean, an error object or a promise from isValid
This commit is contained in:
parent
b710e706da
commit
3306670459
@ -219,9 +219,14 @@ export function loadUnpublishedEntries() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistUnpublishedEntry(collection, entryDraft, existingUnpublishedEntry) {
|
export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const entryDraft = state.entryDraft;
|
||||||
|
|
||||||
|
// Early return if draft contains validation errors
|
||||||
|
if (!entryDraft.get('fieldsErrors').isEmpty()) return;
|
||||||
|
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
|
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
|
||||||
const entry = entryDraft.get('entry');
|
const entry = entryDraft.get('entry');
|
||||||
|
@ -24,6 +24,7 @@ export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY';
|
|||||||
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
|
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 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';
|
||||||
@ -141,6 +142,7 @@ export function createDraftFromEntry(entry) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function discardDraft() {
|
export function discardDraft() {
|
||||||
return {
|
return {
|
||||||
type: DRAFT_DISCARD,
|
type: DRAFT_DISCARD,
|
||||||
@ -161,6 +163,14 @@ export function changeDraftField(field, value, metadata) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeDraftFieldValidation(field, errors) {
|
||||||
|
return {
|
||||||
|
type: DRAFT_VALIDATION_ERRORS,
|
||||||
|
payload: { field, errors },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Exported Thunk Action Creators
|
* Exported Thunk Action Creators
|
||||||
*/
|
*/
|
||||||
@ -213,9 +223,14 @@ export function createEmptyDraft(collection) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistEntry(collection, entryDraft) {
|
export function persistEntry(collection) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const entryDraft = state.entryDraft;
|
||||||
|
|
||||||
|
// Early return if draft contains validation errors
|
||||||
|
if (!entryDraft.get('fieldsErrors').isEmpty()) return;
|
||||||
|
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
|
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
|
||||||
const entry = entryDraft.get('entry');
|
const entry = entryDraft.get('entry');
|
||||||
|
@ -19,12 +19,26 @@
|
|||||||
color: #7c8382;
|
color: #7c8382;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
color: #AAB0AF;
|
color: #AAB0AF;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelWithError {
|
||||||
|
composes: label;
|
||||||
|
color: #FF706F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errors {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #FF706F;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
border-bottom: 1px solid #e8eae8;
|
border-bottom: 1px solid #e8eae8;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { Map, fromJS } from 'immutable';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { resolveWidget } from '../Widgets';
|
import { resolveWidget } from '../Widgets';
|
||||||
|
import ControlHOC from '../Widgets/ControlHOC';
|
||||||
import styles from './ControlPane.css';
|
import styles from './ControlPane.css';
|
||||||
|
|
||||||
function isHidden(field) {
|
function isHidden(field) {
|
||||||
@ -8,28 +10,47 @@ function isHidden(field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class ControlPane extends Component {
|
export default class ControlPane extends Component {
|
||||||
|
componentValidate = {};
|
||||||
|
processControlRef(fieldName, wrappedControl) {
|
||||||
|
if (!wrappedControl) return;
|
||||||
|
this.componentValidate[fieldName] = wrappedControl.validate;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate = () => {
|
||||||
|
this.props.fields.forEach(field => this.componentValidate[field.get("name")]());
|
||||||
|
};
|
||||||
|
|
||||||
controlFor(field) {
|
controlFor(field) {
|
||||||
const { entry, fieldsMetaData, getAsset, onChange, onAddAsset, onRemoveAsset } = this.props;
|
const { entry, fieldsMetaData, fieldsErrors, getAsset, onChange, onAddAsset, onRemoveAsset } = this.props;
|
||||||
const widget = resolveWidget(field.get('widget'));
|
const widget = resolveWidget(field.get('widget'));
|
||||||
const fieldName = field.get('name');
|
const fieldName = field.get('name');
|
||||||
const value = entry.getIn(['data', fieldName]);
|
const value = entry.getIn(['data', fieldName]);
|
||||||
const metadata = fieldsMetaData.get(fieldName);
|
const metadata = fieldsMetaData.get(fieldName);
|
||||||
|
const errors = fieldsErrors.get(fieldName);
|
||||||
|
const labelClass = errors ? styles.labelWithError : styles.label;
|
||||||
if (entry.size === 0 || entry.get('partial') === true) return null;
|
if (entry.size === 0 || entry.get('partial') === true) return null;
|
||||||
return (
|
return (
|
||||||
<div className={styles.control}>
|
<div className={styles.control}>
|
||||||
<label className={styles.label} htmlFor={fieldName}>{field.get('label')}</label>
|
<label className={labelClass} htmlFor={fieldName}>{field.get('label')}</label>
|
||||||
|
<ul className={styles.errors}>
|
||||||
{
|
{
|
||||||
React.createElement(widget.control, {
|
errors && errors.map(error => (
|
||||||
field,
|
typeof error === 'string' && <li key={error.trim().replace(/[^a-z0-9]+/gi, '-')}>{error}</li>
|
||||||
value,
|
))
|
||||||
metadata,
|
|
||||||
onChange: (newValue, newMetadata) => onChange(fieldName, newValue, newMetadata),
|
|
||||||
onAddAsset,
|
|
||||||
onRemoveAsset,
|
|
||||||
getAsset,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
</ul>
|
||||||
|
<ControlHOC
|
||||||
|
controlComponent={widget.control}
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
metadata={metadata}
|
||||||
|
onChange={(newValue, newMetadata) => onChange(fieldName, newValue, newMetadata)}
|
||||||
|
onValidate={this.props.onValidate.bind(this, fieldName)}
|
||||||
|
onAddAsset={onAddAsset}
|
||||||
|
onRemoveAsset={onRemoveAsset}
|
||||||
|
getAsset={getAsset}
|
||||||
|
ref={this.processControlRef.bind(this, fieldName)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,8 +81,10 @@ ControlPane.propTypes = {
|
|||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
fields: ImmutablePropTypes.list.isRequired,
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
||||||
|
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
||||||
getAsset: PropTypes.func.isRequired,
|
getAsset: PropTypes.func.isRequired,
|
||||||
onAddAsset: PropTypes.func.isRequired,
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onValidate: PropTypes.func.isRequired,
|
||||||
onRemoveAsset: PropTypes.func.isRequired,
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -20,17 +20,23 @@ class EntryEditor extends Component {
|
|||||||
this.setState({ showEventBlocker: false });
|
this.setState({ showEventBlocker: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleOnPersist = () => {
|
||||||
|
this.controlPaneRef.validate();
|
||||||
|
this.props.onPersist();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
fields,
|
fields,
|
||||||
fieldsMetaData,
|
fieldsMetaData,
|
||||||
|
fieldsErrors,
|
||||||
getAsset,
|
getAsset,
|
||||||
onChange,
|
onChange,
|
||||||
|
onValidate,
|
||||||
onAddAsset,
|
onAddAsset,
|
||||||
onRemoveAsset,
|
onRemoveAsset,
|
||||||
onPersist,
|
|
||||||
onCancelEdit,
|
onCancelEdit,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -53,10 +59,13 @@ class EntryEditor extends Component {
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsMetaData={fieldsMetaData}
|
fieldsMetaData={fieldsMetaData}
|
||||||
|
fieldsErrors={fieldsErrors}
|
||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onValidate={onValidate}
|
||||||
onAddAsset={onAddAsset}
|
onAddAsset={onAddAsset}
|
||||||
onRemoveAsset={onRemoveAsset}
|
onRemoveAsset={onRemoveAsset}
|
||||||
|
ref={c => this.controlPaneRef = c} // eslint-disable-line
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +85,7 @@ class EntryEditor extends Component {
|
|||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
isPersisting={entry.get('isPersisting')}
|
isPersisting={entry.get('isPersisting')}
|
||||||
onPersist={onPersist}
|
onPersist={this.handleOnPersist}
|
||||||
onCancelEdit={onCancelEdit}
|
onCancelEdit={onCancelEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -91,9 +100,11 @@ EntryEditor.propTypes = {
|
|||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
fields: ImmutablePropTypes.list.isRequired,
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
||||||
|
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
||||||
getAsset: PropTypes.func.isRequired,
|
getAsset: PropTypes.func.isRequired,
|
||||||
onAddAsset: PropTypes.func.isRequired,
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onValidate: PropTypes.func.isRequired,
|
||||||
onPersist: PropTypes.func.isRequired,
|
onPersist: PropTypes.func.isRequired,
|
||||||
onRemoveAsset: PropTypes.func.isRequired,
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
onCancelEdit: PropTypes.func.isRequired,
|
onCancelEdit: PropTypes.func.isRequired,
|
||||||
|
92
src/components/Widgets/ControlHOC.js
Normal file
92
src/components/Widgets/ControlHOC.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||||
|
|
||||||
|
const truthy = () => ({ error: false });
|
||||||
|
|
||||||
|
class ControlHOC extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
controlComponent: PropTypes.func.isRequired,
|
||||||
|
field: ImmutablePropTypes.map.isRequired,
|
||||||
|
value: PropTypes.node,
|
||||||
|
metadata: ImmutablePropTypes.map,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onValidate: PropTypes.func.isRequired,
|
||||||
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
processInnerControlRef = (wrappedControl) => {
|
||||||
|
if (!wrappedControl) return;
|
||||||
|
this.wrappedControlValid = wrappedControl.isValid || truthy;
|
||||||
|
};
|
||||||
|
|
||||||
|
validate = (skipWrapped = false) => {
|
||||||
|
const { field, value } = this.props;
|
||||||
|
const errors = [];
|
||||||
|
const validations = [this.validatePresence, this.validatePattern];
|
||||||
|
validations.forEach((func) => {
|
||||||
|
const response = func(field, value);
|
||||||
|
if (response.error) errors.push(response.error);
|
||||||
|
});
|
||||||
|
if (skipWrapped) {
|
||||||
|
if (skipWrapped.error) errors.push(skipWrapped.error);
|
||||||
|
} else {
|
||||||
|
const wrappedError = this.validateWrappedControl(field);
|
||||||
|
if (wrappedError.error) errors.push(wrappedError.error);
|
||||||
|
}
|
||||||
|
this.props.onValidate(errors);
|
||||||
|
};
|
||||||
|
|
||||||
|
validatePresence(field, value) {
|
||||||
|
const isRequired = field.get('required', true);
|
||||||
|
if (isRequired && (value === null || value.length === 0)) {
|
||||||
|
return { error: true };
|
||||||
|
}
|
||||||
|
return { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePattern(field, value) {
|
||||||
|
const pattern = field.get('pattern', false);
|
||||||
|
if (pattern && !RegExp(pattern.first()).test(value)) {
|
||||||
|
return { error: `${ field.get('label', field.get('name')) } didn't match the pattern: ${ pattern.last() }` };
|
||||||
|
}
|
||||||
|
return { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
validateWrappedControl = (field) => {
|
||||||
|
const response = this.wrappedControlValid();
|
||||||
|
if (typeof response === "boolean") {
|
||||||
|
const isValid = response;
|
||||||
|
return { error: (!isValid) };
|
||||||
|
} else if (response.hasOwnProperty('error')) {
|
||||||
|
return response;
|
||||||
|
} else if (response instanceof Promise) {
|
||||||
|
response.then(
|
||||||
|
() => { this.validate({ error: false }); },
|
||||||
|
(error) => {
|
||||||
|
this.validate({ error: `${ field.get('label', field.get('name')) } - ${ error }.` });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { error: `${ field.get('label', field.get('name')) } is processing.` };
|
||||||
|
}
|
||||||
|
return { error: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { controlComponent, field, value, metadata, onChange, onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||||
|
return React.createElement(controlComponent, {
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
metadata,
|
||||||
|
onChange,
|
||||||
|
onAddAsset,
|
||||||
|
onRemoveAsset,
|
||||||
|
getAsset,
|
||||||
|
ref: this.processInnerControlRef,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlHOC;
|
@ -10,6 +10,16 @@ export default class FileControl extends React.Component {
|
|||||||
processing: false,
|
processing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
promise = null;
|
||||||
|
|
||||||
|
isValid = () => {
|
||||||
|
if (this.promise) {
|
||||||
|
return this.promise;
|
||||||
|
}
|
||||||
|
return { error: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
handleFileInputRef = (el) => {
|
handleFileInputRef = (el) => {
|
||||||
this._fileInput = el;
|
this._fileInput = el;
|
||||||
};
|
};
|
||||||
@ -42,7 +52,7 @@ export default class FileControl extends React.Component {
|
|||||||
this.props.onRemoveAsset(this.props.value);
|
this.props.onRemoveAsset(this.props.value);
|
||||||
if (file) {
|
if (file) {
|
||||||
this.setState({ processing: true });
|
this.setState({ processing: true });
|
||||||
createAssetProxy(file.name, file, false, this.props.field.get('private', false))
|
this.promise = createAssetProxy(file.name, file, false, this.props.field.get('private', false))
|
||||||
.then((assetProxy) => {
|
.then((assetProxy) => {
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
this.props.onAddAsset(assetProxy);
|
this.props.onAddAsset(assetProxy);
|
||||||
|
@ -10,6 +10,16 @@ export default class ImageControl extends React.Component {
|
|||||||
processing: false,
|
processing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
promise = null;
|
||||||
|
|
||||||
|
isValid = () => {
|
||||||
|
if (this.promise) {
|
||||||
|
return this.promise;
|
||||||
|
}
|
||||||
|
return { error: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
handleFileInputRef = (el) => {
|
handleFileInputRef = (el) => {
|
||||||
this._fileInput = el;
|
this._fileInput = el;
|
||||||
};
|
};
|
||||||
@ -46,7 +56,7 @@ export default class ImageControl extends React.Component {
|
|||||||
this.props.onRemoveAsset(this.props.value);
|
this.props.onRemoveAsset(this.props.value);
|
||||||
if (file) {
|
if (file) {
|
||||||
this.setState({ processing: true });
|
this.setState({ processing: true });
|
||||||
createAssetProxy(file.name, file)
|
this.promise = createAssetProxy(file.name, file)
|
||||||
.then((assetProxy) => {
|
.then((assetProxy) => {
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
this.props.onAddAsset(assetProxy);
|
this.props.onAddAsset(assetProxy);
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
createEmptyDraft,
|
createEmptyDraft,
|
||||||
discardDraft,
|
discardDraft,
|
||||||
changeDraftField,
|
changeDraftField,
|
||||||
|
changeDraftFieldValidation,
|
||||||
persistEntry,
|
persistEntry,
|
||||||
} from '../actions/entries';
|
} from '../actions/entries';
|
||||||
import { closeEntry } from '../actions/editor';
|
import { closeEntry } from '../actions/editor';
|
||||||
@ -23,6 +24,7 @@ class EntryPage extends React.Component {
|
|||||||
addAsset: PropTypes.func.isRequired,
|
addAsset: PropTypes.func.isRequired,
|
||||||
boundGetAsset: PropTypes.func.isRequired,
|
boundGetAsset: PropTypes.func.isRequired,
|
||||||
changeDraftField: PropTypes.func.isRequired,
|
changeDraftField: PropTypes.func.isRequired,
|
||||||
|
changeDraftFieldValidation: PropTypes.func.isRequired,
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
createDraftFromEntry: PropTypes.func.isRequired,
|
createDraftFromEntry: PropTypes.func.isRequired,
|
||||||
createEmptyDraft: PropTypes.func.isRequired,
|
createEmptyDraft: PropTypes.func.isRequired,
|
||||||
@ -72,8 +74,8 @@ class EntryPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handlePersistEntry = () => {
|
handlePersistEntry = () => {
|
||||||
const { persistEntry, collection, entryDraft } = this.props;
|
const { persistEntry, collection } = this.props;
|
||||||
persistEntry(collection, entryDraft);
|
persistEntry(collection);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -84,6 +86,7 @@ class EntryPage extends React.Component {
|
|||||||
boundGetAsset,
|
boundGetAsset,
|
||||||
collection,
|
collection,
|
||||||
changeDraftField,
|
changeDraftField,
|
||||||
|
changeDraftFieldValidation,
|
||||||
addAsset,
|
addAsset,
|
||||||
removeAsset,
|
removeAsset,
|
||||||
closeEntry,
|
closeEntry,
|
||||||
@ -104,7 +107,9 @@ class EntryPage extends React.Component {
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsMetaData={entryDraft.get('fieldsMetaData')}
|
fieldsMetaData={entryDraft.get('fieldsMetaData')}
|
||||||
|
fieldsErrors={entryDraft.get('fieldsErrors')}
|
||||||
onChange={changeDraftField}
|
onChange={changeDraftField}
|
||||||
|
onValidate={changeDraftFieldValidation}
|
||||||
onAddAsset={addAsset}
|
onAddAsset={addAsset}
|
||||||
onRemoveAsset={removeAsset}
|
onRemoveAsset={removeAsset}
|
||||||
onPersist={this.handlePersistEntry}
|
onPersist={this.handlePersistEntry}
|
||||||
@ -138,6 +143,7 @@ export default connect(
|
|||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
{
|
{
|
||||||
changeDraftField,
|
changeDraftField,
|
||||||
|
changeDraftFieldValidation,
|
||||||
addAsset,
|
addAsset,
|
||||||
removeAsset,
|
removeAsset,
|
||||||
loadEntry,
|
loadEntry,
|
||||||
|
@ -40,8 +40,8 @@ export default function EntryPageHOC(EntryPage) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Overwrite persistEntry to persistUnpublishedEntry
|
// Overwrite persistEntry to persistUnpublishedEntry
|
||||||
returnObj.persistEntry = (collection, entryDraft) => {
|
returnObj.persistEntry = (collection) => {
|
||||||
dispatch(persistUnpublishedEntry(collection, entryDraft, unpublishedEntry));
|
dispatch(persistUnpublishedEntry(collection, unpublishedEntry));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { Map, List, fromJS } from 'immutable';
|
|||||||
import * as actions from '../../actions/entries';
|
import * as actions from '../../actions/entries';
|
||||||
import reducer from '../entryDraft';
|
import reducer from '../entryDraft';
|
||||||
|
|
||||||
let initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
|
let initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map(), fieldsErrors: Map() });
|
||||||
|
|
||||||
const entry = {
|
const entry = {
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
@ -30,6 +30,7 @@ describe('entryDraft reducer', () => {
|
|||||||
},
|
},
|
||||||
mediaFiles: [],
|
mediaFiles: [],
|
||||||
fieldsMetaData: Map(),
|
fieldsMetaData: Map(),
|
||||||
|
fieldsErrors: Map(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -50,6 +51,7 @@ describe('entryDraft reducer', () => {
|
|||||||
},
|
},
|
||||||
mediaFiles: [],
|
mediaFiles: [],
|
||||||
fieldsMetaData: Map(),
|
fieldsMetaData: Map(),
|
||||||
|
fieldsErrors: Map(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
DRAFT_CREATE_EMPTY,
|
DRAFT_CREATE_EMPTY,
|
||||||
DRAFT_DISCARD,
|
DRAFT_DISCARD,
|
||||||
DRAFT_CHANGE_FIELD,
|
DRAFT_CHANGE_FIELD,
|
||||||
|
DRAFT_VALIDATION_ERRORS,
|
||||||
ENTRY_PERSIST_REQUEST,
|
ENTRY_PERSIST_REQUEST,
|
||||||
ENTRY_PERSIST_SUCCESS,
|
ENTRY_PERSIST_SUCCESS,
|
||||||
ENTRY_PERSIST_FAILURE,
|
ENTRY_PERSIST_FAILURE,
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
REMOVE_ASSET,
|
REMOVE_ASSET,
|
||||||
} from '../actions/media';
|
} from '../actions/media';
|
||||||
|
|
||||||
const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
|
const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map(), fieldsErrors: Map() });
|
||||||
|
|
||||||
const entryDraftReducer = (state = Map(), action) => {
|
const entryDraftReducer = (state = Map(), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -29,6 +30,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
|||||||
state.setIn(['entry', 'newRecord'], false);
|
state.setIn(['entry', 'newRecord'], false);
|
||||||
state.set('mediaFiles', List());
|
state.set('mediaFiles', List());
|
||||||
state.set('fieldsMetaData', Map());
|
state.set('fieldsMetaData', Map());
|
||||||
|
state.set('fieldsErrors', Map());
|
||||||
});
|
});
|
||||||
case DRAFT_CREATE_EMPTY:
|
case DRAFT_CREATE_EMPTY:
|
||||||
// New Entry
|
// New Entry
|
||||||
@ -37,6 +39,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
|||||||
state.setIn(['entry', 'newRecord'], true);
|
state.setIn(['entry', 'newRecord'], true);
|
||||||
state.set('mediaFiles', List());
|
state.set('mediaFiles', List());
|
||||||
state.set('fieldsMetaData', Map());
|
state.set('fieldsMetaData', Map());
|
||||||
|
state.set('fieldsErrors', Map());
|
||||||
});
|
});
|
||||||
case DRAFT_DISCARD:
|
case DRAFT_DISCARD:
|
||||||
return initialState;
|
return initialState;
|
||||||
@ -45,6 +48,14 @@ const entryDraftReducer = (state = Map(), action) => {
|
|||||||
state.setIn(['entry', 'data', action.payload.field], action.payload.value);
|
state.setIn(['entry', 'data', action.payload.field], action.payload.value);
|
||||||
state.mergeIn(['fieldsMetaData'], fromJS(action.payload.metadata));
|
state.mergeIn(['fieldsMetaData'], fromJS(action.payload.metadata));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case DRAFT_VALIDATION_ERRORS:
|
||||||
|
if (action.payload.errors.length === 0) {
|
||||||
|
return state.deleteIn(['fieldsErrors', action.payload.field]);
|
||||||
|
} else {
|
||||||
|
return state.setIn(['fieldsErrors', action.payload.field], action.payload.errors);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user