mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Use redux-form instead of custom actions/reducers (fixes #5524)
- Removed custom form reducers in favour of redux-form (updateField(), addForm(), etc) - Storing 'state' in schema reducer to avoid a separate forms reducer tree (no longer needed for other use cases). This means 'state' is only the "initial state", any form edits will be handled by redux-form internally in the 'reduxForm' reducer namespace - Removed componentWillUnmount() from <Form> since there's no more reducer state to clean up (removed formReducer.js), and redux-form handles that internally - Removed isFetching state from <FormBuilder> since there's now a props.submitting coming from redux-form - Improved passing of "clicked button" (submittingAction), using component state rather than reducer and passing it into action handlers (no need for components to inspect it any other way) - Hacky duplication of this.submittingAction and this.state.submittingAction to avoid re-render of <FormBuilder> *during* a submission (see https://github.com/erikras/redux-form/issues/1944) - Inlined handleSubmit() XHR (rather than using a redux action). There's other ways for form consumers to listen to form evens (e.g. onSubmitSuccess passed through <FormBuilder> into reduxForm()). - Adapting checkbox/radio fields to redux-forms Need to send onChange event with values rather than the original event, see http://redux-form.com/6.1.1/docs/api/Field.md/#-input-onchange-eventorvalue-function- - Using reduxForm() within render() causes DOM to get thrown away, and has weird side effects like https://github.com/erikras/redux-form/issues/1944. See https://github.com/erikras/redux-form/issues/603#issuecomment-176397728 - Refactored <FormBuilderLoader> as a separate container component which connects to redux and redux-form. This keeps the <FormBuilder> component dependency free and easy to test. It'll also be an advantage if we switch to a GraphQL backed component, in which case the async loading routines will look very different from the current <FormBuilderLoader> implementation - Refactoring out the redux-form dependency from FormBuilder to make it more testable (through baseFormComponent and baseFieldComponent) - Passing through 'form' to allow custom identifiers (which is important because currently the schema "id" contains the record identifier, making the form identifier non-deterministic - which means you can't use it with the redux-form selector API)
This commit is contained in:
parent
5f81122ad3
commit
5b31a40593
@ -1,16 +1,16 @@
|
|||||||
import BootRoutes from './BootRoutes';
|
import BootRoutes from './BootRoutes';
|
||||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
|
import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
|
import { reducer as ReduxFormReducer } from 'redux-form';
|
||||||
|
import { routerReducer } from 'react-router-redux';
|
||||||
import Config from 'lib/Config';
|
import Config from 'lib/Config';
|
||||||
import reducerRegister from 'lib/ReducerRegister';
|
import reducerRegister from 'lib/ReducerRegister';
|
||||||
import * as configActions from 'state/config/ConfigActions';
|
import * as configActions from 'state/config/ConfigActions';
|
||||||
import ConfigReducer from 'state/config/ConfigReducer';
|
import ConfigReducer from 'state/config/ConfigReducer';
|
||||||
import FormReducer from 'state/form/FormReducer';
|
|
||||||
import SchemaReducer from 'state/schema/SchemaReducer';
|
import SchemaReducer from 'state/schema/SchemaReducer';
|
||||||
import RecordsReducer from 'state/records/RecordsReducer';
|
import RecordsReducer from 'state/records/RecordsReducer';
|
||||||
import CampaignReducer from 'state/campaign/CampaignReducer';
|
import CampaignReducer from 'state/campaign/CampaignReducer';
|
||||||
import BreadcrumbsReducer from 'state/breadcrumbs/BreadcrumbsReducer';
|
import BreadcrumbsReducer from 'state/breadcrumbs/BreadcrumbsReducer';
|
||||||
import { routerReducer } from 'react-router-redux';
|
|
||||||
import bootInjector from 'boot/BootInjector';
|
import bootInjector from 'boot/BootInjector';
|
||||||
|
|
||||||
// Sections
|
// Sections
|
||||||
@ -19,7 +19,7 @@ import CampaignAdmin from 'containers/CampaignAdmin/controller';
|
|||||||
|
|
||||||
function appBoot() {
|
function appBoot() {
|
||||||
reducerRegister.add('config', ConfigReducer);
|
reducerRegister.add('config', ConfigReducer);
|
||||||
reducerRegister.add('form', FormReducer);
|
reducerRegister.add('form', ReduxFormReducer);
|
||||||
reducerRegister.add('schemas', SchemaReducer);
|
reducerRegister.add('schemas', SchemaReducer);
|
||||||
reducerRegister.add('records', RecordsReducer);
|
reducerRegister.add('records', RecordsReducer);
|
||||||
reducerRegister.add('campaign', CampaignReducer);
|
reducerRegister.add('campaign', CampaignReducer);
|
||||||
|
@ -5,6 +5,7 @@ require('expose?Form!components/Form/Form');
|
|||||||
require('expose?FormConstants!components/Form/FormConstants');
|
require('expose?FormConstants!components/Form/FormConstants');
|
||||||
require('expose?FormAction!components/FormAction/FormAction');
|
require('expose?FormAction!components/FormAction/FormAction');
|
||||||
require('expose?FormBuilder!components/FormBuilder/FormBuilder');
|
require('expose?FormBuilder!components/FormBuilder/FormBuilder');
|
||||||
|
require('expose?FormBuilderLoader!containers/FormBuilderLoader/FormBuilderLoader');
|
||||||
require('expose?FormBuilderModal!components/FormBuilderModal/FormBuilderModal');
|
require('expose?FormBuilderModal!components/FormBuilderModal/FormBuilderModal');
|
||||||
require('expose?GridField!components/GridField/GridField');
|
require('expose?GridField!components/GridField/GridField');
|
||||||
require('expose?GridFieldCell!components/GridField/GridFieldCell');
|
require('expose?GridFieldCell!components/GridField/GridFieldCell');
|
||||||
|
@ -17,6 +17,7 @@ require('expose?Tether!tether');
|
|||||||
require('expose?ReactDom!react-dom');
|
require('expose?ReactDom!react-dom');
|
||||||
require('expose?Redux!redux');
|
require('expose?Redux!redux');
|
||||||
require('expose?ReactRedux!react-redux');
|
require('expose?ReactRedux!react-redux');
|
||||||
|
require('expose?ReduxForm!redux-form');
|
||||||
require('expose?ReduxThunk!redux-thunk');
|
require('expose?ReduxThunk!redux-thunk');
|
||||||
require('expose?ReactRouter!react-router');
|
require('expose?ReactRouter!react-router');
|
||||||
require('expose?ReactRouterRedux!react-router-redux');
|
require('expose?ReactRouterRedux!react-router-redux');
|
||||||
|
@ -65,7 +65,7 @@ class CheckboxSetField extends SilverStripeComponent {
|
|||||||
})
|
})
|
||||||
.map((item) => `${item.value}`);
|
.map((item) => `${item.value}`);
|
||||||
|
|
||||||
this.props.onChange(event, { id: this.props.id, value: newValue });
|
this.props.onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,8 +90,7 @@ describe('CheckboxSetField', () => {
|
|||||||
checkboxSetField.handleChange(event, { id: 'checkbox-two', value: 1 });
|
checkboxSetField.handleChange(event, { id: 'checkbox-two', value: 1 });
|
||||||
|
|
||||||
expect(checkboxSetField.props.onChange).toBeCalledWith(
|
expect(checkboxSetField.props.onChange).toBeCalledWith(
|
||||||
event,
|
['one', 'two', 'four']
|
||||||
{ id: 'checkbox', value: ['one', 'two', 'four'] }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,8 +100,7 @@ describe('CheckboxSetField', () => {
|
|||||||
checkboxSetField.handleChange(event, { id: 'checkbox-one', value: 0 });
|
checkboxSetField.handleChange(event, { id: 'checkbox-one', value: 0 });
|
||||||
|
|
||||||
expect(checkboxSetField.props.onChange).toBeCalledWith(
|
expect(checkboxSetField.props.onChange).toBeCalledWith(
|
||||||
event,
|
['four']
|
||||||
{ id: 'checkbox', value: ['four'] }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,21 +8,15 @@ class Form extends SilverStripeComponent {
|
|||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (typeof this.props.componentWillUnmount === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.componentWillUnmount(this.props.formId);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const defaultFormProps = {
|
const formProps = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
className: 'form',
|
className: 'form',
|
||||||
onSubmit: this.handleSubmit,
|
onSubmit: this.handleSubmit,
|
||||||
};
|
},
|
||||||
const formProps = Object.assign({}, defaultFormProps, this.props.attributes);
|
this.props.attributes
|
||||||
|
);
|
||||||
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
||||||
const actions = this.props.mapActionsToComponents(this.props.actions);
|
const actions = this.props.mapActionsToComponents(this.props.actions);
|
||||||
|
|
||||||
@ -62,10 +56,7 @@ Form.propTypes = {
|
|||||||
id: React.PropTypes.string,
|
id: React.PropTypes.string,
|
||||||
method: React.PropTypes.string.isRequired,
|
method: React.PropTypes.string.isRequired,
|
||||||
}),
|
}),
|
||||||
componentWillUnmount: React.PropTypes.func,
|
|
||||||
data: React.PropTypes.array,
|
|
||||||
fields: React.PropTypes.array.isRequired,
|
fields: React.PropTypes.array.isRequired,
|
||||||
formId: React.PropTypes.string.isRequired,
|
|
||||||
handleSubmit: React.PropTypes.func,
|
handleSubmit: React.PropTypes.func,
|
||||||
mapActionsToComponents: React.PropTypes.func.isRequired,
|
mapActionsToComponents: React.PropTypes.func.isRequired,
|
||||||
mapFieldsToComponents: React.PropTypes.func.isRequired,
|
mapFieldsToComponents: React.PropTypes.func.isRequired,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Form Component
|
# Form Component
|
||||||
|
|
||||||
The FormComponent is used to render forms in SilverStripe. The only time you should need to use `FormComponent` directly is when you're composing custom layouts. Forms can be automatically generated from a schema using the `FormBuilder` component.
|
The `Form` component is used to render forms in SilverStripe.
|
||||||
|
The only time you should need to use `FormComponent` directly is when you're composing custom layouts.
|
||||||
This component should be moved to Framework when dependency injection is implemented.
|
Forms can be automatically generated from a schema using the `FormBuilder` component.
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@ -19,7 +19,6 @@ attributes = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `componentWillUnmount` (func): Optional function which will be called in the component's 'native' `componentWillUnmount` method. This can be used to tidy up Redux state that's no longer required.
|
|
||||||
* `fields` (required): A list of field objects to display in the form. These objects should be transformed to Components using the `this.props.mapFieldsToComponents` method.
|
* `fields` (required): A list of field objects to display in the form. These objects should be transformed to Components using the `this.props.mapFieldsToComponents` method.
|
||||||
* `mapFieldsToComponents` (required): A function that maps each schema field (`this.props.fields`) to the component responsibe for render it.
|
* `mapFieldsToComponents` (required): A function that maps each schema field (`this.props.fields`) to the component responsibe for render it.
|
||||||
* `handleSubmit` (func): Called then the form is submitted.
|
* `handleSubmit` (func): Called then the form is submitted.
|
||||||
|
@ -28,6 +28,7 @@ class FormAction extends SilverStripeComponent {
|
|||||||
typeof this.props.attributes === 'undefined' ? {} : this.props.attributes,
|
typeof this.props.attributes === 'undefined' ? {} : this.props.attributes,
|
||||||
{
|
{
|
||||||
id: this.props.id,
|
id: this.props.id,
|
||||||
|
name: this.props.name,
|
||||||
className: this.getButtonClasses(),
|
className: this.getButtonClasses(),
|
||||||
disabled: this.props.disabled,
|
disabled: this.props.disabled,
|
||||||
onClick: this.handleClick,
|
onClick: this.handleClick,
|
||||||
|
@ -1,197 +1,45 @@
|
|||||||
import React from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import * as formActions from 'state/form/FormActions';
|
|
||||||
import * as schemaActions from 'state/schema/SchemaActions';
|
|
||||||
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||||
import Form from 'components/Form/Form';
|
|
||||||
import fetch from 'isomorphic-fetch';
|
|
||||||
import backend from 'lib/Backend';
|
import backend from 'lib/Backend';
|
||||||
import injector from 'lib/Injector';
|
import injector from 'lib/Injector';
|
||||||
import merge from 'merge';
|
import merge from 'merge';
|
||||||
|
|
||||||
import es6promise from 'es6-promise';
|
class FormBuilder extends SilverStripeComponent {
|
||||||
es6promise.polyfill();
|
|
||||||
|
|
||||||
export class FormBuilderComponent extends SilverStripeComponent {
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.formSchemaPromise = null;
|
const schemaStructure = props.schema.schema;
|
||||||
this.state = { isFetching: false };
|
this.state = { submittingAction: null };
|
||||||
|
this.submitApi = backend.createEndpointFetcher({
|
||||||
|
url: schemaStructure.attributes.action,
|
||||||
|
method: schemaStructure.attributes.method,
|
||||||
|
});
|
||||||
this.mapActionsToComponents = this.mapActionsToComponents.bind(this);
|
this.mapActionsToComponents = this.mapActionsToComponents.bind(this);
|
||||||
this.mapFieldsToComponents = this.mapFieldsToComponents.bind(this);
|
this.mapFieldsToComponents = this.mapFieldsToComponents.bind(this);
|
||||||
this.handleFieldUpdate = this.handleFieldUpdate.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleAction = this.handleAction.bind(this);
|
this.handleAction = this.handleAction.bind(this);
|
||||||
this.removeForm = this.removeForm.bind(this);
|
|
||||||
this.getFormId = this.getFormId.bind(this);
|
|
||||||
this.getFormSchema = this.getFormSchema.bind(this);
|
|
||||||
this.findField = this.findField.bind(this);
|
this.findField = this.findField.bind(this);
|
||||||
}
|
this.buildComponent = this.buildComponent.bind(this);
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the schema for this form
|
|
||||||
*
|
|
||||||
* @returns {array}
|
|
||||||
*/
|
|
||||||
getFormSchema() {
|
|
||||||
return this.props.schemas[this.props.schemaUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the ID for this form
|
|
||||||
*
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
getFormId() {
|
|
||||||
const schema = this.getFormSchema();
|
|
||||||
if (schema) {
|
|
||||||
return schema.id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.schemaUrl !== prevProps.schemaUrl) {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (this.state.isFetching === true) {
|
|
||||||
return this.formSchemaPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema === true) {
|
|
||||||
headerValues.push('schema');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === true) {
|
|
||||||
headerValues.push('state');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.formSchemaPromise = fetch(this.props.schemaUrl, {
|
|
||||||
headers: { 'X-FormSchema-Request': headerValues.join() },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(json => {
|
|
||||||
const formSchema = Object.assign({}, { id: json.id, schema: json.schema });
|
|
||||||
const formState = Object.assign({}, json.state);
|
|
||||||
|
|
||||||
// TODO See "Enable once <CampaignAdmin> ..." below
|
|
||||||
// this.setState({ isFetching: false });
|
|
||||||
|
|
||||||
if (typeof formSchema.id !== 'undefined') {
|
|
||||||
const defaultData = {
|
|
||||||
SecurityID: this.props.config.SecurityID,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.submitApi = (...args) => {
|
|
||||||
const endPoint = backend.createEndpointFetcher({
|
|
||||||
url: formSchema.schema.attributes.action,
|
|
||||||
method: formSchema.schema.attributes.method,
|
|
||||||
defaultData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure that schema changes are handled prior to updating state
|
|
||||||
return endPoint(...args)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.schema) {
|
|
||||||
const newSchema = Object.assign({}, { id: response.id, schema: response.schema });
|
|
||||||
this.props.schemaActions.setSchema(newSchema);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.props.schemaActions.setSchema(formSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof formState.id !== 'undefined') {
|
|
||||||
this.props.formActions.addForm(formState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO Enable once <CampaignAdmin> is initialised via page.js route callbacks
|
|
||||||
// At the moment, it's running an Entwine onadd() rule which ends up
|
|
||||||
// rendering the index view, and only then calling route.start() to
|
|
||||||
// match the detail view (admin/campaigns/set/:id/show).
|
|
||||||
// This causes the form builder to be unmounted during a fetch() call.
|
|
||||||
// this.setState({ isFetching: true });
|
|
||||||
|
|
||||||
return this.formSchemaPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update handler passed down to each form field as a prop.
|
|
||||||
* Form fields call this method when their state changes.
|
|
||||||
*
|
|
||||||
* You can pass an optional callback as the third param. This can be used to
|
|
||||||
* implement custom behaviour. For example you can use `createFn` hook from
|
|
||||||
* your controller context like this.
|
|
||||||
*
|
|
||||||
* controller.js
|
|
||||||
* ...
|
|
||||||
* detailEditFormCreateFn(Component, props) {
|
|
||||||
* const extendedProps = Object.assign({}, props, {
|
|
||||||
* handleFieldUpdate: (event, updates) => {
|
|
||||||
* props.handleFieldUpdate(event, updates, (formId, updateFieldAction) => {
|
|
||||||
* const customUpdates = Object.assign({}, updates, {
|
|
||||||
* value: someCustomParsing(updates.value),
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* updateFieldAction(formId, customUpdates);
|
|
||||||
* });
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* return <Component {...extendedProps} />;
|
|
||||||
* }
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
* @param {object} event - Change event from the form field component.
|
|
||||||
* @param {object} updates - Values to set in state.
|
|
||||||
* @param {string} updates.id - Field ID. Required to identify the field in the store.
|
|
||||||
* @param {function} [fn] - Optional function for custom behaviour. See example in description.
|
|
||||||
*/
|
|
||||||
handleFieldUpdate(event, updates, fn) {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
fn(this.getFormId(), this.props.formActions.updateField);
|
|
||||||
} else {
|
|
||||||
this.props.formActions.updateField(this.getFormId(), updates);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the action is clicked on, records which action was clicked on
|
* When the action is clicked on, records which action was clicked on
|
||||||
* This can allow for preventing the submit action, such as a custom action for the button
|
* This can allow for preventing the submit action, such as a custom action for the button
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {Event} event
|
||||||
* @param submitAction
|
|
||||||
*/
|
*/
|
||||||
handleAction(event, submitAction) {
|
handleAction(event) {
|
||||||
this.props.formActions.setSubmitAction(this.getFormId(), submitAction);
|
// Custom handlers
|
||||||
if (typeof this.props.handleAction === 'function') {
|
if (typeof this.props.handleAction === 'function') {
|
||||||
this.props.handleAction(event, submitAction, this.getFieldValues());
|
this.props.handleAction(event, this.getFieldValues());
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = event.currentTarget.name;
|
||||||
|
|
||||||
|
// Allow custom handlers to cancel event
|
||||||
|
if (!event.isPropagationStopped()) {
|
||||||
|
this.setState({ submittingAction: name });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,48 +47,45 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
* Form submission handler passed to the Form Component as a prop.
|
* Form submission handler passed to the Form Component as a prop.
|
||||||
* Provides a hook for controllers to access for state and provide custom functionality.
|
* Provides a hook for controllers to access for state and provide custom functionality.
|
||||||
*
|
*
|
||||||
* For example:
|
* @param {Object} data Processed and validated data from redux-form
|
||||||
*
|
* (originally retrieved through getFieldValues())
|
||||||
* controller.js
|
|
||||||
* ```
|
|
||||||
* constructor(props) {
|
|
||||||
* super(props);
|
|
||||||
* this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* handleSubmit(event, fieldValues, submitFn) {
|
|
||||||
* event.preventDefault();
|
|
||||||
*
|
|
||||||
* // Apply custom validation.
|
|
||||||
* if (!this.validate(fieldValues)) {
|
|
||||||
* return;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* submitFn();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* render() {
|
|
||||||
* return <FormBuilder handleSubmit={this.handleSubmit} />
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {Object} event
|
|
||||||
* @return {Promise|null}
|
* @return {Promise|null}
|
||||||
*/
|
*/
|
||||||
handleSubmit(event) {
|
handleSubmit(data) {
|
||||||
const fieldValues = this.getFieldValues();
|
// Add form action data (or default to first action, same as browser behaviour)
|
||||||
|
const action = this.state.submittingAction
|
||||||
|
? this.state.submittingAction
|
||||||
|
: this.props.schema.schema.actions[0].name;
|
||||||
|
|
||||||
const submitFn = () => this.props.formActions.submitForm(
|
const dataWithAction = Object.assign({}, data, {
|
||||||
this.submitApi,
|
[action]: 1,
|
||||||
this.getFormId(),
|
});
|
||||||
fieldValues
|
const headers = {
|
||||||
);
|
'X-Formschema-Request': 'state,schema',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
};
|
||||||
|
|
||||||
if (typeof this.props.handleSubmit !== 'undefined') {
|
const resetSubmittingFn = () => {
|
||||||
return this.props.handleSubmit(event, fieldValues, submitFn);
|
this.setState({ submittingAction: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitFn = (customData) =>
|
||||||
|
this.submitApi(customData || dataWithAction, headers)
|
||||||
|
.then(formSchema => {
|
||||||
|
resetSubmittingFn();
|
||||||
|
return formSchema;
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
// TODO Generic CMS error reporting
|
||||||
|
// TODO Handle validation errors
|
||||||
|
resetSubmittingFn();
|
||||||
|
return reason;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof this.props.handleSubmit === 'function') {
|
||||||
|
return this.props.handleSubmit(dataWithAction, action, submitFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
return submitFn();
|
return submitFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,35 +95,31 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
getFieldValues() {
|
getFieldValues() {
|
||||||
const schema = this.props.schemas[this.props.schemaUrl];
|
|
||||||
// using state is more efficient and has the same fields, fallback to nested schema
|
// using state is more efficient and has the same fields, fallback to nested schema
|
||||||
const fields = (schema.state)
|
const schema = this.props.schema.schema;
|
||||||
? schema.state.fields
|
const state = this.props.schema.state;
|
||||||
: schema.schema.fields;
|
|
||||||
|
|
||||||
// Set action
|
if (!state) {
|
||||||
const action = this.getSubmitAction();
|
return {};
|
||||||
const values = {};
|
|
||||||
if (action) {
|
|
||||||
values[action] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce all other fields
|
return state.fields
|
||||||
return this.props.form[this.getFormId()].fields
|
|
||||||
.reduce((prev, curr) => {
|
.reduce((prev, curr) => {
|
||||||
const match = this.findField(fields, curr.id);
|
const match = this.findField(schema.fields, curr.id);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip non-data fields
|
||||||
|
if (match.type === 'Structural' || match.readOnly === true) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign({}, prev, {
|
return Object.assign({}, prev, {
|
||||||
[match.name]: curr.value,
|
[match.name]: curr.value,
|
||||||
});
|
});
|
||||||
}, values);
|
}, {});
|
||||||
}
|
|
||||||
|
|
||||||
getSubmitAction() {
|
|
||||||
return this.props.form[this.getFormId()].submitAction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -309,39 +150,42 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
/**
|
/**
|
||||||
* Common functionality for building a Field or Action from schema.
|
* Common functionality for building a Field or Action from schema.
|
||||||
*
|
*
|
||||||
* @param field
|
* @param {Object} props Props which every form field receives. Leave it up to the
|
||||||
* @param extraProps
|
* schema and component to determine which props are required.
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
buildComponent(field, extraProps = {}) {
|
buildComponent(props) {
|
||||||
const Component = field.component !== null
|
let componentProps = props;
|
||||||
? injector.getComponentByName(field.component)
|
// 'component' key is renamed to 'schemaComponent' in normalize*() methods
|
||||||
: injector.getComponentByDataType(field.type);
|
const SchemaComponent = componentProps.schemaComponent !== null
|
||||||
|
? injector.getComponentByName(componentProps.schemaComponent)
|
||||||
|
: injector.getComponentByDataType(componentProps.type);
|
||||||
|
|
||||||
if (Component === null) {
|
if (SchemaComponent === null) {
|
||||||
return null;
|
return null;
|
||||||
} else if (field.component !== null && Component === undefined) {
|
} else if (componentProps.schemaComponent !== null && SchemaComponent === undefined) {
|
||||||
throw Error(`Component not found in injector: ${field.component}`);
|
throw Error(`Component not found in injector: ${componentProps.schemaComponent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props which every form field receives.
|
|
||||||
// Leave it up to the schema and component to determine
|
|
||||||
// which props are required.
|
|
||||||
const props = Object.assign({}, field, extraProps);
|
|
||||||
|
|
||||||
// if no value, it is better to unset it
|
// if no value, it is better to unset it
|
||||||
if (props.value === null) {
|
if (componentProps.value === null) {
|
||||||
delete props.value;
|
delete componentProps.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline `input` props into main field props
|
||||||
|
// (each component can pick and choose the props required for it's <input>
|
||||||
|
// See http://redux-form.com/6.0.5/docs/api/Field.md/#input-props
|
||||||
|
componentProps = Object.assign({}, componentProps, componentProps.input);
|
||||||
|
delete componentProps.input;
|
||||||
|
|
||||||
// Provides container components a place to hook in
|
// Provides container components a place to hook in
|
||||||
// and apply customisations to scaffolded components.
|
// and apply customisations to scaffolded components.
|
||||||
const createFn = this.props.createFn;
|
const createFn = this.props.createFn;
|
||||||
if (typeof createFn === 'function') {
|
if (typeof createFn === 'function') {
|
||||||
return createFn(Component, props);
|
return createFn(SchemaComponent, componentProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component key={props.id} {...props} />;
|
return <SchemaComponent key={componentProps.id} {...componentProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -353,16 +197,25 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
mapFieldsToComponents(fields) {
|
mapFieldsToComponents(fields) {
|
||||||
|
const FieldComponent = this.props.baseFieldComponent;
|
||||||
return fields.map((field) => {
|
return fields.map((field) => {
|
||||||
// Events
|
let props = field;
|
||||||
const extraProps = { onChange: this.handleFieldUpdate };
|
|
||||||
|
|
||||||
// Build child nodes
|
|
||||||
if (field.children) {
|
if (field.children) {
|
||||||
extraProps.children = this.mapFieldsToComponents(field.children);
|
props = Object.assign(
|
||||||
|
{},
|
||||||
|
field,
|
||||||
|
{ children: this.mapFieldsToComponents(field.children) }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.buildComponent(field, extraProps);
|
// Don't wrap structural or readonly fields, since they don't need connected fields.
|
||||||
|
// The redux-form connected fields also messed up react-bootstrap's tab handling.
|
||||||
|
if (field.type === 'Structural' || field.readOnly === true) {
|
||||||
|
return this.buildComponent(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FieldComponent key={props.id} {...props} component={this.buildComponent} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,23 +226,21 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
mapActionsToComponents(actions) {
|
mapActionsToComponents(actions) {
|
||||||
const form = this.props.form[this.getFormId()];
|
|
||||||
|
|
||||||
return actions.map((action) => {
|
return actions.map((action) => {
|
||||||
const loading = (form && form.submitting && form.submitAction === action.name);
|
const props = Object.assign({}, action);
|
||||||
// Events
|
|
||||||
const extraProps = {
|
|
||||||
handleClick: this.handleAction,
|
|
||||||
loading,
|
|
||||||
disabled: loading || action.disabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build child nodes
|
|
||||||
if (action.children) {
|
if (action.children) {
|
||||||
extraProps.children = this.mapActionsToComponents(action.children);
|
props.children = this.mapActionsToComponents(action.children);
|
||||||
|
} else {
|
||||||
|
props.handleClick = this.handleAction;
|
||||||
|
|
||||||
|
// Reset through componentWillReceiveProps()
|
||||||
|
if (this.state.submittingAction === action.name) {
|
||||||
|
props.loading = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.buildComponent(action, extraProps);
|
return this.buildComponent(props);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,106 +267,144 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up Redux state used by the form when the Form component is unmonuted.
|
* If there is structural and state data available merge those data for each field.
|
||||||
|
* Otherwise just use the structural data. Ensure that keys don't conflict
|
||||||
|
* with redux-form expectations.
|
||||||
*
|
*
|
||||||
* @param {string} formId - ID of the form to clean up.
|
* @param {array} fields
|
||||||
|
* @param {Object} state Optional
|
||||||
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
removeForm(formId) {
|
normalizeFields(fields, state) {
|
||||||
this.props.formActions.removeForm(formId);
|
return fields.map((field) => {
|
||||||
}
|
const fieldState = (state && state.fields)
|
||||||
|
? state.fields.find((item) => item.id === field.id)
|
||||||
|
: {};
|
||||||
|
|
||||||
/**
|
const data = merge.recursive(
|
||||||
* If there is structural and state data availabe merge those data for each field.
|
true,
|
||||||
* Otherwise just use the structural data.
|
this.mergeFieldData(field, fieldState),
|
||||||
*/
|
// Overlap with redux-form prop handling : createFieldProps filters out the 'component' key
|
||||||
getFieldData(formFields, formState) {
|
{ schemaComponent: field.component }
|
||||||
if (!formFields || !formState || !formState.fields) {
|
);
|
||||||
return formFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formFields.map((field) => {
|
|
||||||
const state = formState.fields.find((item) => item.id === field.id);
|
|
||||||
const data = this.mergeFieldData(field, state);
|
|
||||||
|
|
||||||
if (field.children) {
|
if (field.children) {
|
||||||
return Object.assign({}, data, {
|
data.children = this.normalizeFields(field.children, state);
|
||||||
children: this.getFieldData(field.children, formState),
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that keys don't conflict with redux-form expectations.
|
||||||
|
*
|
||||||
|
* @param {array} actions
|
||||||
|
* @return {array}
|
||||||
|
*/
|
||||||
|
normalizeActions(actions) {
|
||||||
|
return actions.map((action) => {
|
||||||
|
const data = merge.recursive(
|
||||||
|
true,
|
||||||
|
action,
|
||||||
|
// Overlap with redux-form prop handling : createFieldProps filters out the 'component' key
|
||||||
|
{ schemaComponent: action.component }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (action.children) {
|
||||||
|
data.children = this.normalizeActions(action.children);
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const formId = this.getFormId();
|
const schema = this.props.schema.schema;
|
||||||
if (!formId) {
|
const state = this.props.schema.state;
|
||||||
return null;
|
const BaseFormComponent = this.props.baseFormComponent;
|
||||||
}
|
|
||||||
const formSchema = this.getFormSchema();
|
|
||||||
const formState = this.props.form[formId];
|
|
||||||
|
|
||||||
// If the response from fetching the initial data
|
|
||||||
// hasn't come back yet, don't render anything.
|
|
||||||
if (!formSchema || !formSchema.schema) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map form schema to React component attribute names,
|
// Map form schema to React component attribute names,
|
||||||
// which requires renaming some of them (by unsetting the original keys)
|
// which requires renaming some of them (by unsetting the original keys)
|
||||||
const attributes = Object.assign({}, formSchema.schema.attributes, {
|
const attributes = Object.assign({}, schema.attributes, {
|
||||||
className: formSchema.schema.attributes.class,
|
className: schema.attributes.class,
|
||||||
encType: formSchema.schema.attributes.enctype,
|
encType: schema.attributes.enctype,
|
||||||
});
|
});
|
||||||
// these two still cause silent errors
|
|
||||||
delete attributes.class;
|
delete attributes.class;
|
||||||
delete attributes.enctype;
|
delete attributes.enctype;
|
||||||
|
|
||||||
const fieldData = this.getFieldData(formSchema.schema.fields, formState);
|
const {
|
||||||
const actionData = this.getFieldData(formSchema.schema.actions, formState);
|
asyncValidate,
|
||||||
|
onSubmitFail,
|
||||||
|
onSubmitSuccess,
|
||||||
|
shouldAsyncValidate,
|
||||||
|
touchOnBlur,
|
||||||
|
touchOnChange,
|
||||||
|
persistentSubmitErrors,
|
||||||
|
validate,
|
||||||
|
form,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const formProps = {
|
const props = {
|
||||||
actions: actionData,
|
form, // required as redux-form identifier
|
||||||
|
fields: this.normalizeFields(schema.fields, state),
|
||||||
|
actions: this.normalizeActions(schema.actions),
|
||||||
attributes,
|
attributes,
|
||||||
componentWillUnmount: this.removeForm,
|
data: schema.data,
|
||||||
data: formSchema.schema.data,
|
initialValues: this.getFieldValues(),
|
||||||
fields: fieldData,
|
onSubmit: this.handleSubmit,
|
||||||
formId,
|
|
||||||
handleSubmit: this.handleSubmit,
|
|
||||||
mapActionsToComponents: this.mapActionsToComponents,
|
mapActionsToComponents: this.mapActionsToComponents,
|
||||||
mapFieldsToComponents: this.mapFieldsToComponents,
|
mapFieldsToComponents: this.mapFieldsToComponents,
|
||||||
|
asyncValidate,
|
||||||
|
onSubmitFail,
|
||||||
|
onSubmitSuccess,
|
||||||
|
shouldAsyncValidate,
|
||||||
|
touchOnBlur,
|
||||||
|
touchOnChange,
|
||||||
|
persistentSubmitErrors,
|
||||||
|
validate,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Form {...formProps} />;
|
return <BaseFormComponent {...props} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormBuilderComponent.propTypes = {
|
const schemaPropType = PropTypes.shape({
|
||||||
config: React.PropTypes.object,
|
id: PropTypes.string.isRequired,
|
||||||
createFn: React.PropTypes.func,
|
schema: PropTypes.shape({
|
||||||
form: React.PropTypes.object.isRequired,
|
attributes: PropTypes.shape({
|
||||||
formActions: React.PropTypes.object.isRequired,
|
class: PropTypes.string,
|
||||||
handleSubmit: React.PropTypes.func,
|
enctype: PropTypes.string,
|
||||||
handleAction: React.PropTypes.func,
|
}),
|
||||||
schemas: React.PropTypes.object.isRequired,
|
fields: PropTypes.array.isRequired,
|
||||||
schemaActions: React.PropTypes.object.isRequired,
|
}).isRequired,
|
||||||
schemaUrl: React.PropTypes.string.isRequired,
|
state: PropTypes.shape({
|
||||||
|
fields: PropTypes.array,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const basePropTypes = {
|
||||||
|
createFn: PropTypes.func,
|
||||||
|
handleSubmit: PropTypes.func,
|
||||||
|
handleAction: PropTypes.func,
|
||||||
|
asyncValidate: PropTypes.func,
|
||||||
|
onSubmitFail: PropTypes.func,
|
||||||
|
onSubmitSuccess: PropTypes.func,
|
||||||
|
shouldAsyncValidate: PropTypes.func,
|
||||||
|
touchOnBlur: PropTypes.bool,
|
||||||
|
touchOnChange: PropTypes.bool,
|
||||||
|
persistentSubmitErrors: PropTypes.bool,
|
||||||
|
validate: PropTypes.func,
|
||||||
|
baseFormComponent: PropTypes.func.isRequired,
|
||||||
|
baseFieldComponent: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
FormBuilder.propTypes = Object.assign({}, basePropTypes, {
|
||||||
return {
|
form: PropTypes.string.isRequired,
|
||||||
config: state.config,
|
schema: schemaPropType.isRequired,
|
||||||
form: state.form,
|
});
|
||||||
schemas: state.schemas,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
export { basePropTypes, schemaPropType };
|
||||||
return {
|
export default FormBuilder;
|
||||||
formActions: bindActionCreators(formActions, dispatch),
|
|
||||||
schemaActions: bindActionCreators(schemaActions, dispatch),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);
|
|
||||||
|
@ -1,15 +1,90 @@
|
|||||||
# FormBuilder Component
|
# FormBuilder Component
|
||||||
|
|
||||||
Used to generate forms, made up of field components and actions, from FormFieldSchema data.
|
Used to generate forms, made up of field components and actions, from form schema data.
|
||||||
|
|
||||||
This component will be moved to Framweork or CMS when dependency injection is implemented.
|
Forms are usually rendered through [redux-form](http://redux-form.com/),
|
||||||
|
although this can be controlled through the `baseFormComponent`
|
||||||
|
and `baseFieldComponent` props.
|
||||||
|
|
||||||
|
If you want to load the schema from a server via XHR, use the
|
||||||
|
[FormBuilderLoader](../../containers/FormBuilderLoader/README.md] instead.
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
* `createFn` (function): Gives container components a chance to access a form component before it's constructed. Use this as an opportunity to pass a custom click handler to to a field for example.
|
* `form` (string): Form identifier (useful to reference this form through redux selectors)
|
||||||
* `schemaUrl` (string): The schema URL where the form will be scaffolded from e.g. '/admin/pages/schema/1'.
|
* `baseFormComponent` (function): A React component to render the form
|
||||||
* `handleSubmit` (function): Event handler passed to the Form Component as a prop. Parameters received are:
|
* `baseFieldComponent` (function): A React component to render each field. Should be a HOC which receives
|
||||||
* `event` (Event): The submit event, it is strongly recommended to call `preventDefault()`
|
the actual field builder through a `component` prop.
|
||||||
* `fieldValues` (object): An object containing the field values captured by the Submit handler
|
* `schema` (object): A form schema (see "Schema Structure" below)
|
||||||
* `submitFn` (function): A callback for when the submission was successful, if submission fails, this function should not be called. (e.g. validation error)
|
* `createFn` (function): Gives container components a chance to access a form component before it's constructed.
|
||||||
* `handleAction` (function): Event handler when a form action is clicked on, allows preventing submit and know which action was clicked on.
|
Use this as an opportunity to pass a custom click handler to to a field for example.
|
||||||
|
* `handleSubmit` (function): Event handler passed to the Form Component as a prop.
|
||||||
|
Should return a promise (usually the result of the `submitFn` argument). Parameters received are:
|
||||||
|
* `data` (object): An object containing the field values captured by the submit handler
|
||||||
|
* `action` (string): The name of the button clicked to perform this action.
|
||||||
|
Defaults to first button when form is submitted by pressing the "enter" key.
|
||||||
|
* `submitFn` (function): A callback for when the submission was successful, if submission fails,
|
||||||
|
this function should not be called. (e.g. validation error). Pass in your modified `data`
|
||||||
|
to influence the data being sent through.
|
||||||
|
* `handleAction` (function): Event handler when a form action is clicked on, allows preventing submit and know which action was clicked on. Arguments:
|
||||||
|
* `event` (function) Allows cancellation of the submission through `event.stopPropagation()`.
|
||||||
|
The action can be identified via `event.currentTarget.name`.
|
||||||
|
* `data` (object): Validated and processed field values, ready for submission
|
||||||
|
* `asyncValidate` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `onSubmitFail` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `onSubmitSuccess` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `shouldAsyncValidate` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `touchOnBlur` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `touchOnChange` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `persistentSubmitErrors` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
* `validate` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
|
||||||
|
|
||||||
|
## Schema Structure
|
||||||
|
|
||||||
|
The `schema` prop expects a particular structure, containing the form structure
|
||||||
|
in a `schema` key, and the form values in a `state` key.
|
||||||
|
See [RFC: FormField React Integration API](https://github.com/silverstripe/silverstripe-framework/issues/4938) for details.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Field as ReduxFormField, reduxForm } from 'redux-form';
|
||||||
|
class MyComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
handleSubmit(data, action, submitFn) {
|
||||||
|
// You can implement custom submission handling and data processing here.
|
||||||
|
// Ensure to always return a promise if you want execution to continue.
|
||||||
|
if (!this.myCheck(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return submitFn();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const props = {
|
||||||
|
form: 'MyForm',
|
||||||
|
schema: { /* ... */ },
|
||||||
|
baseFormComponent: reduxForm()(Form),
|
||||||
|
baseFieldComponent: ReduxFormField,
|
||||||
|
handleSubmit: this.handleSubmit
|
||||||
|
};
|
||||||
|
return <FormBuilder {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With the default implementation of [redux-form](http://http://redux-form.com)
|
||||||
|
(passed in through `baseFormComponent` and `baseFieldComponent`),
|
||||||
|
the submission process works as follows:
|
||||||
|
|
||||||
|
1. `<FormBuilder>` passes it's `handleSubmit` to `reduxForm()` as an `onSubmit` prop
|
||||||
|
1. `reduxForm()` passes it's own `handleSubmit` to `<Form>`
|
||||||
|
1. `<Form>` sets `<Form onSubmit={this.props.handleSubmit}>`
|
||||||
|
1. `<Form>` calls `reduxForm()` own `handleSubmit()`, which does normalisation and validation
|
||||||
|
1. `reduxForm()` calls its `onSubmit` prop, which is set to `<FormBuilder>` `handleSubmit()`
|
||||||
|
1. `<FormBuilder>` either submits the form, or calls it's own overloaded `handleSubmit()` prop
|
||||||
|
|
||||||
|
See [handleSubmit](http://redux-form.com/6.0.5/docs/api/Props.md#-handlesubmit-eventorsubmit-function-)
|
||||||
|
in the redux-form docs for more details.
|
||||||
|
@ -3,23 +3,39 @@
|
|||||||
jest.unmock('merge');
|
jest.unmock('merge');
|
||||||
jest.unmock('lib/SilverStripeComponent');
|
jest.unmock('lib/SilverStripeComponent');
|
||||||
jest.unmock('../FormBuilder');
|
jest.unmock('../FormBuilder');
|
||||||
|
jest.unmock('redux-form');
|
||||||
|
|
||||||
import { FormBuilderComponent } from '../FormBuilder';
|
const React = require('react');
|
||||||
|
import ReactTestUtils from 'react-addons-test-utils';
|
||||||
|
import FormBuilder from '../FormBuilder';
|
||||||
|
|
||||||
|
describe('FormBuilder', () => {
|
||||||
|
const baseProps = {
|
||||||
|
form: 'MyForm',
|
||||||
|
baseFormComponent: () => <form />,
|
||||||
|
baseFieldComponent: (props) => {
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
const Component = props.component;
|
||||||
|
return <Component {...props} />;
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
id: 'MyForm',
|
||||||
|
schema: {
|
||||||
|
attributes: {},
|
||||||
|
fields: [],
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('FormBuilderComponent', () => {
|
|
||||||
describe('mergeFieldData()', () => {
|
describe('mergeFieldData()', () => {
|
||||||
let formBuilder = null;
|
let formBuilder = null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const props = {
|
formBuilder = new FormBuilder(baseProps);
|
||||||
form: {},
|
|
||||||
formActions: {},
|
|
||||||
schemas: {},
|
|
||||||
schemaActions: {},
|
|
||||||
schemaUrl: 'admin/assets/schema/1',
|
|
||||||
};
|
|
||||||
|
|
||||||
formBuilder = new FormBuilderComponent(props);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deep merge properties on the originalobject', () => {
|
it('should deep merge properties on the originalobject', () => {
|
||||||
@ -57,40 +73,22 @@ describe('FormBuilderComponent', () => {
|
|||||||
describe('getFieldValues()', () => {
|
describe('getFieldValues()', () => {
|
||||||
let formBuilder = null;
|
let formBuilder = null;
|
||||||
let fieldValues = null;
|
let fieldValues = null;
|
||||||
let props = null;
|
const props = Object.assign({}, baseProps);
|
||||||
|
|
||||||
it('should retrieve field values based on schema', () => {
|
it('should retrieve field values based on schema', () => {
|
||||||
props = {
|
props.schema.schema.fields = [
|
||||||
form: {
|
{ id: 'fieldOne', name: 'fieldOne' },
|
||||||
MyForm: {
|
{ id: 'fieldTwo', name: 'fieldTwo' },
|
||||||
submitAction: 'action_save',
|
];
|
||||||
fields: [
|
props.schema.state.fields = [
|
||||||
{ id: 'fieldOne', value: 'valOne' },
|
{ id: 'fieldOne', value: 'valOne' },
|
||||||
{ id: 'fieldTwo', value: null },
|
{ id: 'fieldTwo', value: null },
|
||||||
{ id: 'notInSchema', value: 'invalid' },
|
{ id: 'notInSchema', value: 'invalid' },
|
||||||
],
|
];
|
||||||
},
|
formBuilder = new FormBuilder(baseProps);
|
||||||
},
|
|
||||||
formActions: {},
|
|
||||||
schemas: {
|
|
||||||
'admin/assets/schema/1': {
|
|
||||||
id: 'MyForm',
|
|
||||||
schema: {
|
|
||||||
fields: [
|
|
||||||
{ id: 'fieldOne', name: 'fieldOne' },
|
|
||||||
{ id: 'fieldTwo', name: 'fieldTwo' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schemaActions: {},
|
|
||||||
schemaUrl: 'admin/assets/schema/1',
|
|
||||||
};
|
|
||||||
formBuilder = new FormBuilderComponent(props);
|
|
||||||
|
|
||||||
fieldValues = formBuilder.getFieldValues();
|
fieldValues = formBuilder.getFieldValues();
|
||||||
expect(fieldValues).toEqual({
|
expect(fieldValues).toEqual({
|
||||||
action_save: 1,
|
|
||||||
fieldOne: 'valOne',
|
fieldOne: 'valOne',
|
||||||
fieldTwo: null,
|
fieldTwo: null,
|
||||||
});
|
});
|
||||||
@ -100,23 +98,9 @@ describe('FormBuilderComponent', () => {
|
|||||||
describe('findField()', () => {
|
describe('findField()', () => {
|
||||||
let formBuilder = null;
|
let formBuilder = null;
|
||||||
let fields = null;
|
let fields = null;
|
||||||
const props = {
|
|
||||||
form: {
|
|
||||||
myForm: {},
|
|
||||||
formActions: {},
|
|
||||||
schemas: {
|
|
||||||
'admin/assets/schema/1': {
|
|
||||||
id: 'myForm',
|
|
||||||
schema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schemaActions: {},
|
|
||||||
schemaUrl: 'admin/assets/schema/1',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
formBuilder = new FormBuilderComponent(props);
|
formBuilder = new FormBuilder(baseProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should retrieve the field in the shallow fields list', () => {
|
it('should retrieve the field in the shallow fields list', () => {
|
||||||
@ -152,4 +136,61 @@ describe('FormBuilderComponent', () => {
|
|||||||
expect(field.id).toBe('fieldTwoThree');
|
expect(field.id).toBe('fieldTwoThree');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleSubmit', () => {
|
||||||
|
let formBuilder = null;
|
||||||
|
const props = baseProps;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
formBuilder = ReactTestUtils.renderIntoDocument(<FormBuilder {...props} />);
|
||||||
|
|
||||||
|
props.schema.schema.fields = [
|
||||||
|
{ id: 'fieldOne', name: 'fieldOne' },
|
||||||
|
{ id: 'fieldTwo', name: 'fieldTwo' },
|
||||||
|
];
|
||||||
|
props.schema.schema.actions = [
|
||||||
|
{ id: 'actionOne', name: 'actionOne' },
|
||||||
|
{ id: 'actionTwo', name: 'actionTwo' },
|
||||||
|
];
|
||||||
|
props.schema.state.fields = [
|
||||||
|
{ id: 'fieldOne', value: 'valOne' },
|
||||||
|
{ id: 'fieldTwo', value: null },
|
||||||
|
{ id: 'notInSchema', value: 'invalid' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include submitted action from schema', () => {
|
||||||
|
formBuilder.setState({ submittingAction: 'actionTwo' });
|
||||||
|
|
||||||
|
const submitApiMock = jest.genMockFunction();
|
||||||
|
submitApiMock.mockImplementation(() => Promise.resolve({}));
|
||||||
|
formBuilder.submitApi = submitApiMock;
|
||||||
|
|
||||||
|
formBuilder.handleSubmit(formBuilder.getFieldValues());
|
||||||
|
|
||||||
|
expect(formBuilder.submitApi.mock.calls[0][0]).toEqual(
|
||||||
|
{
|
||||||
|
fieldOne: 'valOne',
|
||||||
|
fieldTwo: null,
|
||||||
|
actionTwo: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to first button when none is specified', () => {
|
||||||
|
const submitApiMock = jest.genMockFunction();
|
||||||
|
submitApiMock.mockImplementation(() => Promise.resolve({}));
|
||||||
|
formBuilder.submitApi = submitApiMock;
|
||||||
|
|
||||||
|
formBuilder.handleSubmit(formBuilder.getFieldValues());
|
||||||
|
|
||||||
|
expect(formBuilder.submitApi.mock.calls[0][0]).toEqual(
|
||||||
|
{
|
||||||
|
fieldOne: 'valOne',
|
||||||
|
fieldTwo: null,
|
||||||
|
actionOne: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal } from 'react-bootstrap-ss';
|
import { Modal } from 'react-bootstrap-ss';
|
||||||
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||||
import FormBuilder from 'components/FormBuilder/FormBuilder';
|
import FormBuilderLoader from 'containers/FormBuilderLoader/FormBuilderLoader';
|
||||||
|
|
||||||
class FormBuilderModal extends SilverStripeComponent {
|
class FormBuilderModal extends SilverStripeComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -19,7 +19,7 @@ class FormBuilderModal extends SilverStripeComponent {
|
|||||||
*/
|
*/
|
||||||
getForm() {
|
getForm() {
|
||||||
return (
|
return (
|
||||||
<FormBuilder
|
<FormBuilderLoader
|
||||||
schemaUrl={this.props.schemaUrl}
|
schemaUrl={this.props.schemaUrl}
|
||||||
handleSubmit={this.handleSubmit}
|
handleSubmit={this.handleSubmit}
|
||||||
handleAction={this.props.handleAction}
|
handleAction={this.props.handleAction}
|
||||||
@ -74,17 +74,16 @@ class FormBuilderModal extends SilverStripeComponent {
|
|||||||
/**
|
/**
|
||||||
* Handle submitting the form in the Modal
|
* Handle submitting the form in the Modal
|
||||||
*
|
*
|
||||||
* @param {Event} event
|
* @param {Object} data
|
||||||
* @param {object} fieldValues
|
* @param {String} action
|
||||||
* @param {function} submitFn
|
* @param {Function} submitFn The original submit function
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
handleSubmit(event, fieldValues, submitFn) {
|
handleSubmit(data, action, submitFn) {
|
||||||
let promise = null;
|
let promise = null;
|
||||||
if (typeof this.props.handleSubmit === 'function') {
|
if (typeof this.props.handleSubmit === 'function') {
|
||||||
promise = this.props.handleSubmit(event, fieldValues, submitFn);
|
promise = this.props.handleSubmit(data, action, submitFn);
|
||||||
} else {
|
} else {
|
||||||
event.preventDefault();
|
|
||||||
promise = submitFn();
|
promise = submitFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +105,7 @@ class FormBuilderModal extends SilverStripeComponent {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class OptionsetField extends SilverStripeComponent {
|
|||||||
const sourceItem = this.props.source
|
const sourceItem = this.props.source
|
||||||
.find((item, index) => this.getItemKey(item, index) === field.id);
|
.find((item, index) => this.getItemKey(item, index) === field.id);
|
||||||
|
|
||||||
this.props.onChange(event, { id: this.props.id, value: `${sourceItem.value}` });
|
this.props.onChange(sourceItem.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,7 @@ describe('OptionsetField', () => {
|
|||||||
setField.handleChange(event, { id: 'set-one', value: 1 });
|
setField.handleChange(event, { id: 'set-one', value: 1 });
|
||||||
|
|
||||||
expect(setField.props.onChange).toBeCalledWith(
|
expect(setField.props.onChange).toBeCalledWith(
|
||||||
event,
|
'one'
|
||||||
{ id: 'set', value: 'one' }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import SilverStripeComponent from 'lib/SilverStripeComponent';
|
|||||||
import FormAction from 'components/FormAction/FormAction';
|
import FormAction from 'components/FormAction/FormAction';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import Toolbar from 'components/Toolbar/Toolbar';
|
import Toolbar from 'components/Toolbar/Toolbar';
|
||||||
import FormBuilder from 'components/FormBuilder/FormBuilder';
|
import FormBuilderLoader from 'containers/FormBuilderLoader/FormBuilderLoader';
|
||||||
import CampaignAdminList from './CampaignAdminList';
|
import CampaignAdminList from './CampaignAdminList';
|
||||||
|
|
||||||
class CampaignAdmin extends SilverStripeComponent {
|
class CampaignAdmin extends SilverStripeComponent {
|
||||||
@ -56,7 +56,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
// NOOP - Lazy loaded in CampaignAdminList.js
|
// NOOP - Lazy loaded in CampaignAdminList.js
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
// @todo - Lazy load in FormBuilder / GridField
|
// @todo - Lazy load in FormBuilderLoader / GridField
|
||||||
breadcrumbs.push({
|
breadcrumbs.push({
|
||||||
text: i18n._t('Campaigns.EDIT_CAMPAIGN', 'Editing Campaign'),
|
text: i18n._t('Campaigns.EDIT_CAMPAIGN', 'Editing Campaign'),
|
||||||
href: this.getActionRoute(id, view),
|
href: this.getActionRoute(id, view),
|
||||||
@ -137,7 +137,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="campaign-admin">
|
<div className="campaign-admin">
|
||||||
<FormBuilder {...formBuilderProps} />
|
<FormBuilderLoader {...formBuilderProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -184,7 +184,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
<div className="panel panel--padded panel--scrollable flexbox-area-grow form--inline">
|
<div className="panel panel--padded panel--scrollable flexbox-area-grow form--inline">
|
||||||
<FormBuilder {...formBuilderProps} />
|
<FormBuilderLoader {...formBuilderProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -210,7 +210,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
<BreadcrumbComponent multiline crumbs={this.props.breadcrumbs} />
|
<BreadcrumbComponent multiline crumbs={this.props.breadcrumbs} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<div className="panel panel--padded panel--scrollable flexbox-area-grow form--inline">
|
<div className="panel panel--padded panel--scrollable flexbox-area-grow form--inline">
|
||||||
<FormBuilder {...formBuilderProps} />
|
<FormBuilderLoader {...formBuilderProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -218,10 +218,10 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to allow customisation of components being constructed
|
* Hook to allow customisation of components being constructed
|
||||||
* by the Campaign DetailEdit FormBuilder.
|
* by the Campaign DetailEdit FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @param {Object} Component - Component constructor.
|
* @param {Object} Component - Component constructor.
|
||||||
* @param {Object} props - Props passed from FormBuilder.
|
* @param {Object} props - Props passed from FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @return {Object} - Instanciated React component
|
* @return {Object} - Instanciated React component
|
||||||
*/
|
*/
|
||||||
@ -245,10 +245,10 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to allow customisation of components being constructed
|
* Hook to allow customisation of components being constructed
|
||||||
* by the Campaign creation FormBuilder.
|
* by the Campaign creation FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @param {Object} Component - Component constructor.
|
* @param {Object} Component - Component constructor.
|
||||||
* @param {Object} props - Props passed from FormBuilder.
|
* @param {Object} props - Props passed from FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @return {Object} - Instanciated React component
|
* @return {Object} - Instanciated React component
|
||||||
*/
|
*/
|
||||||
@ -272,10 +272,10 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to allow customisation of components being constructed
|
* Hook to allow customisation of components being constructed
|
||||||
* by the Campaign list FormBuilder.
|
* by the Campaign list FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @param object Component - Component constructor.
|
* @param object Component - Component constructor.
|
||||||
* @param object props - Props passed from FormBuilder.
|
* @param object props - Props passed from FormBuilderLoader.
|
||||||
*
|
*
|
||||||
* @return object - Instanciated React component
|
* @return object - Instanciated React component
|
||||||
*/
|
*/
|
||||||
@ -283,7 +283,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
const sectionUrl = this.props.sectionConfig.url;
|
const sectionUrl = this.props.sectionConfig.url;
|
||||||
const typeUrlParam = 'set';
|
const typeUrlParam = 'set';
|
||||||
|
|
||||||
if (props.component === 'GridField') {
|
if (props.schemaComponent === 'GridField') {
|
||||||
const extendedProps = Object.assign({}, props, {
|
const extendedProps = Object.assign({}, props, {
|
||||||
data: Object.assign({}, props.data, {
|
data: Object.assign({}, props.data, {
|
||||||
handleDrillDown: (event, record) => {
|
handleDrillDown: (event, record) => {
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
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 } from 'redux-form';
|
||||||
|
import * as schemaActions from 'state/schema/SchemaActions';
|
||||||
|
import Form from 'components/Form/Form';
|
||||||
|
import FormBuilder, { basePropTypes, schemaPropType } from 'components/FormBuilder/FormBuilder';
|
||||||
|
|
||||||
|
import es6promise from 'es6-promise';
|
||||||
|
es6promise.polyfill();
|
||||||
|
|
||||||
|
class FormBuilderLoader extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmitSuccess = this.handleSubmitSuccess.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.schemaUrl !== prevProps.schemaUrl) {
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmitSuccess(result) {
|
||||||
|
this.props.schemaActions.setSchema(result);
|
||||||
|
|
||||||
|
if (this.props.onSubmitSuccess) {
|
||||||
|
this.props.onSubmitSuccess(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 === true) {
|
||||||
|
headerValues.push('schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === true) {
|
||||||
|
headerValues.push('state');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(this.props.schemaUrl, {
|
||||||
|
headers: { 'X-FormSchema-Request': headerValues.join() },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(formSchema => {
|
||||||
|
if (typeof formSchema.id !== 'undefined') {
|
||||||
|
this.props.schemaActions.setSchema(formSchema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// If the response from fetching the initial data
|
||||||
|
// hasn't come back yet, don't render anything.
|
||||||
|
if (!this.props.schema) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = Object.assign({}, this.props, {
|
||||||
|
onSubmitSuccess: this.handleSubmitSuccess,
|
||||||
|
});
|
||||||
|
return <FormBuilder {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormBuilderLoader.propTypes = Object.assign({}, basePropTypes, {
|
||||||
|
schemaActions: PropTypes.object.isRequired,
|
||||||
|
schemaUrl: PropTypes.string.isRequired,
|
||||||
|
schema: schemaPropType,
|
||||||
|
form: PropTypes.string,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
(state, ownProps) => {
|
||||||
|
const schema = state.schemas[ownProps.schemaUrl];
|
||||||
|
const form = schema ? schema.id : null;
|
||||||
|
return { schema, form };
|
||||||
|
},
|
||||||
|
(dispatch) => ({
|
||||||
|
schemaActions: bindActionCreators(schemaActions, dispatch),
|
||||||
|
})
|
||||||
|
)(FormBuilderLoader);
|
12
admin/client/src/containers/FormBuilderLoader/README.md
Normal file
12
admin/client/src/containers/FormBuilderLoader/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# FormBuilderLoader Component
|
||||||
|
|
||||||
|
Used to retrieve a schema for FormBuilder to generate forms made up of field components and actions.
|
||||||
|
Wraps a [FormBuilder](../../components/FormBuilder/README.md] component with async loading logic,
|
||||||
|
and stores the loaded schemas in a Redux store.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
* `schemaUrl` (string): The schema URL where the form will be scaffolded from e.g. '/admin/pages/schema/1'.
|
||||||
|
* `schemaActions` (object): A `setSchema()` function to interact with the redux store.
|
||||||
|
|
||||||
|
See [FormBuilder](../../components/FormBuilder/README.md] for more properties.
|
@ -1,6 +0,0 @@
|
|||||||
import { reduxForm } from 'redux-form';
|
|
||||||
import Form from 'components/Form/Form';
|
|
||||||
|
|
||||||
export default reduxForm({
|
|
||||||
// configured at runtime
|
|
||||||
})(Form);
|
|
@ -1,9 +0,0 @@
|
|||||||
export const ACTION_TYPES = {
|
|
||||||
ADD_FORM: 'ADD_FORM',
|
|
||||||
REMOVE_FORM: 'REMOVE_FORM',
|
|
||||||
SUBMIT_FORM_FAILURE: 'SUBMIT_FORM_FAILURE',
|
|
||||||
SUBMIT_FORM_REQUEST: 'SUBMIT_FORM_REQUEST',
|
|
||||||
SUBMIT_FORM_SUCCESS: 'SUBMIT_FORM_SUCCESS',
|
|
||||||
UPDATE_FIELD: 'UPDATE_FIELD',
|
|
||||||
SET_SUBMIT_ACTION: 'SET_SUBMIT_ACTION',
|
|
||||||
};
|
|
@ -1,96 +0,0 @@
|
|||||||
import { ACTION_TYPES } from './FormActionTypes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a form from state.
|
|
||||||
* This action should be called when a Redux managed Form component unmounts.
|
|
||||||
*
|
|
||||||
* @param {string} formId - ID of the form you want to remove.
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
export function removeForm(formId) {
|
|
||||||
return (dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.REMOVE_FORM,
|
|
||||||
payload: { formId },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets one or more values on an existing form field.
|
|
||||||
*
|
|
||||||
* @param {string} formId - Id of the form where the field lives.
|
|
||||||
* @param {object} updates - The values to update on the field.
|
|
||||||
* @param {string} updates.id - Field ID.
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
export function updateField(formId, updates) {
|
|
||||||
return (dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.UPDATE_FIELD,
|
|
||||||
payload: { formId, updates },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a form to the store.
|
|
||||||
*
|
|
||||||
* @param {object} formState
|
|
||||||
* @param {string} formState.id - The ID the form will be keyed as in state.
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
export function addForm(formState) {
|
|
||||||
return (dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.ADD_FORM,
|
|
||||||
payload: { formState },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits a form and handles the response.
|
|
||||||
*
|
|
||||||
* @param {Function} submitApi
|
|
||||||
* @param {String} formId
|
|
||||||
*/
|
|
||||||
export function submitForm(submitApi, formId, fieldValues) {
|
|
||||||
return (dispatch) => {
|
|
||||||
const headers = {
|
|
||||||
'X-Formschema-Request': 'schema,state',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
};
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_REQUEST,
|
|
||||||
payload: { formId },
|
|
||||||
});
|
|
||||||
|
|
||||||
return submitApi(Object.assign({ ID: formId }, fieldValues), headers)
|
|
||||||
.then((response) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_SUCCESS,
|
|
||||||
payload: { response },
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error.response.text().then((errorText) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_FAILURE,
|
|
||||||
payload: { formId, error: errorText },
|
|
||||||
});
|
|
||||||
return errorText;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSubmitAction(formId, submitAction) {
|
|
||||||
return (dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION_TYPES.SET_SUBMIT_ACTION,
|
|
||||||
payload: { formId, submitAction },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import deepFreeze from 'deep-freeze-strict';
|
|
||||||
import { ACTION_TYPES } from './FormActionTypes';
|
|
||||||
|
|
||||||
const initialState = deepFreeze({});
|
|
||||||
|
|
||||||
function formReducer(state = initialState, action) {
|
|
||||||
const updateForm = (formId, data) => Object.assign({},
|
|
||||||
state, {
|
|
||||||
[formId]: Object.assign({},
|
|
||||||
state[formId],
|
|
||||||
data
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case ACTION_TYPES.SUBMIT_FORM_REQUEST:
|
|
||||||
return deepFreeze(updateForm(action.payload.formId, {
|
|
||||||
error: false,
|
|
||||||
submitting: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.REMOVE_FORM:
|
|
||||||
return deepFreeze(Object.keys(state).reduce((previous, current) => {
|
|
||||||
if (current === action.payload.formId) {
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
return Object.assign({}, previous, {
|
|
||||||
[current]: state[current],
|
|
||||||
});
|
|
||||||
}, {}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.ADD_FORM:
|
|
||||||
return deepFreeze(Object.assign({}, state, {
|
|
||||||
[action.payload.formState.id]: {
|
|
||||||
fields: action.payload.formState.fields,
|
|
||||||
error: false,
|
|
||||||
submitting: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.UPDATE_FIELD:
|
|
||||||
return deepFreeze(updateForm(action.payload.formId, {
|
|
||||||
fields: state[action.payload.formId].fields.map((field) => {
|
|
||||||
if (field.id === action.payload.updates.id) {
|
|
||||||
return Object.assign({}, field, action.payload.updates);
|
|
||||||
}
|
|
||||||
return field;
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.SUBMIT_FORM_SUCCESS:
|
|
||||||
return deepFreeze(updateForm(action.payload.response.id, {
|
|
||||||
fields: action.payload.response.state.fields,
|
|
||||||
error: false,
|
|
||||||
messages: action.payload.response.state.messages,
|
|
||||||
submitting: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.SUBMIT_FORM_FAILURE:
|
|
||||||
return deepFreeze(updateForm(action.payload.formId, {
|
|
||||||
error: true,
|
|
||||||
messages: action.payload.error,
|
|
||||||
submitting: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
case ACTION_TYPES.SET_SUBMIT_ACTION:
|
|
||||||
return deepFreeze(updateForm(action.payload.formId, {
|
|
||||||
submitAction: action.payload.submitAction,
|
|
||||||
}));
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default formReducer;
|
|
@ -1,24 +0,0 @@
|
|||||||
# form
|
|
||||||
|
|
||||||
This state key holds form and form field data. Forms built using the `FormBuilder` component
|
|
||||||
have their state stored in child keys of `form` (keyed by form ID) automatically.
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
form: {
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: "Form_DetailEditForm_Name",
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: "My Campaign"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Forms built using `FormBuilder` will tidy up their state when unmounted.
|
|
@ -1,233 +0,0 @@
|
|||||||
/* global jest, describe, expect, it, beforeEach */
|
|
||||||
|
|
||||||
jest.unmock('deep-freeze-strict');
|
|
||||||
jest.unmock('../FormReducer');
|
|
||||||
jest.unmock('../FormActionTypes');
|
|
||||||
|
|
||||||
import deepFreeze from 'deep-freeze-strict';
|
|
||||||
import { ACTION_TYPES } from '../FormActionTypes';
|
|
||||||
import formReducer from '../FormReducer';
|
|
||||||
|
|
||||||
describe('formReducer', () => {
|
|
||||||
describe('ADD_FORM', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submitting: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a form', () => {
|
|
||||||
const payload = {
|
|
||||||
formState: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_EditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: 'EditForm',
|
|
||||||
messages: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.ADD_FORM,
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm).toBeDefined();
|
|
||||||
expect(nextState.EditForm).toBeDefined();
|
|
||||||
expect(nextState.EditForm.fields).toBeDefined();
|
|
||||||
expect(nextState.EditForm.fields[0].data).toBeDefined();
|
|
||||||
expect(nextState.EditForm.fields[0].id).toBe('Form_EditForm_Name');
|
|
||||||
expect(nextState.EditForm.fields[0].messages).toBeDefined();
|
|
||||||
expect(nextState.EditForm.fields[0].valid).toBe(true);
|
|
||||||
expect(nextState.EditForm.fields[0].value).toBe('Test');
|
|
||||||
expect(nextState.EditForm.submitting).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('REMOVE_FORM', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
EditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_EditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove the form', () => {
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.REMOVE_FORM,
|
|
||||||
payload: { formId: 'DetailEditForm' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm).toBeUndefined();
|
|
||||||
expect(nextState.EditForm).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('UPDATE_FIELD', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update properties of a form field', () => {
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.UPDATE_FIELD,
|
|
||||||
payload: {
|
|
||||||
formId: 'DetailEditForm',
|
|
||||||
updates: {
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
value: 'Updated',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm.fields[0].value).toBe('Updated');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SUBMIT_FORM_SUCCESS', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submitting: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add top level form messages', () => {
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_SUCCESS,
|
|
||||||
payload: {
|
|
||||||
id: 'DetailEditForm',
|
|
||||||
response: {
|
|
||||||
id: 'DetailEditForm',
|
|
||||||
state: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
type: 'good',
|
|
||||||
value: 'Saved.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm.messages).toBeDefined();
|
|
||||||
expect(nextState.DetailEditForm.messages.length).toBe(1);
|
|
||||||
expect(nextState.DetailEditForm.messages[0].type).toBe('good');
|
|
||||||
expect(nextState.DetailEditForm.messages[0].value).toBe('Saved.');
|
|
||||||
expect(nextState.DetailEditForm.submitting).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SUBMIT_FORM_REQUEST', () => {
|
|
||||||
it('should set submitting to true', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submitting: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_REQUEST,
|
|
||||||
payload: { formId: 'DetailEditForm' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm.submitting).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SUBMIT_FORM_FAILURE', () => {
|
|
||||||
it('should set submitting to false', () => {
|
|
||||||
const initialState = deepFreeze({
|
|
||||||
DetailEditForm: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
data: [],
|
|
||||||
id: 'Form_DetailEditForm_Name',
|
|
||||||
messages: [],
|
|
||||||
valid: true,
|
|
||||||
value: 'Test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submitting: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextState = formReducer(initialState, {
|
|
||||||
type: ACTION_TYPES.SUBMIT_FORM_FAILURE,
|
|
||||||
payload: { formId: 'DetailEditForm' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.DetailEditForm.submitting).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -2,6 +2,8 @@ import ACTION_TYPES from './SchemaActionTypes';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the schema being used to generate the current form layout.
|
* Sets the schema being used to generate the current form layout.
|
||||||
|
* Note that the `state` key just determines the initial form field values,
|
||||||
|
* and is overruled by redux-form behaviour (stored in separate reducer key)
|
||||||
*
|
*
|
||||||
* @param string schema - JSON schema for the layout.
|
* @param string schema - JSON schema for the layout.
|
||||||
*/
|
*/
|
||||||
|
70
npm-shrinkwrap.json
generated
70
npm-shrinkwrap.json
generated
@ -8388,6 +8388,76 @@
|
|||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz"
|
"resolved": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz"
|
||||||
},
|
},
|
||||||
|
"redux-form": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-6.0.5.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"array-findindex-polyfill": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-findindex-polyfill/-/array-findindex-polyfill-0.1.0.tgz"
|
||||||
|
},
|
||||||
|
"es6-error": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-3.1.0.tgz"
|
||||||
|
},
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz"
|
||||||
|
},
|
||||||
|
"invariant": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.2.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-promise": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz"
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.0.tgz"
|
||||||
|
},
|
||||||
|
"lodash-es": {
|
||||||
|
"version": "4.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.16.0.tgz"
|
||||||
|
},
|
||||||
|
"shallowequal": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.keys": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash._getnative": {
|
||||||
|
"version": "3.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz"
|
||||||
|
},
|
||||||
|
"lodash.isarguments": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz"
|
||||||
|
},
|
||||||
|
"lodash.isarray": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"redux-logger": {
|
"redux-logger": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-2.6.1.tgz"
|
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-2.6.1.tgz"
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"react-router": "^2.4.1",
|
"react-router": "^2.4.1",
|
||||||
"react-router-redux": "^4.0.5",
|
"react-router-redux": "^4.0.5",
|
||||||
"redux": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz",
|
"redux": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz",
|
||||||
|
"redux-form": "^6.0.2",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
"tether": "^1.3.2",
|
"tether": "^1.3.2",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
@ -104,7 +105,8 @@
|
|||||||
"mocksPattern": "mocks",
|
"mocksPattern": "mocks",
|
||||||
"unmockedModulePathPatterns": [
|
"unmockedModulePathPatterns": [
|
||||||
"<rootDir>/node_modules/react",
|
"<rootDir>/node_modules/react",
|
||||||
"<rootDir>/node_modules/qs"
|
"<rootDir>/node_modules/qs",
|
||||||
|
"<rootDir>/node_modules/redux-form"
|
||||||
],
|
],
|
||||||
"bail": true,
|
"bail": true,
|
||||||
"testRunner": "<rootDir>/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js"
|
"testRunner": "<rootDir>/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js"
|
||||||
|
@ -68,6 +68,7 @@ const config = [
|
|||||||
'components/FormBuilderModal/FormBuilderModal': 'FormBuilderModal',
|
'components/FormBuilderModal/FormBuilderModal': 'FormBuilderModal',
|
||||||
'components/GridField/GridField': 'GridField',
|
'components/GridField/GridField': 'GridField',
|
||||||
'components/Toolbar/Toolbar': 'Toolbar',
|
'components/Toolbar/Toolbar': 'Toolbar',
|
||||||
|
'containers/FormBuilderLoader/FormBuilderLoader': 'FormBuilderLoader',
|
||||||
'deep-freeze-strict': 'DeepFreezeStrict',
|
'deep-freeze-strict': 'DeepFreezeStrict',
|
||||||
i18n: 'i18n',
|
i18n: 'i18n',
|
||||||
jQuery: 'jQuery',
|
jQuery: 'jQuery',
|
||||||
@ -85,6 +86,7 @@ const config = [
|
|||||||
'react-router': 'ReactRouter',
|
'react-router': 'ReactRouter',
|
||||||
'react-addons-css-transition-group': 'ReactAddonsCssTransitionGroup',
|
'react-addons-css-transition-group': 'ReactAddonsCssTransitionGroup',
|
||||||
react: 'React',
|
react: 'React',
|
||||||
|
'redux-form': 'ReduxForm',
|
||||||
'redux-thunk': 'ReduxThunk',
|
'redux-thunk': 'ReduxThunk',
|
||||||
redux: 'Redux',
|
redux: 'Redux',
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
|
Loading…
Reference in New Issue
Block a user