265 lines
7.7 KiB
JavaScript
265 lines
7.7 KiB
JavaScript
import React, { PropTypes, Component } from 'react';
|
|
import { connect } from 'react-redux';
|
|
import { bindActionCreators } from 'redux';
|
|
import fetch from 'isomorphic-fetch';
|
|
import {
|
|
Field as ReduxFormField,
|
|
reduxForm,
|
|
SubmissionError,
|
|
destroy as reduxDestroyForm,
|
|
} from 'redux-form';
|
|
import * as schemaActions from 'state/schema/SchemaActions';
|
|
import merge from 'merge';
|
|
import Form from 'components/Form/Form';
|
|
import FormBuilder, { basePropTypes, schemaPropType } from 'components/FormBuilder/FormBuilder';
|
|
|
|
class FormBuilderLoader extends Component {
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
this.clearSchema = this.clearSchema.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.fetch();
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.schemaUrl !== prevProps.schemaUrl) {
|
|
this.clearSchema(prevProps.schemaUrl);
|
|
this.fetch();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.clearSchema(this.props.schemaUrl);
|
|
}
|
|
|
|
/**
|
|
* Get server-side validation messages returned and display them on the form.
|
|
*
|
|
* @param state
|
|
* @returns {object}
|
|
*/
|
|
getMessages(state) {
|
|
const messages = {};
|
|
|
|
// only error messages are collected
|
|
// TODO define message type as standard "success", "info", "warning" and "danger"
|
|
if (state && state.fields) {
|
|
state.fields.forEach((field) => {
|
|
if (field.message) {
|
|
messages[field.name] = field.message;
|
|
}
|
|
});
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
clearSchema(schemaUrl) {
|
|
if (schemaUrl) {
|
|
// we will reload the schema anyway when we mount again, this is here so that redux-form
|
|
// doesn't preload previous data mistakenly. (since it only accepts initialised values)
|
|
reduxDestroyForm(schemaUrl);
|
|
this.props.schemaActions.setSchema(schemaUrl, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles updating the schema after response is received and gathering server-side validation
|
|
* messages.
|
|
*
|
|
* @param {object} data
|
|
* @param {string} action
|
|
* @param {function} submitFn
|
|
* @returns {Promise}
|
|
*/
|
|
handleSubmit(data, action, submitFn) {
|
|
let promise = null;
|
|
if (typeof this.props.handleSubmit === 'function') {
|
|
promise = this.props.handleSubmit(data, action, submitFn);
|
|
} else {
|
|
promise = submitFn();
|
|
}
|
|
|
|
if (!promise) {
|
|
throw new Error('Promise was not returned for submitting');
|
|
}
|
|
|
|
return promise
|
|
.then(formSchema => {
|
|
if (formSchema) {
|
|
this.props.schemaActions.setSchema(this.props.schemaUrl, formSchema);
|
|
}
|
|
return formSchema;
|
|
})
|
|
// TODO Suggest storing messages in a separate redux store rather than throw an error
|
|
// ref: https://github.com/erikras/redux-form/issues/94#issuecomment-143398399
|
|
.then(formSchema => {
|
|
if (!formSchema || !formSchema.state) {
|
|
return formSchema;
|
|
}
|
|
const messages = this.getMessages(formSchema.state);
|
|
|
|
if (Object.keys(messages).length) {
|
|
throw new SubmissionError(messages);
|
|
}
|
|
return formSchema;
|
|
});
|
|
}
|
|
|
|
overrideStateData(state) {
|
|
if (!this.props.stateOverrides || !state) {
|
|
return state;
|
|
}
|
|
const fieldOverrides = this.props.stateOverrides.fields;
|
|
let fields = state.fields;
|
|
if (fieldOverrides && fields) {
|
|
fields = fields.map((field) => {
|
|
const fieldOverride = fieldOverrides.find((override) => override.name === field.name);
|
|
if (!fieldOverride) {
|
|
return field;
|
|
}
|
|
// need to be recursive for the unknown-sized "data" properly
|
|
return merge.recursive(true, field, fieldOverride);
|
|
});
|
|
}
|
|
return Object.assign({},
|
|
state,
|
|
this.props.stateOverrides,
|
|
{ fields }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks for any state override data provided, which will take precendence over the state
|
|
* received through fetch.
|
|
*
|
|
* This is important for editing a WYSIWYG item which needs the form schema and only parts of
|
|
* the form state.
|
|
*
|
|
* @param {object} state
|
|
* @returns {object}
|
|
*/
|
|
overrideStateData(state) {
|
|
if (!this.props.stateOverrides || !state) {
|
|
return state;
|
|
}
|
|
const fieldOverrides = this.props.stateOverrides.fields;
|
|
let fields = state.fields;
|
|
if (fieldOverrides && fields) {
|
|
fields = fields.map((field) => {
|
|
const fieldOverride = fieldOverrides.find((override) => override.name === field.name);
|
|
// need to be recursive for the unknown-sized "data" properly
|
|
return (fieldOverride) ? merge.recursive(true, field, fieldOverride) : field;
|
|
});
|
|
}
|
|
|
|
return Object.assign({},
|
|
state,
|
|
this.props.stateOverrides,
|
|
{ fields }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fetches data used to generate a form. This can be form schema and or form state data.
|
|
* When the response comes back the data is saved to state.
|
|
*
|
|
* @param {Boolean} schema If form schema data should be returned in the response.
|
|
* @param {Boolean} state If form state data should be returned in the response.
|
|
* @return {Object} Promise from the AJAX request.
|
|
*/
|
|
fetch(schema = true, state = true) {
|
|
const headerValues = [];
|
|
|
|
if (schema) {
|
|
headerValues.push('schema');
|
|
}
|
|
|
|
if (state) {
|
|
headerValues.push('state');
|
|
}
|
|
|
|
if (this.props.loading) {
|
|
return Promise.resolve({});
|
|
}
|
|
|
|
// using `this.state.fetching` caused race-condition issues.
|
|
this.props.schemaActions.setSchemaLoading(this.props.schemaUrl, true);
|
|
|
|
return fetch(this.props.schemaUrl, {
|
|
headers: { 'X-FormSchema-Request': headerValues.join() },
|
|
credentials: 'same-origin',
|
|
})
|
|
.then(response => response.json())
|
|
.then(formSchema => {
|
|
this.props.schemaActions.setSchemaLoading(this.props.schemaUrl, false);
|
|
|
|
if (typeof formSchema.id !== 'undefined') {
|
|
const overriddenSchema = Object.assign({},
|
|
formSchema,
|
|
{ state: this.overrideStateData(formSchema.state) }
|
|
);
|
|
this.props.schemaActions.setSchema(this.props.schemaUrl, overriddenSchema);
|
|
|
|
return overriddenSchema;
|
|
}
|
|
return formSchema;
|
|
});
|
|
}
|
|
|
|
render() {
|
|
// If the response from fetching the initial data
|
|
// hasn't come back yet, don't render anything.
|
|
if (!this.props.schema || !this.props.schema.schema || this.props.loading) {
|
|
return null;
|
|
}
|
|
|
|
const props = Object.assign({}, this.props, {
|
|
form: this.props.schemaUrl,
|
|
onSubmitSuccess: this.props.onSubmitSuccess,
|
|
handleSubmit: this.handleSubmit,
|
|
});
|
|
return <FormBuilder {...props} />;
|
|
}
|
|
}
|
|
|
|
FormBuilderLoader.propTypes = Object.assign({}, basePropTypes, {
|
|
schemaActions: PropTypes.object.isRequired,
|
|
schemaUrl: PropTypes.string.isRequired,
|
|
schema: schemaPropType,
|
|
form: PropTypes.string,
|
|
submitting: PropTypes.bool,
|
|
});
|
|
|
|
FormBuilderLoader.defaultProps = {
|
|
// Perform this *outside* of render() to avoid re-rendering of the whole DOM structure
|
|
// every time render() is triggered.
|
|
baseFormComponent: reduxForm()(Form),
|
|
baseFieldComponent: ReduxFormField,
|
|
};
|
|
|
|
function mapStateToProps(state, ownProps) {
|
|
const schema = state.schemas[ownProps.schemaUrl];
|
|
|
|
const reduxFormState = state.form && state.form[ownProps.schemaUrl];
|
|
const submitting = reduxFormState && reduxFormState.submitting;
|
|
const values = reduxFormState && reduxFormState.values;
|
|
|
|
const stateOverrides = schema && schema.stateOverride;
|
|
const loading = schema && schema.metadata && schema.metadata.loading;
|
|
|
|
return { schema, submitting, values, stateOverrides, loading };
|
|
}
|
|
|
|
function mapDispatchToProps(dispatch) {
|
|
return {
|
|
schemaActions: bindActionCreators(schemaActions, dispatch),
|
|
};
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderLoader);
|