mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +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 ConfigReducer from 'state/config/ConfigReducer';
|
||||
import FormsReducer from 'state/forms/FormsReducer';
|
||||
import SchemaReducer from 'state/schema/SchemaReducer';
|
||||
import RecordsReducer from 'state/records/RecordsReducer';
|
||||
import CampaignReducer from 'state/campaign/CampaignReducer';
|
||||
@ -16,6 +17,7 @@ import CampaignAdmin from 'containers/CampaignAdmin/index';
|
||||
|
||||
function appBoot() {
|
||||
reducerRegister.add('config', ConfigReducer);
|
||||
reducerRegister.add('forms', FormsReducer);
|
||||
reducerRegister.add('schemas', SchemaReducer);
|
||||
reducerRegister.add('records', RecordsReducer);
|
||||
reducerRegister.add('campaign', CampaignReducer);
|
||||
|
@ -4,7 +4,7 @@ Used for form actions. For example a submit button.
|
||||
|
||||
## Props
|
||||
|
||||
### handleClick (function - required)
|
||||
### handleClick (function)
|
||||
|
||||
The handler for when a button is clicked
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as formsActions from 'state/forms/FormsActions';
|
||||
import * as schemaActions from 'state/schema/SchemaActions';
|
||||
import SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||
import FormComponent from 'components/Form/Form';
|
||||
import FormActionComponent from 'components/FormAction/FormAction';
|
||||
import TextField from 'components/TextField/TextField';
|
||||
import HiddenField from 'components/HiddenField/HiddenField';
|
||||
import GridField from 'components/GridField/GridField';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import deepFreeze from 'deep-freeze';
|
||||
import backend from 'lib/Backend';
|
||||
|
||||
import es6promise from 'es6-promise';
|
||||
es6promise.polyfill();
|
||||
@ -47,7 +50,7 @@ const fakeInjector = {
|
||||
*/
|
||||
getComponentByDataType(dataType) {
|
||||
switch (dataType) {
|
||||
case 'String':
|
||||
case 'Text':
|
||||
return this.components.TextField;
|
||||
case 'Hidden':
|
||||
return this.components.HiddenField;
|
||||
@ -66,7 +69,12 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
|
||||
this.formSchemaPromise = null;
|
||||
this.state = { isFetching: false };
|
||||
|
||||
this.mapActionsToComponents = this.mapActionsToComponents.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() {
|
||||
@ -82,7 +90,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
*
|
||||
* @return object - Promise from the AJAX request.
|
||||
*/
|
||||
fetch(schema = true, state = false) {
|
||||
fetch(schema = true, state = true) {
|
||||
const headerValues = [];
|
||||
|
||||
if (this.state.isFetching === true) {
|
||||
@ -103,9 +111,34 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
})
|
||||
.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 });
|
||||
this.props.actions.setSchema(json);
|
||||
// this.setState({ isFetching: false });
|
||||
|
||||
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
|
||||
@ -118,6 +151,101 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
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.
|
||||
* Only top level form fields are handled here, composite fields (TabSets etc),
|
||||
@ -129,6 +257,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
*/
|
||||
mapFieldsToComponents(fields) {
|
||||
const createFn = this.props.createFn;
|
||||
const handleFieldUpdate = this.handleFieldUpdate;
|
||||
|
||||
return fields.map((field, i) => {
|
||||
const Component = field.component !== null
|
||||
@ -142,7 +271,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
// Props which every form field receives.
|
||||
// Leave it up to the schema and component to determine
|
||||
// which props are required.
|
||||
const props = deepFreeze(field);
|
||||
const props = deepFreeze(Object.assign({}, field, { handleFieldUpdate }));
|
||||
|
||||
// Provides container components a place to hook in
|
||||
// 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() {
|
||||
const formSchema = this.props.schemas[this.props.schemaUrl];
|
||||
const formState = this.props.forms[this.props.formId];
|
||||
|
||||
// If the response from fetching the initial data
|
||||
// hasn't come back yet, don't render anything.
|
||||
@ -172,11 +351,21 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
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 = {
|
||||
actions: formSchema.schema.actions,
|
||||
attributes,
|
||||
componentWillUnmount: this.removeForm,
|
||||
data: formSchema.schema.data,
|
||||
fields: formSchema.schema.fields,
|
||||
fields: fieldData,
|
||||
formId: formSchema.id,
|
||||
handleSubmit: this.handleSubmit,
|
||||
mapActionsToComponents: this.mapActionsToComponents,
|
||||
mapFieldsToComponents: this.mapFieldsToComponents,
|
||||
};
|
||||
|
||||
@ -185,21 +374,29 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
||||
}
|
||||
|
||||
FormBuilderComponent.propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
config: React.PropTypes.object,
|
||||
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,
|
||||
schemaActions: React.PropTypes.object.isRequired,
|
||||
schemaUrl: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
config: state.config,
|
||||
forms: state.forms,
|
||||
schemas: state.schemas,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
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
|
||||
|
||||
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 event
|
||||
* @param object event
|
||||
* @param number id
|
||||
*/
|
||||
deleteRecord(event, id) {
|
||||
event.preventDefault();
|
||||
@ -123,9 +123,19 @@ class GridField extends SilverStripeComponent {
|
||||
);
|
||||
}
|
||||
|
||||
editRecord(event) {
|
||||
/**
|
||||
* @param object event
|
||||
* @param number id
|
||||
*/
|
||||
editRecord(event, id) {
|
||||
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,
|
||||
collectionReadEndpoint: React.PropTypes.object,
|
||||
handleDrillDown: React.PropTypes.func,
|
||||
handleEditRecord: React.PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ Generates an editable text field.
|
||||
|
||||
## Props
|
||||
|
||||
### label
|
||||
### leftTitle
|
||||
|
||||
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.
|
||||
|
||||
### onChange
|
||||
### handleFieldUpdate
|
||||
|
||||
Handler function called when the field's value changes.
|
||||
|
||||
|
@ -10,11 +10,15 @@ class TextField extends SilverStripeComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const labelText = this.props.leftTitle !== null
|
||||
? this.props.leftTitle
|
||||
: this.props.title;
|
||||
|
||||
return (
|
||||
<div className="field text">
|
||||
{this.props.label &&
|
||||
{labelText &&
|
||||
<label className="left" htmlFor={`gallery_${this.props.name}`}>
|
||||
{this.props.label}
|
||||
{labelText}
|
||||
</label>
|
||||
}
|
||||
<div className="middleColumn">
|
||||
@ -29,26 +33,31 @@ class TextField extends SilverStripeComponent {
|
||||
className: ['text', this.props.extraClass].join(' '),
|
||||
id: `gallery_${this.props.name}`,
|
||||
name: this.props.name,
|
||||
onChange: this.props.onChange,
|
||||
onChange: this.handleChange,
|
||||
type: 'text',
|
||||
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;
|
||||
}
|
||||
|
||||
this.props.onChange();
|
||||
this.props.handleFieldUpdate(event, { id: this.props.id, value: event.target.value });
|
||||
}
|
||||
}
|
||||
|
||||
TextField.propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
leftTitle: React.PropTypes.string,
|
||||
extraClass: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
handleFieldUpdate: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
};
|
||||
|
||||
|
@ -16,7 +16,7 @@ describe('TextField', () => {
|
||||
label: '',
|
||||
name: '',
|
||||
value: '',
|
||||
onChange: jest.genMockFunction(),
|
||||
handleFieldUpdate: jest.genMockFunction(),
|
||||
};
|
||||
});
|
||||
|
||||
@ -29,10 +29,10 @@ describe('TextField', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the onChange function on props', () => {
|
||||
textField.handleChange();
|
||||
it('should call the handleFieldUpdate function on props', () => {
|
||||
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 SilverStripeComponent from 'lib/SilverStripeComponent';
|
||||
import FormAction from 'components/FormAction/FormAction';
|
||||
|
||||
class Form extends SilverStripeComponent {
|
||||
|
||||
/**
|
||||
* Gets the components responsible for perfoming actions on the form.
|
||||
* For example form submission.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
getFormAction() {
|
||||
return this.props.actions.map((action) =>
|
||||
<FormAction {...action} />
|
||||
);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (typeof this.props.componentWillUnmount === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.componentWillUnmount(this.props.formId);
|
||||
}
|
||||
|
||||
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 actions = this.getFormAction();
|
||||
const actions = this.props.mapActionsToComponents(this.props.actions);
|
||||
|
||||
return (
|
||||
<form {...attr}>
|
||||
<form {...props}>
|
||||
{fields &&
|
||||
<fieldset className="form-group">
|
||||
{fields}
|
||||
@ -40,6 +41,14 @@ class Form extends SilverStripeComponent {
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
if (typeof this.props.handleSubmit === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.handleSubmit(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Form.propTypes = {
|
||||
@ -51,8 +60,12 @@ Form.propTypes = {
|
||||
id: React.PropTypes.string,
|
||||
method: React.PropTypes.string.isRequired,
|
||||
}),
|
||||
componentWillUnmount: React.PropTypes.func,
|
||||
data: React.PropTypes.array,
|
||||
fields: React.PropTypes.array.isRequired,
|
||||
formId: React.PropTypes.string.isRequired,
|
||||
handleSubmit: React.PropTypes.func,
|
||||
mapActionsToComponents: 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
|
||||
|
||||
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)
|
||||
|
||||
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);
|
||||
|
||||
this.addCampaign = this.addCampaign.bind(this);
|
||||
this.createFn = this.createFn.bind(this);
|
||||
this.publishApi = backend.createEndpointFetcher({
|
||||
url: this.props.sectionConfig.publishEndpoint.url,
|
||||
method: this.props.sectionConfig.publishEndpoint.method,
|
||||
@ -25,6 +24,8 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
id: { urlReplacement: ':id', remove: true },
|
||||
},
|
||||
});
|
||||
this.campaignListCreateFn = this.campaignListCreateFn.bind(this);
|
||||
this.campaignEditCreateFn = this.campaignEditCreateFn.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -38,7 +39,7 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
if (captureRoute) {
|
||||
// If this component is mounted, then handle all page changes via
|
||||
// state / redux
|
||||
this.props.actions.showCampaignView(ctx.params.id, ctx.params.view);
|
||||
this.props.actions.showCampaignView(ctx.params.id, ctx.params.view);
|
||||
} else {
|
||||
// If component is not mounted, we need to allow root routes to load
|
||||
// this section in via ajax
|
||||
@ -80,7 +81,17 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
* @return object
|
||||
*/
|
||||
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 (
|
||||
<div className="cms-content__inner no-preview">
|
||||
@ -93,17 +104,13 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
<div className="panel-scrollable--single-toolbar">
|
||||
<div className="toolbar--content">
|
||||
<div className="btn-toolbar">
|
||||
<FormAction
|
||||
label={i18n._t('Campaigns.ADDCAMPAIGN')}
|
||||
icon={'plus'}
|
||||
handleClick={this.addCampaign}
|
||||
/>
|
||||
<FormAction {...formActionProps} />
|
||||
</div>
|
||||
</div>
|
||||
<FormBuilder schemaUrl={schemaUrl} createFn={this.createFn} />
|
||||
<FormBuilder {...formBuilderProps} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -126,22 +133,38 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* Renders the Detail Edit Form for a Campaign.
|
||||
*/
|
||||
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 props - Props passed from FormBuilder.
|
||||
*
|
||||
* @return object - Instanciated React component
|
||||
*/
|
||||
createFn(Component, props) {
|
||||
campaignListCreateFn(Component, props) {
|
||||
const campaignViewRoute = this.props.sectionConfig.campaignViewRoute;
|
||||
const typeUrlParam = 'set';
|
||||
|
||||
if (props.component === 'GridField') {
|
||||
const extendedProps = Object.assign({}, props, {
|
||||
@ -149,12 +172,20 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
handleDrillDown: (event, record) => {
|
||||
// Set url and set list
|
||||
const path = campaignViewRoute
|
||||
.replace(/:type\?/, 'set')
|
||||
.replace(/:type\?/, typeUrlParam)
|
||||
.replace(/:id\?/, record.ID)
|
||||
.replace(/:view\?/, 'show');
|
||||
|
||||
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
|
||||
* @param int id
|
||||
@ -201,17 +255,19 @@ class CampaignAdmin extends SilverStripeComponent {
|
||||
}
|
||||
|
||||
CampaignAdmin.propTypes = {
|
||||
sectionConfig: React.PropTypes.shape({
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
campaignId: React.PropTypes.string,
|
||||
config: React.PropTypes.shape({
|
||||
forms: React.PropTypes.shape({
|
||||
editForm: React.PropTypes.shape({
|
||||
schemaUrl: React.PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
config: React.PropTypes.shape({
|
||||
SecurityID: React.PropTypes.string,
|
||||
}),
|
||||
sectionConfig: React.PropTypes.object.isRequired,
|
||||
sectionConfigKey: React.PropTypes.string.isRequired,
|
||||
view: React.PropTypes.string,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
|
@ -305,7 +305,7 @@ class Backend {
|
||||
? [url, headers]
|
||||
: [url, encodedData, headers];
|
||||
|
||||
return this[refinedSpec.method](...args)
|
||||
return this[refinedSpec.method.toLowerCase()](...args)
|
||||
.then(parseResponse);
|
||||
};
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import deepFreeze from 'deep-freeze';
|
||||
import ACTION_TYPES from './CampaignActionTypes';
|
||||
|
||||
const initialState = {
|
||||
const initialState = deepFreeze({
|
||||
campaignId: null,
|
||||
isPublishing: false,
|
||||
view: null,
|
||||
};
|
||||
});
|
||||
|
||||
function reducer(state = initialState, action) {
|
||||
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.
|
||||
|
||||
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' => 'createCampaign',
|
||||
'GET set/$ID/$Name' => 'readCampaign',
|
||||
'PUT set/$ID' => 'updateCampaign',
|
||||
'POST $ID' => 'updateCampaign',
|
||||
'DELETE set/$ID' => 'deleteCampaign',
|
||||
];
|
||||
|
||||
@ -56,8 +56,11 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
||||
return array_merge(parent::getClientConfig(), [
|
||||
'forms' => [
|
||||
// TODO Use schemaUrl instead
|
||||
'editForm' => [
|
||||
'EditForm' => [
|
||||
'schemaUrl' => $this->Link('schema/EditForm')
|
||||
],
|
||||
'DetailEditForm' => [
|
||||
'schemaUrl' => $this->Link('schema/DetailEditForm')
|
||||
]
|
||||
],
|
||||
'campaignViewRoute' => $this->Link() . ':type?/:id?/:view?',
|
||||
@ -325,8 +328,6 @@ JSON;
|
||||
return $hal;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Gets viewable list of campaigns
|
||||
*
|
||||
@ -353,21 +354,14 @@ JSON;
|
||||
|
||||
if ($request->getHeader('Accept') == 'text/json') {
|
||||
$response->addHeader('Content-Type', 'application/json');
|
||||
$changeSet = ChangeSet::get()->byId($request->param('ID'));
|
||||
|
||||
switch ($request->param('Name')) {
|
||||
case "edit":
|
||||
$response->setBody('{"message":"show the edit view"}');
|
||||
break;
|
||||
case "show":
|
||||
$response->setBody(Convert::raw2json($this->getChangeSetResource($changeSet)));
|
||||
break;
|
||||
default:
|
||||
$response->setBody('{"message":"404"}');
|
||||
if ($request->param('Name')) {
|
||||
$changeSet = ChangeSet::get()->byId($request->param('ID'));
|
||||
$response->setBody(Convert::raw2json($this->getChangeSetResource($changeSet)));
|
||||
} else {
|
||||
$response->setBody('{"message":"Resource not found"}');
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
} else {
|
||||
return $this->index($request);
|
||||
}
|
||||
@ -381,6 +375,8 @@ JSON;
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function updateCampaign(SS_HTTPRequest $request) {
|
||||
$id = $request->param('ID');
|
||||
|
||||
$response = new SS_HTTPResponse();
|
||||
$response->addHeader('Content-Type', 'application/json');
|
||||
$response->setBody(Convert::raw2json(['campaign' => 'update']));
|
||||
@ -469,11 +465,11 @@ JSON;
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function getDetailEditForm() {
|
||||
public function getDetailEditForm($id) {
|
||||
return Form::create(
|
||||
$this,
|
||||
'DetailEditForm',
|
||||
ChangeSet::singleton()->getCMSFields(),
|
||||
ChangeSet::get()->byId($id)->getCMSFields(),
|
||||
FieldList::create(
|
||||
FormAction::create('save', 'Save')
|
||||
)
|
||||
|
@ -105,6 +105,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
'schema',
|
||||
];
|
||||
|
||||
private static $url_handlers = [
|
||||
'GET schema/$FormName/$RecordType/$ItemID' => 'schema'
|
||||
];
|
||||
|
||||
private static $dependencies = [
|
||||
'schema' => '%$FormSchema'
|
||||
];
|
||||
@ -226,7 +230,16 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
*/
|
||||
public function schema($request) {
|
||||
$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}")) {
|
||||
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->setBody(Convert::raw2json($this->getSchemaForForm($form)));
|
||||
|
||||
|
@ -9,9 +9,9 @@ class FormSchemaTest extends SapphireTest {
|
||||
$formSchema = new FormSchema();
|
||||
$expected = [
|
||||
'name' => 'TestForm',
|
||||
'id' => null,
|
||||
'action' => null,
|
||||
'method' => '',
|
||||
'id' => 'Form_TestForm',
|
||||
'action' => 'Controller/TestForm',
|
||||
'method' => 'POST',
|
||||
'schema_url' => '',
|
||||
'attributes' => [
|
||||
'id' => 'Form_TestForm',
|
||||
@ -54,7 +54,7 @@ class FormSchemaTest extends SapphireTest {
|
||||
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList());
|
||||
$formSchema = new FormSchema();
|
||||
$expected = [
|
||||
'id' => 'TestForm',
|
||||
'id' => 'Form_TestForm',
|
||||
'fields' => [
|
||||
[
|
||||
'id' => 'Form_TestForm_SecurityID',
|
||||
@ -79,7 +79,7 @@ class FormSchemaTest extends SapphireTest {
|
||||
$form->sessionMessage('All saved', 'good');
|
||||
$formSchema = new FormSchema();
|
||||
$expected = [
|
||||
'id' => 'TestForm',
|
||||
'id' => 'Form_TestForm',
|
||||
'fields' => [
|
||||
[
|
||||
'id' => 'Form_TestForm_SecurityID',
|
||||
@ -113,7 +113,7 @@ class FormSchemaTest extends SapphireTest {
|
||||
$validator->validationError('Title', 'Title is invalid', 'error');
|
||||
$formSchema = new FormSchema();
|
||||
$expected = [
|
||||
'id' => 'TestForm',
|
||||
'id' => 'Form_TestForm',
|
||||
'fields' => [
|
||||
[
|
||||
'id' => 'Form_TestForm_Title',
|
||||
|
Loading…
Reference in New Issue
Block a user