mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-29 20:59:23 +02:00
Add DetailEditForm to Campaign admin
- Add edit form to campaigns section - Handle form submissions with FormBuilder - Handle form state via Redux - Garbage collect form state - Removes $itemID as a required param for schema requests. Developers should be able to scaffold forms without populating values from an existing record. For example when building a form for creating new records.
This commit is contained in:
parent
e01846d418
commit
7fcdf35438
@ -6,6 +6,7 @@ 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 FormsReducer from 'state/forms/FormsReducer';
|
||||||
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';
|
||||||
@ -16,6 +17,7 @@ import CampaignAdmin from 'containers/CampaignAdmin/index';
|
|||||||
|
|
||||||
function appBoot() {
|
function appBoot() {
|
||||||
reducerRegister.add('config', ConfigReducer);
|
reducerRegister.add('config', ConfigReducer);
|
||||||
|
reducerRegister.add('forms', FormsReducer);
|
||||||
reducerRegister.add('schemas', SchemaReducer);
|
reducerRegister.add('schemas', SchemaReducer);
|
||||||
reducerRegister.add('records', RecordsReducer);
|
reducerRegister.add('records', RecordsReducer);
|
||||||
reducerRegister.add('campaign', CampaignReducer);
|
reducerRegister.add('campaign', CampaignReducer);
|
||||||
|
@ -4,7 +4,7 @@ Used for form actions. For example a submit button.
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
### handleClick (function - required)
|
### handleClick (function)
|
||||||
|
|
||||||
The handler for when a button is clicked
|
The handler for when a button is clicked
|
||||||
|
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
import * as formsActions from 'state/forms/FormsActions';
|
||||||
import * as schemaActions from 'state/schema/SchemaActions';
|
import * as schemaActions from 'state/schema/SchemaActions';
|
||||||
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||||
import FormComponent from 'components/Form/Form';
|
import FormComponent from 'components/Form/Form';
|
||||||
|
import FormActionComponent from 'components/FormAction/FormAction';
|
||||||
import TextField from 'components/TextField/TextField';
|
import TextField from 'components/TextField/TextField';
|
||||||
import HiddenField from 'components/HiddenField/HiddenField';
|
import HiddenField from 'components/HiddenField/HiddenField';
|
||||||
import GridField from 'components/GridField/GridField';
|
import GridField from 'components/GridField/GridField';
|
||||||
import fetch from 'isomorphic-fetch';
|
import fetch from 'isomorphic-fetch';
|
||||||
import deepFreeze from 'deep-freeze';
|
import deepFreeze from 'deep-freeze';
|
||||||
|
import backend from 'lib/Backend';
|
||||||
|
|
||||||
import es6promise from 'es6-promise';
|
import es6promise from 'es6-promise';
|
||||||
es6promise.polyfill();
|
es6promise.polyfill();
|
||||||
@ -47,7 +50,7 @@ const fakeInjector = {
|
|||||||
*/
|
*/
|
||||||
getComponentByDataType(dataType) {
|
getComponentByDataType(dataType) {
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case 'String':
|
case 'Text':
|
||||||
return this.components.TextField;
|
return this.components.TextField;
|
||||||
case 'Hidden':
|
case 'Hidden':
|
||||||
return this.components.HiddenField;
|
return this.components.HiddenField;
|
||||||
@ -66,7 +69,12 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
|
|
||||||
this.formSchemaPromise = null;
|
this.formSchemaPromise = null;
|
||||||
this.state = { isFetching: false };
|
this.state = { isFetching: false };
|
||||||
|
|
||||||
|
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.removeForm = this.removeForm.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -82,7 +90,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
*
|
*
|
||||||
* @return object - Promise from the AJAX request.
|
* @return object - Promise from the AJAX request.
|
||||||
*/
|
*/
|
||||||
fetch(schema = true, state = false) {
|
fetch(schema = true, state = true) {
|
||||||
const headerValues = [];
|
const headerValues = [];
|
||||||
|
|
||||||
if (this.state.isFetching === true) {
|
if (this.state.isFetching === true) {
|
||||||
@ -103,9 +111,34 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(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
|
// TODO See "Enable once <CampaignAdmin> ..." below
|
||||||
this.setState({ isFetching: false });
|
// this.setState({ isFetching: false });
|
||||||
this.props.actions.setSchema(json);
|
|
||||||
|
if (typeof formSchema.id !== 'undefined') {
|
||||||
|
const defaultData = {
|
||||||
|
ID: formSchema.schema.id,
|
||||||
|
SecurityID: this.props.config.SecurityID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formSchema.schema.actions.length > 0) {
|
||||||
|
defaultData[formSchema.schema.actions[0].name] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitApi = backend.createEndpointFetcher({
|
||||||
|
url: formSchema.schema.attributes.action,
|
||||||
|
method: formSchema.schema.attributes.method,
|
||||||
|
defaultData,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.schemaActions.setSchema(formSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof formState.id !== 'undefined') {
|
||||||
|
this.props.formsActions.addForm(formState);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO Enable once <CampaignAdmin> is initialised via page.js route callbacks
|
// TODO Enable once <CampaignAdmin> is initialised via page.js route callbacks
|
||||||
@ -118,6 +151,101 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
return this.formSchemaPromise;
|
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 !== 'undefined') {
|
||||||
|
fn(this.props.formId, this.props.formsActions.updateField);
|
||||||
|
} else {
|
||||||
|
this.props.formsActions.updateField(this.props.formId, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form submission handler passed to the Form Component as a prop.
|
||||||
|
* Provides a hook for controllers to access for state and provide custom functionality.
|
||||||
|
*
|
||||||
|
* For example:
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
handleSubmit(event) {
|
||||||
|
const schemaFields = this.props.schemas[this.props.schemaUrl].schema.fields;
|
||||||
|
const fieldValues = this.props.forms[this.props.formId].fields
|
||||||
|
.reduce((prev, curr) => Object.assign({}, prev, {
|
||||||
|
[schemaFields.find(schemaField => schemaField.id === curr.id).name]: curr.value,
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const submitFn = () => {
|
||||||
|
this.props.formsActions.submitForm(
|
||||||
|
this.submitApi,
|
||||||
|
this.props.formId,
|
||||||
|
fieldValues
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof this.props.handleSubmit !== 'undefined') {
|
||||||
|
this.props.handleSubmit(event, fieldValues, submitFn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
submitFn();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a list of schema fields to their React Component.
|
* Maps a list of schema fields to their React Component.
|
||||||
* Only top level form fields are handled here, composite fields (TabSets etc),
|
* Only top level form fields are handled here, composite fields (TabSets etc),
|
||||||
@ -129,6 +257,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
*/
|
*/
|
||||||
mapFieldsToComponents(fields) {
|
mapFieldsToComponents(fields) {
|
||||||
const createFn = this.props.createFn;
|
const createFn = this.props.createFn;
|
||||||
|
const handleFieldUpdate = this.handleFieldUpdate;
|
||||||
|
|
||||||
return fields.map((field, i) => {
|
return fields.map((field, i) => {
|
||||||
const Component = field.component !== null
|
const Component = field.component !== null
|
||||||
@ -142,7 +271,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
// Props which every form field receives.
|
// Props which every form field receives.
|
||||||
// Leave it up to the schema and component to determine
|
// Leave it up to the schema and component to determine
|
||||||
// which props are required.
|
// which props are required.
|
||||||
const props = deepFreeze(field);
|
const props = deepFreeze(Object.assign({}, field, { handleFieldUpdate }));
|
||||||
|
|
||||||
// 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.
|
||||||
@ -154,8 +283,58 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a list of form actions to their React Component.
|
||||||
|
*
|
||||||
|
* @param array actions
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
mapActionsToComponents(actions) {
|
||||||
|
const createFn = this.props.createFn;
|
||||||
|
|
||||||
|
return actions.map((action, i) => {
|
||||||
|
const props = deepFreeze(action);
|
||||||
|
|
||||||
|
if (typeof createFn === 'function') {
|
||||||
|
return createFn(FormActionComponent, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FormActionComponent key={i} {...props} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the structural and state data of a form field.
|
||||||
|
* The structure of the objects being merged should match the structures
|
||||||
|
* generated by the SilverStripe FormSchema.
|
||||||
|
*
|
||||||
|
* @param object structure - Structural data for a single field.
|
||||||
|
* @param object state - State data for a single field.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
mergeFieldData(structure, state) {
|
||||||
|
return Object.assign({}, structure, {
|
||||||
|
data: Object.assign({}, structure.data, state.data),
|
||||||
|
messages: state.messages,
|
||||||
|
valid: state.valid,
|
||||||
|
value: state.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up Redux state used by the form when the Form component is unmonuted.
|
||||||
|
*
|
||||||
|
* @param {string} formId - ID of the form to clean up.
|
||||||
|
*/
|
||||||
|
removeForm(formId) {
|
||||||
|
this.props.formsActions.removeForm(formId);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const formSchema = this.props.schemas[this.props.schemaUrl];
|
const formSchema = this.props.schemas[this.props.schemaUrl];
|
||||||
|
const formState = this.props.forms[this.props.formId];
|
||||||
|
|
||||||
// If the response from fetching the initial data
|
// If the response from fetching the initial data
|
||||||
// hasn't come back yet, don't render anything.
|
// hasn't come back yet, don't render anything.
|
||||||
@ -172,11 +351,21 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
encType: formSchema.schema.attributes.enctype,
|
encType: formSchema.schema.attributes.enctype,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If there is structural and state data availabe merge those data for each field.
|
||||||
|
// Otherwise just use the structural data.
|
||||||
|
const fieldData = formSchema.schema && formState
|
||||||
|
? formSchema.schema.fields.map((f, i) => this.mergeFieldData(f, formState.fields[i]))
|
||||||
|
: formSchema.schema.fields;
|
||||||
|
|
||||||
const formProps = {
|
const formProps = {
|
||||||
actions: formSchema.schema.actions,
|
actions: formSchema.schema.actions,
|
||||||
attributes,
|
attributes,
|
||||||
|
componentWillUnmount: this.removeForm,
|
||||||
data: formSchema.schema.data,
|
data: formSchema.schema.data,
|
||||||
fields: formSchema.schema.fields,
|
fields: fieldData,
|
||||||
|
formId: formSchema.id,
|
||||||
|
handleSubmit: this.handleSubmit,
|
||||||
|
mapActionsToComponents: this.mapActionsToComponents,
|
||||||
mapFieldsToComponents: this.mapFieldsToComponents,
|
mapFieldsToComponents: this.mapFieldsToComponents,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,21 +374,29 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormBuilderComponent.propTypes = {
|
FormBuilderComponent.propTypes = {
|
||||||
actions: React.PropTypes.object.isRequired,
|
config: React.PropTypes.object,
|
||||||
createFn: React.PropTypes.func,
|
createFn: React.PropTypes.func,
|
||||||
schemaUrl: React.PropTypes.string.isRequired,
|
forms: React.PropTypes.object.isRequired,
|
||||||
|
formsActions: React.PropTypes.object.isRequired,
|
||||||
|
formId: React.PropTypes.string.isRequired,
|
||||||
|
handleSubmit: React.PropTypes.func,
|
||||||
schemas: React.PropTypes.object.isRequired,
|
schemas: React.PropTypes.object.isRequired,
|
||||||
|
schemaActions: React.PropTypes.object.isRequired,
|
||||||
|
schemaUrl: React.PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
|
config: state.config,
|
||||||
|
forms: state.forms,
|
||||||
schemas: state.schemas,
|
schemas: state.schemas,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators(schemaActions, dispatch),
|
formsActions: bindActionCreators(formsActions, dispatch),
|
||||||
|
schemaActions: bindActionCreators(schemaActions, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,3 +25,7 @@ The schema URL where the form will be scaffolded from e.g. '/admin/pages/schema/
|
|||||||
### schema
|
### schema
|
||||||
|
|
||||||
JSON schema representing the form. Used as the blueprint for generating the form.
|
JSON schema representing the form. Used as the blueprint for generating the form.
|
||||||
|
|
||||||
|
### onSubmit (func)
|
||||||
|
|
||||||
|
Event handler passed to the Form Component as a prop.
|
||||||
|
@ -110,8 +110,8 @@ class GridField extends SilverStripeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param number int
|
* @param object event
|
||||||
* @param event
|
* @param number id
|
||||||
*/
|
*/
|
||||||
deleteRecord(event, id) {
|
deleteRecord(event, id) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -123,9 +123,19 @@ class GridField extends SilverStripeComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
editRecord(event) {
|
/**
|
||||||
|
* @param object event
|
||||||
|
* @param number id
|
||||||
|
*/
|
||||||
|
editRecord(event, id) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// TODO
|
|
||||||
|
if (typeof this.props.data === 'undefined' ||
|
||||||
|
typeof this.props.data.handleEditRecord === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.data.handleEditRecord(event, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -136,6 +146,7 @@ GridField.propTypes = {
|
|||||||
headerColumns: React.PropTypes.array,
|
headerColumns: React.PropTypes.array,
|
||||||
collectionReadEndpoint: React.PropTypes.object,
|
collectionReadEndpoint: React.PropTypes.object,
|
||||||
handleDrillDown: React.PropTypes.func,
|
handleDrillDown: React.PropTypes.func,
|
||||||
|
handleEditRecord: React.PropTypes.func,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ Generates an editable text field.
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
### label
|
### leftTitle
|
||||||
|
|
||||||
The label text to display with the field.
|
The label text to display with the field.
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ Addition CSS classes to apply to the `<input>` element.
|
|||||||
|
|
||||||
Used for the field's `name` attribute.
|
Used for the field's `name` attribute.
|
||||||
|
|
||||||
### onChange
|
### handleFieldUpdate
|
||||||
|
|
||||||
Handler function called when the field's value changes.
|
Handler function called when the field's value changes.
|
||||||
|
|
||||||
|
@ -10,11 +10,15 @@ class TextField extends SilverStripeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const labelText = this.props.leftTitle !== null
|
||||||
|
? this.props.leftTitle
|
||||||
|
: this.props.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="field text">
|
<div className="field text">
|
||||||
{this.props.label &&
|
{labelText &&
|
||||||
<label className="left" htmlFor={`gallery_${this.props.name}`}>
|
<label className="left" htmlFor={`gallery_${this.props.name}`}>
|
||||||
{this.props.label}
|
{labelText}
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
<div className="middleColumn">
|
<div className="middleColumn">
|
||||||
@ -29,26 +33,31 @@ class TextField extends SilverStripeComponent {
|
|||||||
className: ['text', this.props.extraClass].join(' '),
|
className: ['text', this.props.extraClass].join(' '),
|
||||||
id: `gallery_${this.props.name}`,
|
id: `gallery_${this.props.name}`,
|
||||||
name: this.props.name,
|
name: this.props.name,
|
||||||
onChange: this.props.onChange,
|
onChange: this.handleChange,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange() {
|
/**
|
||||||
if (typeof this.props.onChange === 'undefined') {
|
* Handles changes to the text field's value.
|
||||||
|
*
|
||||||
|
* @param object event
|
||||||
|
*/
|
||||||
|
handleChange(event) {
|
||||||
|
if (typeof this.props.handleFieldUpdate === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange();
|
this.props.handleFieldUpdate(event, { id: this.props.id, value: event.target.value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.propTypes = {
|
TextField.propTypes = {
|
||||||
label: React.PropTypes.string,
|
leftTitle: React.PropTypes.string,
|
||||||
extraClass: React.PropTypes.string,
|
extraClass: React.PropTypes.string,
|
||||||
name: React.PropTypes.string.isRequired,
|
name: React.PropTypes.string.isRequired,
|
||||||
onChange: React.PropTypes.func,
|
handleFieldUpdate: React.PropTypes.func,
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ describe('TextField', () => {
|
|||||||
label: '',
|
label: '',
|
||||||
name: '',
|
name: '',
|
||||||
value: '',
|
value: '',
|
||||||
onChange: jest.genMockFunction(),
|
handleFieldUpdate: jest.genMockFunction(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,10 +29,10 @@ describe('TextField', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the onChange function on props', () => {
|
it('should call the handleFieldUpdate function on props', () => {
|
||||||
textField.handleChange();
|
textField.handleChange({ target: { value: '' } });
|
||||||
|
|
||||||
expect(textField.props.onChange.mock.calls.length).toBe(1);
|
expect(textField.props.handleFieldUpdate).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||||
import FormAction from 'components/FormAction/FormAction';
|
|
||||||
|
|
||||||
class Form extends SilverStripeComponent {
|
class Form extends SilverStripeComponent {
|
||||||
|
|
||||||
/**
|
constructor(props) {
|
||||||
* Gets the components responsible for perfoming actions on the form.
|
super(props);
|
||||||
* For example form submission.
|
|
||||||
*
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
* @return array|null
|
}
|
||||||
*/
|
|
||||||
getFormAction() {
|
componentWillUnmount() {
|
||||||
return this.props.actions.map((action) =>
|
if (typeof this.props.componentWillUnmount === 'undefined') {
|
||||||
<FormAction {...action} />
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
this.props.componentWillUnmount(this.props.formId);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const attr = this.props.attributes;
|
const props = Object.assign({ onSubmit: this.handleSubmit }, this.props.attributes);
|
||||||
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
||||||
const actions = this.getFormAction();
|
const actions = this.props.mapActionsToComponents(this.props.actions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form {...attr}>
|
<form {...props}>
|
||||||
{fields &&
|
{fields &&
|
||||||
<fieldset className="form-group">
|
<fieldset className="form-group">
|
||||||
{fields}
|
{fields}
|
||||||
@ -40,6 +41,14 @@ class Form extends SilverStripeComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
if (typeof this.props.handleSubmit === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.handleSubmit(event);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Form.propTypes = {
|
Form.propTypes = {
|
||||||
@ -51,8 +60,12 @@ 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,
|
data: React.PropTypes.array,
|
||||||
fields: React.PropTypes.array.isRequired,
|
fields: React.PropTypes.array.isRequired,
|
||||||
|
formId: React.PropTypes.string.isRequired,
|
||||||
|
handleSubmit: React.PropTypes.func,
|
||||||
|
mapActionsToComponents: React.PropTypes.func.isRequired,
|
||||||
mapFieldsToComponents: React.PropTypes.func.isRequired,
|
mapFieldsToComponents: React.PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,6 +24,12 @@ An object of HTML attributes for the form. For example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
### data
|
### data
|
||||||
|
|
||||||
Ad hoc data passed to the front-end from the server.
|
Ad hoc data passed to the front-end from the server.
|
||||||
@ -35,3 +41,7 @@ A list of field objects to display in the form. These objects should be transfor
|
|||||||
### mapFieldsToComponents (required)
|
### mapFieldsToComponents (required)
|
||||||
|
|
||||||
A function that maps each schema field (`this.props.fields`) to the component responsibe for render it.
|
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.
|
||||||
|
@ -16,7 +16,6 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.addCampaign = this.addCampaign.bind(this);
|
this.addCampaign = this.addCampaign.bind(this);
|
||||||
this.createFn = this.createFn.bind(this);
|
|
||||||
this.publishApi = backend.createEndpointFetcher({
|
this.publishApi = backend.createEndpointFetcher({
|
||||||
url: this.props.sectionConfig.publishEndpoint.url,
|
url: this.props.sectionConfig.publishEndpoint.url,
|
||||||
method: this.props.sectionConfig.publishEndpoint.method,
|
method: this.props.sectionConfig.publishEndpoint.method,
|
||||||
@ -25,6 +24,8 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
id: { urlReplacement: ':id', remove: true },
|
id: { urlReplacement: ':id', remove: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.campaignListCreateFn = this.campaignListCreateFn.bind(this);
|
||||||
|
this.campaignEditCreateFn = this.campaignEditCreateFn.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -38,7 +39,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
if (captureRoute) {
|
if (captureRoute) {
|
||||||
// If this component is mounted, then handle all page changes via
|
// If this component is mounted, then handle all page changes via
|
||||||
// state / redux
|
// state / redux
|
||||||
this.props.actions.showCampaignView(ctx.params.id, ctx.params.view);
|
this.props.actions.showCampaignView(ctx.params.id, ctx.params.view);
|
||||||
} else {
|
} else {
|
||||||
// If component is not mounted, we need to allow root routes to load
|
// If component is not mounted, we need to allow root routes to load
|
||||||
// this section in via ajax
|
// this section in via ajax
|
||||||
@ -80,7 +81,17 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
* @return object
|
* @return object
|
||||||
*/
|
*/
|
||||||
renderIndexView() {
|
renderIndexView() {
|
||||||
const schemaUrl = this.props.sectionConfig.forms.editForm.schemaUrl;
|
const schemaUrl = this.props.sectionConfig.forms.EditForm.schemaUrl;
|
||||||
|
const formActionProps = {
|
||||||
|
label: i18n._t('Campaigns.ADDCAMPAIGN'),
|
||||||
|
icon: 'plus',
|
||||||
|
handleClick: this.addCampaign,
|
||||||
|
};
|
||||||
|
const formBuilderProps = {
|
||||||
|
createFn: this.campaignListCreateFn,
|
||||||
|
formId: 'EditForm',
|
||||||
|
schemaUrl,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cms-content__inner no-preview">
|
<div className="cms-content__inner no-preview">
|
||||||
@ -93,17 +104,13 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
<div className="panel-scrollable--single-toolbar">
|
<div className="panel-scrollable--single-toolbar">
|
||||||
<div className="toolbar--content">
|
<div className="toolbar--content">
|
||||||
<div className="btn-toolbar">
|
<div className="btn-toolbar">
|
||||||
<FormAction
|
<FormAction {...formActionProps} />
|
||||||
label={i18n._t('Campaigns.ADDCAMPAIGN')}
|
|
||||||
icon={'plus'}
|
|
||||||
handleClick={this.addCampaign}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormBuilder schemaUrl={schemaUrl} createFn={this.createFn} />
|
<FormBuilder {...formBuilderProps} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,22 +133,38 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo
|
* Renders the Detail Edit Form for a Campaign.
|
||||||
*/
|
*/
|
||||||
renderDetailEditView() {
|
renderDetailEditView() {
|
||||||
return <p>Edit</p>;
|
const baseSchemaUrl = this.props.sectionConfig.forms.DetailEditForm.schemaUrl;
|
||||||
|
const formBuilderProps = {
|
||||||
|
createFn: this.campaignEditCreateFn,
|
||||||
|
formId: 'DetailEditForm',
|
||||||
|
schemaUrl: `${baseSchemaUrl}/ChangeSet/${this.props.campaignId}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cms-middle no-preview">
|
||||||
|
<div className="cms-campaigns collapse in" aria-expanded="true">
|
||||||
|
<NorthHeader />
|
||||||
|
<FormBuilder {...formBuilderProps} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to allow customisation of components being constructed by FormBuilder.
|
* Hook to allow customisation of components being constructed
|
||||||
|
* by the Campaign list FormBuilder.
|
||||||
*
|
*
|
||||||
* @param object Component - Component constructor.
|
* @param object Component - Component constructor.
|
||||||
* @param object props - Props passed from FormBuilder.
|
* @param object props - Props passed from FormBuilder.
|
||||||
*
|
*
|
||||||
* @return object - Instanciated React component
|
* @return object - Instanciated React component
|
||||||
*/
|
*/
|
||||||
createFn(Component, props) {
|
campaignListCreateFn(Component, props) {
|
||||||
const campaignViewRoute = this.props.sectionConfig.campaignViewRoute;
|
const campaignViewRoute = this.props.sectionConfig.campaignViewRoute;
|
||||||
|
const typeUrlParam = 'set';
|
||||||
|
|
||||||
if (props.component === 'GridField') {
|
if (props.component === 'GridField') {
|
||||||
const extendedProps = Object.assign({}, props, {
|
const extendedProps = Object.assign({}, props, {
|
||||||
@ -149,12 +172,20 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
handleDrillDown: (event, record) => {
|
handleDrillDown: (event, record) => {
|
||||||
// Set url and set list
|
// Set url and set list
|
||||||
const path = campaignViewRoute
|
const path = campaignViewRoute
|
||||||
.replace(/:type\?/, 'set')
|
.replace(/:type\?/, typeUrlParam)
|
||||||
.replace(/:id\?/, record.ID)
|
.replace(/:id\?/, record.ID)
|
||||||
.replace(/:view\?/, 'show');
|
.replace(/:view\?/, 'show');
|
||||||
|
|
||||||
window.ss.router.show(path);
|
window.ss.router.show(path);
|
||||||
},
|
},
|
||||||
|
handleEditRecord: (event, id) => {
|
||||||
|
const path = campaignViewRoute
|
||||||
|
.replace(/:type\?/, typeUrlParam)
|
||||||
|
.replace(/:id\?/, id)
|
||||||
|
.replace(/:view\?/, 'edit');
|
||||||
|
|
||||||
|
window.ss.router.show(path);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,6 +211,29 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hook to allow customisation of components being constructed
|
||||||
|
* by the Campaign detail edit FormBuilder.
|
||||||
|
*
|
||||||
|
* @param object Component - Component constructor.
|
||||||
|
* @param object props - Props passed from FormBuilder.
|
||||||
|
*
|
||||||
|
* @return object - Instanciated React component
|
||||||
|
*/
|
||||||
|
campaignEditCreateFn(Component, props) {
|
||||||
|
if (props.name === 'action_save') {
|
||||||
|
const extendedProps = Object.assign({}, props, {
|
||||||
|
type: 'submit',
|
||||||
|
label: props.title,
|
||||||
|
icon: 'save',
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Component key={props.name} {...extendedProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component key={props.name} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets preview URL for itemid
|
* Gets preview URL for itemid
|
||||||
* @param int id
|
* @param int id
|
||||||
@ -201,17 +255,19 @@ class CampaignAdmin extends SilverStripeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CampaignAdmin.propTypes = {
|
CampaignAdmin.propTypes = {
|
||||||
sectionConfig: React.PropTypes.shape({
|
actions: React.PropTypes.object.isRequired,
|
||||||
|
campaignId: React.PropTypes.string,
|
||||||
|
config: React.PropTypes.shape({
|
||||||
forms: React.PropTypes.shape({
|
forms: React.PropTypes.shape({
|
||||||
editForm: React.PropTypes.shape({
|
editForm: React.PropTypes.shape({
|
||||||
schemaUrl: React.PropTypes.string,
|
schemaUrl: React.PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
config: React.PropTypes.shape({
|
|
||||||
SecurityID: React.PropTypes.string,
|
SecurityID: React.PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
sectionConfig: React.PropTypes.object.isRequired,
|
||||||
sectionConfigKey: React.PropTypes.string.isRequired,
|
sectionConfigKey: React.PropTypes.string.isRequired,
|
||||||
|
view: React.PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
|
@ -305,7 +305,7 @@ class Backend {
|
|||||||
? [url, headers]
|
? [url, headers]
|
||||||
: [url, encodedData, headers];
|
: [url, encodedData, headers];
|
||||||
|
|
||||||
return this[refinedSpec.method](...args)
|
return this[refinedSpec.method.toLowerCase()](...args)
|
||||||
.then(parseResponse);
|
.then(parseResponse);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import deepFreeze from 'deep-freeze';
|
import deepFreeze from 'deep-freeze';
|
||||||
import ACTION_TYPES from './CampaignActionTypes';
|
import ACTION_TYPES from './CampaignActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = deepFreeze({
|
||||||
campaignId: null,
|
campaignId: null,
|
||||||
isPublishing: false,
|
isPublishing: false,
|
||||||
view: null,
|
view: null,
|
||||||
};
|
});
|
||||||
|
|
||||||
function reducer(state = initialState, action) {
|
function reducer(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
24
admin/client/src/state/forms/README.md
Normal file
24
admin/client/src/state/forms/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# forms
|
||||||
|
|
||||||
|
This state key holds form and form field data. Forms built using the `FormBuilder` component
|
||||||
|
have their state stored in child keys of `forms` (keyed by form ID) automatically.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
forms: {
|
||||||
|
DetailEditForm: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
id: "Form_DetailEditForm_Name",
|
||||||
|
messages: [],
|
||||||
|
valid: true,
|
||||||
|
value: "My Campaign"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Forms built using `FormBuilder` will tidy up their state when unmounted.
|
8
admin/client/src/state/forms/action-types.js
Normal file
8
admin/client/src/state/forms/action-types.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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',
|
||||||
|
};
|
79
admin/client/src/state/forms/actions.js
Normal file
79
admin/client/src/state/forms/actions.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { ACTION_TYPES } from './action-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTION_TYPES.SUBMIT_FORM_REQUEST,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
submitApi(Object.assign({ ID: formId }, fieldValues))
|
||||||
|
.then(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTION_TYPES.SUBMIT_FORM_SUCCESS,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTION_TYPES.SUBMIT_FORM_FAILURE,
|
||||||
|
payload: { error },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
42
admin/client/src/state/forms/reducer.js
Normal file
42
admin/client/src/state/forms/reducer.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import deepFreeze from 'deep-freeze';
|
||||||
|
import { ACTION_TYPES } from './action-types';
|
||||||
|
|
||||||
|
const initialState = deepFreeze({});
|
||||||
|
|
||||||
|
function formsReducer(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
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 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
case ACTION_TYPES.UPDATE_FIELD:
|
||||||
|
return deepFreeze(Object.assign({}, state, {
|
||||||
|
[action.payload.formId]: Object.assign({}, state[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;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formsReducer;
|
126
admin/client/src/state/forms/tests/reducer-test.js
Normal file
126
admin/client/src/state/forms/tests/reducer-test.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
jest.unmock('deep-freeze');
|
||||||
|
jest.unmock('../reducer');
|
||||||
|
jest.unmock('../action-types');
|
||||||
|
|
||||||
|
import deepFreeze from 'deep-freeze';
|
||||||
|
import { ACTION_TYPES } from '../action-types';
|
||||||
|
import formsReducer from '../reducer';
|
||||||
|
|
||||||
|
describe('formsReducer', () => {
|
||||||
|
|
||||||
|
describe('ADD_FORM', () => {
|
||||||
|
const initialState = deepFreeze({
|
||||||
|
DetailEditForm: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
id: 'Form_DetailEditForm_Name',
|
||||||
|
messages: [],
|
||||||
|
valid: true,
|
||||||
|
value: 'Test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a form', () => {
|
||||||
|
const payload = {
|
||||||
|
formState: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
id: 'Form_EditForm_Name',
|
||||||
|
messages: [],
|
||||||
|
valid: true,
|
||||||
|
value: 'Test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'EditForm',
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextState = formsReducer(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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = formsReducer(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 = formsReducer(initialState, {
|
||||||
|
type: ACTION_TYPES.UPDATE_FIELD,
|
||||||
|
payload: {
|
||||||
|
formId: 'DetailEditForm',
|
||||||
|
updates: {
|
||||||
|
id: 'Form_DetailEditForm_Name',
|
||||||
|
value: 'Updated',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.DetailEditForm.fields[0].value).toBe('Updated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
Manages state associated with the FormFieldSchema.
|
Manages state associated with the FormFieldSchema.
|
||||||
|
|
||||||
When dependency injection is implemented, this will be moved into either Framework or CMS. We can't moveit there sooner because there is no way of extending state.
|
When dependency injection is implemented, this will be moved into either Framework or CMS.
|
||||||
|
We can't move it sooner because there's no way of extending state.
|
||||||
|
|
||||||
|
Note form state is stored under the `forms` _not_ the `schema` key.
|
||||||
|
@ -30,7 +30,7 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
|||||||
'POST set/$ID/publish' => 'publishCampaign',
|
'POST set/$ID/publish' => 'publishCampaign',
|
||||||
'POST set/$ID' => 'createCampaign',
|
'POST set/$ID' => 'createCampaign',
|
||||||
'GET set/$ID/$Name' => 'readCampaign',
|
'GET set/$ID/$Name' => 'readCampaign',
|
||||||
'PUT set/$ID' => 'updateCampaign',
|
'POST $ID' => 'updateCampaign',
|
||||||
'DELETE set/$ID' => 'deleteCampaign',
|
'DELETE set/$ID' => 'deleteCampaign',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -56,8 +56,11 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
|||||||
return array_merge(parent::getClientConfig(), [
|
return array_merge(parent::getClientConfig(), [
|
||||||
'forms' => [
|
'forms' => [
|
||||||
// TODO Use schemaUrl instead
|
// TODO Use schemaUrl instead
|
||||||
'editForm' => [
|
'EditForm' => [
|
||||||
'schemaUrl' => $this->Link('schema/EditForm')
|
'schemaUrl' => $this->Link('schema/EditForm')
|
||||||
|
],
|
||||||
|
'DetailEditForm' => [
|
||||||
|
'schemaUrl' => $this->Link('schema/DetailEditForm')
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'campaignViewRoute' => $this->Link() . ':type?/:id?/:view?',
|
'campaignViewRoute' => $this->Link() . ':type?/:id?/:view?',
|
||||||
@ -325,8 +328,6 @@ JSON;
|
|||||||
return $hal;
|
return $hal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets viewable list of campaigns
|
* Gets viewable list of campaigns
|
||||||
*
|
*
|
||||||
@ -353,21 +354,14 @@ JSON;
|
|||||||
|
|
||||||
if ($request->getHeader('Accept') == 'text/json') {
|
if ($request->getHeader('Accept') == 'text/json') {
|
||||||
$response->addHeader('Content-Type', 'application/json');
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
$changeSet = ChangeSet::get()->byId($request->param('ID'));
|
if ($request->param('Name')) {
|
||||||
|
$changeSet = ChangeSet::get()->byId($request->param('ID'));
|
||||||
switch ($request->param('Name')) {
|
$response->setBody(Convert::raw2json($this->getChangeSetResource($changeSet)));
|
||||||
case "edit":
|
} else {
|
||||||
$response->setBody('{"message":"show the edit view"}');
|
$response->setBody('{"message":"Resource not found"}');
|
||||||
break;
|
|
||||||
case "show":
|
|
||||||
$response->setBody(Convert::raw2json($this->getChangeSetResource($changeSet)));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$response->setBody('{"message":"404"}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return $this->index($request);
|
return $this->index($request);
|
||||||
}
|
}
|
||||||
@ -381,6 +375,8 @@ JSON;
|
|||||||
* @return SS_HTTPResponse
|
* @return SS_HTTPResponse
|
||||||
*/
|
*/
|
||||||
public function updateCampaign(SS_HTTPRequest $request) {
|
public function updateCampaign(SS_HTTPRequest $request) {
|
||||||
|
$id = $request->param('ID');
|
||||||
|
|
||||||
$response = new SS_HTTPResponse();
|
$response = new SS_HTTPResponse();
|
||||||
$response->addHeader('Content-Type', 'application/json');
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
$response->setBody(Convert::raw2json(['campaign' => 'update']));
|
$response->setBody(Convert::raw2json(['campaign' => 'update']));
|
||||||
@ -469,11 +465,11 @@ JSON;
|
|||||||
*
|
*
|
||||||
* @return Form
|
* @return Form
|
||||||
*/
|
*/
|
||||||
public function getDetailEditForm() {
|
public function getDetailEditForm($id) {
|
||||||
return Form::create(
|
return Form::create(
|
||||||
$this,
|
$this,
|
||||||
'DetailEditForm',
|
'DetailEditForm',
|
||||||
ChangeSet::singleton()->getCMSFields(),
|
ChangeSet::get()->byId($id)->getCMSFields(),
|
||||||
FieldList::create(
|
FieldList::create(
|
||||||
FormAction::create('save', 'Save')
|
FormAction::create('save', 'Save')
|
||||||
)
|
)
|
||||||
|
@ -105,6 +105,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
'schema',
|
'schema',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static $url_handlers = [
|
||||||
|
'GET schema/$FormName/$RecordType/$ItemID' => 'schema'
|
||||||
|
];
|
||||||
|
|
||||||
private static $dependencies = [
|
private static $dependencies = [
|
||||||
'schema' => '%$FormSchema'
|
'schema' => '%$FormSchema'
|
||||||
];
|
];
|
||||||
@ -226,7 +230,16 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
*/
|
*/
|
||||||
public function schema($request) {
|
public function schema($request) {
|
||||||
$response = $this->getResponse();
|
$response = $this->getResponse();
|
||||||
$formName = $request->param('ID');
|
$formName = $request->param('FormName');
|
||||||
|
$recordType = $request->param('RecordType');
|
||||||
|
$itemID = $request->param('ItemID');
|
||||||
|
|
||||||
|
if (!$formName || !$recordType) {
|
||||||
|
throw new SS_HTTPResponse_Exception(
|
||||||
|
'Missing request params',
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if(!$this->hasMethod("get{$formName}")) {
|
if(!$this->hasMethod("get{$formName}")) {
|
||||||
throw new SS_HTTPResponse_Exception(
|
throw new SS_HTTPResponse_Exception(
|
||||||
@ -242,7 +255,12 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->{"get{$formName}"}();
|
$form = $this->{"get{$formName}"}($itemID);
|
||||||
|
|
||||||
|
if ($itemID) {
|
||||||
|
$form->loadDataFrom($recordType::get()->byId($itemID));
|
||||||
|
}
|
||||||
|
|
||||||
$response->addHeader('Content-Type', 'application/json');
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
|
$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ class FormSchemaTest extends SapphireTest {
|
|||||||
$formSchema = new FormSchema();
|
$formSchema = new FormSchema();
|
||||||
$expected = [
|
$expected = [
|
||||||
'name' => 'TestForm',
|
'name' => 'TestForm',
|
||||||
'id' => null,
|
'id' => 'Form_TestForm',
|
||||||
'action' => null,
|
'action' => 'Controller/TestForm',
|
||||||
'method' => '',
|
'method' => 'POST',
|
||||||
'schema_url' => '',
|
'schema_url' => '',
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
'id' => 'Form_TestForm',
|
'id' => 'Form_TestForm',
|
||||||
@ -54,7 +54,7 @@ class FormSchemaTest extends SapphireTest {
|
|||||||
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList());
|
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList());
|
||||||
$formSchema = new FormSchema();
|
$formSchema = new FormSchema();
|
||||||
$expected = [
|
$expected = [
|
||||||
'id' => 'TestForm',
|
'id' => 'Form_TestForm',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'id' => 'Form_TestForm_SecurityID',
|
'id' => 'Form_TestForm_SecurityID',
|
||||||
@ -79,7 +79,7 @@ class FormSchemaTest extends SapphireTest {
|
|||||||
$form->sessionMessage('All saved', 'good');
|
$form->sessionMessage('All saved', 'good');
|
||||||
$formSchema = new FormSchema();
|
$formSchema = new FormSchema();
|
||||||
$expected = [
|
$expected = [
|
||||||
'id' => 'TestForm',
|
'id' => 'Form_TestForm',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'id' => 'Form_TestForm_SecurityID',
|
'id' => 'Form_TestForm_SecurityID',
|
||||||
@ -113,7 +113,7 @@ class FormSchemaTest extends SapphireTest {
|
|||||||
$validator->validationError('Title', 'Title is invalid', 'error');
|
$validator->validationError('Title', 'Title is invalid', 'error');
|
||||||
$formSchema = new FormSchema();
|
$formSchema = new FormSchema();
|
||||||
$expected = [
|
$expected = [
|
||||||
'id' => 'TestForm',
|
'id' => 'Form_TestForm',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'id' => 'Form_TestForm_Title',
|
'id' => 'Form_TestForm_Title',
|
||||||
|
Loading…
Reference in New Issue
Block a user