diff --git a/admin/client/lang/en.js b/admin/client/lang/en.js
index 5d0459d50..7916d2a9f 100644
--- a/admin/client/lang/en.js
+++ b/admin/client/lang/en.js
@@ -6,6 +6,7 @@ if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
}
} else {
ss.i18n.addDictionary('en', {
+ "Boolean.ANY": "Any",
"CMSMAIN.BATCH_ARCHIVE_PROMPT": "You have {num} page(s) selected.\n\nAre you sure you want to archive these pages?\n\nThese pages and all of their children pages will be unpublished and sent to the archive.",
"CMSMAIN.BATCH_DELETELIVE_PROMPT": "You have {num} page(s) selected.\n\nDo you really want to delete these pages from live?",
"CMSMAIN.BATCH_DELETE_PROMPT": "You have {num} page(s) selected.\n\nDo you really want to delete?",
diff --git a/admin/client/src/components/AddToCampaignModal/AddToCampaignModal.js b/admin/client/src/components/AddToCampaignModal/AddToCampaignModal.js
index c0f75f263..35e184678 100644
--- a/admin/client/src/components/AddToCampaignModal/AddToCampaignModal.js
+++ b/admin/client/src/components/AddToCampaignModal/AddToCampaignModal.js
@@ -13,11 +13,6 @@ class AddToCampaignModal extends SilverStripeComponent {
handleSubmit(event, fieldValues, submitFn) {
- if (!fieldValues.Campaign && fieldValues.Campaign !== 0) {
- event.preventDefault();
- return;
- }
-
if (typeof this.props.handleSubmit === 'function') {
this.props.handleSubmit(event, fieldValues, submitFn);
return;
diff --git a/admin/client/src/components/FormAction/FormAction.js b/admin/client/src/components/FormAction/FormAction.js
index 9d13bca25..ed44bb643 100644
--- a/admin/client/src/components/FormAction/FormAction.js
+++ b/admin/client/src/components/FormAction/FormAction.js
@@ -131,17 +131,17 @@ class FormAction extends SilverStripeComponent {
* @return undefined
*/
handleClick(event) {
- if (typeof this.props.handleClick === 'undefined') {
- return;
+ if (typeof this.props.handleClick === 'function') {
+ this.props.handleClick(event, this.props.name || this.props.id);
}
- this.props.handleClick(event);
}
}
FormAction.propTypes = {
id: React.PropTypes.string,
+ name: React.PropTypes.string,
handleClick: React.PropTypes.func,
title: React.PropTypes.string,
type: React.PropTypes.string,
diff --git a/admin/client/src/components/FormBuilder/FormBuilder.js b/admin/client/src/components/FormBuilder/FormBuilder.js
index b7b05b99e..a828199bf 100644
--- a/admin/client/src/components/FormBuilder/FormBuilder.js
+++ b/admin/client/src/components/FormBuilder/FormBuilder.js
@@ -25,6 +25,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
this.mapFieldsToComponents = this.mapFieldsToComponents.bind(this);
this.handleFieldUpdate = this.handleFieldUpdate.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleAction = this.handleAction.bind(this);
this.removeForm = this.removeForm.bind(this);
this.getFormId = this.getFormId.bind(this);
this.getFormSchema = this.getFormSchema.bind(this);
@@ -171,6 +172,12 @@ export class FormBuilderComponent extends SilverStripeComponent {
}
}
+ handleAction(event, name) {
+ if (typeof this.props.handleAction === 'function') {
+ this.props.handleAction(event, name);
+ }
+ }
+
/**
* Form submission handler passed to the Form Component as a prop.
* Provides a hook for controllers to access for state and provide custom functionality.
@@ -228,6 +235,30 @@ export class FormBuilderComponent extends SilverStripeComponent {
submitFn();
}
+ buildComponent(field, extraProps = {}) {
+ const Component = field.component !== null
+ ? injector.getComponentByName(field.component)
+ : injector.getComponentByDataType(field.type);
+
+ if (Component === null) {
+ return null;
+ }
+
+ // Props which every form field receives.
+ // Leave it up to the schema and component to determine
+ // which props are required.
+ const props = Object.assign({}, field, extraProps);
+
+ // Provides container components a place to hook in
+ // and apply customisations to scaffolded components.
+ const createFn = this.props.createFn;
+ if (typeof createFn === 'function') {
+ return createFn(Component, props);
+ }
+
+ return ;
+ }
+
/**
* Maps a list of schema fields to their React Component.
* Only top level form fields are handled here, composite fields (TabSets etc),
@@ -237,38 +268,16 @@ export class FormBuilderComponent extends SilverStripeComponent {
* @return {Array}
*/
mapFieldsToComponents(fields) {
- const createFn = this.props.createFn;
- const handleFieldUpdate = this.handleFieldUpdate;
-
return fields.map((field) => {
- const Component = field.component !== null
- ? injector.getComponentByName(field.component)
- : injector.getComponentByDataType(field.type);
-
- if (Component === null) {
- return null;
- }
-
// Events
- const extraProps = { onChange: handleFieldUpdate };
+ const extraProps = { onChange: this.handleFieldUpdate };
// Build child nodes
if (field.children) {
extraProps.children = this.mapFieldsToComponents(field.children);
}
- // Props which every form field receives.
- // Leave it up to the schema and component to determine
- // which props are required.
- const props = Object.assign({}, field, extraProps);
-
- // Provides container components a place to hook in
- // and apply customisations to scaffolded components.
- if (typeof createFn === 'function') {
- return createFn(Component, props);
- }
-
- return ;
+ return this.buildComponent(field, extraProps);
});
}
@@ -279,7 +288,17 @@ export class FormBuilderComponent extends SilverStripeComponent {
* @return {Array}
*/
mapActionsToComponents(actions) {
- return this.mapFieldsToComponents(actions);
+ return actions.map((action) => {
+ // Events
+ const extraProps = { handleClick: this.handleAction };
+
+ // Build child nodes
+ if (action.children) {
+ extraProps.children = this.mapActionsToComponents(action.children);
+ }
+
+ return this.buildComponent(action, extraProps);
+ });
}
/**
@@ -297,7 +316,8 @@ export class FormBuilderComponent extends SilverStripeComponent {
return structure;
}
return merge.recursive(true, structure, {
- data: state.data,
+ data: Object.assign({}, structure.data, state.data),
+ source: state.source,
messages: state.messages,
valid: state.valid,
value: state.value,
@@ -368,6 +388,7 @@ FormBuilderComponent.propTypes = {
form: React.PropTypes.object.isRequired,
formActions: React.PropTypes.object.isRequired,
handleSubmit: React.PropTypes.func,
+ handleAction: React.PropTypes.func,
schemas: React.PropTypes.object.isRequired,
schemaActions: React.PropTypes.object.isRequired,
schemaUrl: React.PropTypes.string.isRequired,
diff --git a/admin/client/src/components/HeaderField/HeaderField.js b/admin/client/src/components/HeaderField/HeaderField.js
index 76509f98b..d4d43cd20 100644
--- a/admin/client/src/components/HeaderField/HeaderField.js
+++ b/admin/client/src/components/HeaderField/HeaderField.js
@@ -28,7 +28,7 @@ HeaderField.propTypes = {
React.PropTypes.array,
React.PropTypes.shape({
headingLevel: React.PropTypes.number.isRequired,
- title: React.PropTypes.string.isRequired,
+ title: React.PropTypes.string,
}),
]).isRequired,
};
diff --git a/admin/client/src/components/PopoverField/PopoverField.js b/admin/client/src/components/PopoverField/PopoverField.js
index f26520252..eec628f03 100644
--- a/admin/client/src/components/PopoverField/PopoverField.js
+++ b/admin/client/src/components/PopoverField/PopoverField.js
@@ -8,7 +8,7 @@ class PopoverField extends SilverStripeComponent {
const placement = this.getPlacement();
const overlay = (
{this.props.children}
@@ -35,52 +35,21 @@ class PopoverField extends SilverStripeComponent {
* @returns {String}
*/
getPlacement() {
- const placement = this.getDataProperty('placement');
+ const placement = this.props.data.placement;
return placement || 'bottom';
}
-
- /**
- * Gets title of popup box
- *
- * @return {String} Return the string to use.
- */
- getPopoverTitle() {
- const title = this.getDataProperty('popoverTitle');
- return title || '';
- }
-
- /**
- * Search for a given property either passed in to data or as a direct prop
- *
- * @param {String} name
- * @returns {String}
- */
- getDataProperty(name) {
- if (typeof this.props[name] !== 'undefined') {
- return this.props[name];
- }
-
- // In case this is nested in the form schema data prop
- if (
- typeof this.props.data !== 'undefined'
- && typeof this.props.data[name] !== 'undefined'
- ) {
- return this.props.data[name];
- }
-
- return null;
- }
}
PopoverField.propTypes = {
id: React.PropTypes.string,
- title: React.PropTypes.string,
- popoverTitle: React.PropTypes.string,
- placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
- data: React.PropTypes.shape({
- popoverTitle: React.PropTypes.string,
- placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
- }),
+ title: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]),
+ data: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.shape({
+ popoverTitle: React.PropTypes.string,
+ placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
+ }),
+ ]),
};
export default PopoverField;
diff --git a/admin/client/src/components/SingleSelectField/SingleSelectField.js b/admin/client/src/components/SingleSelectField/SingleSelectField.js
index 6038b572d..d6eb58116 100644
--- a/admin/client/src/components/SingleSelectField/SingleSelectField.js
+++ b/admin/client/src/components/SingleSelectField/SingleSelectField.js
@@ -60,12 +60,7 @@ class SingleSelectField extends SilverStripeComponent {
* @returns ReactComponent
*/
getSelectField() {
- const options = this.props.source.map((item) => {
- return Object.assign({},
- item,
- {disabled: this.props.data.disabled.indexOf(item.value) > -1}
- );
- });
+ const options = this.props.source || [];
if (this.props.hasEmptyDefault) {
options.unshift({
@@ -124,21 +119,15 @@ SingleSelectField.propTypes = {
source: React.PropTypes.arrayOf(React.PropTypes.shape({
value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]),
title: React.PropTypes.string,
- })).isRequired,
- data: React.PropTypes.shape({
- disabled: React.PropTypes.arrayOf(
- React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number])
- ),
- }),
+ disabled: React.PropTypes.bool,
+ })),
hasEmptyDefault: React.PropTypes.bool,
emptyString: React.PropTypes.string,
};
SingleSelectField.defaultProps = {
- data: {
- disabled: [],
- },
- emptyString: '',
+ source: [],
+ emptyString: i18n._t('Boolean.ANY', 'Any'),
};
export default SingleSelectField;
diff --git a/admin/client/src/state/form/FormActions.js b/admin/client/src/state/form/FormActions.js
index 3eea135a2..fad7cca37 100644
--- a/admin/client/src/state/form/FormActions.js
+++ b/admin/client/src/state/form/FormActions.js
@@ -79,6 +79,7 @@ export function submitForm(submitApi, formId, fieldValues) {
type: ACTION_TYPES.SUBMIT_FORM_FAILURE,
payload: { formId, error },
});
+ return error;
});
};
}
diff --git a/forms/FormSchema.php b/forms/FormSchema.php
index 81c6e4a45..7738715ed 100644
--- a/forms/FormSchema.php
+++ b/forms/FormSchema.php
@@ -4,6 +4,7 @@ namespace SilverStripe\Forms\Schema;
use Form;
use FormField;
+use CompositeField;
/**
* Class FormSchema
@@ -63,11 +64,8 @@ class FormSchema {
'messages' => []
];
- // @todo - Flatten all nested fields for returning state. At the moment, only top
- // level fields are returned.
- foreach ($form->Fields() as $field) {
- $state['fields'][] = $field->getSchemaState();
- }
+ // flattened nested fields are returned, rather than only top level fields.
+ $state['fields'] = $this->getFieldStates($form->Fields());
if($form->Message()) {
$state['messages'][] = [
@@ -78,4 +76,17 @@ class FormSchema {
return $state;
}
+
+ protected function getFieldStates($fields) {
+ $states = [];
+ foreach ($fields as $field) {
+ $states[] = $field->getSchemaState();
+
+ if ($field instanceof CompositeField) {
+ $subFields = $field->FieldList();
+ array_merge($states, $this->getFieldStates($subFields));
+ }
+ }
+ return $states;
+ }
}
diff --git a/forms/PopoverField.php b/forms/PopoverField.php
index e4c865748..2611148ea 100644
--- a/forms/PopoverField.php
+++ b/forms/PopoverField.php
@@ -25,6 +25,13 @@ class PopoverField extends FieldGroup
*/
protected $popoverTitle = null;
+ /**
+ * Placement of the popup box
+ *
+ * @var string
+ */
+ protected $placement = 'bottom';
+
/**
* Get popup title
*
@@ -47,19 +54,33 @@ class PopoverField extends FieldGroup
return $this;
}
+ /**
+ * Get popup placement
+ *
+ * @return string
+ */
+ public function getPlacement()
+ {
+ return $this->placement;
+ }
+
+ public function setPlacement($placement)
+ {
+ $valid = ['top', 'right', 'bottom', 'left'];
+
+ if (in_array($placement, $valid)) {
+ $this->placement = $placement;
+ }
+ return $this;
+ }
+
public function getSchemaDataDefaults()
{
$schema = parent::getSchemaDataDefaults();
- if($this->getPopoverTitle()) {
- $data = [
- 'popoverTitle' => $this->getPopoverTitle()
- ];
- if(isset($schema['data'])) {
- $schema['data'] = array_merge($schema['data'], $data);
- } else {
- $schema['data'] = $data;
- }
- }
+
+ $schema['data']['popoverTitle'] = $this->getPopoverTitle();
+ $schema['data']['placement'] = $this->getPlacement();
+
return $schema;
}
}
diff --git a/forms/SelectField.php b/forms/SelectField.php
index 3ff9a98e2..99d7775b9 100644
--- a/forms/SelectField.php
+++ b/forms/SelectField.php
@@ -39,20 +39,21 @@ abstract class SelectField extends FormField {
parent::__construct($name, $title, $value);
}
- public function getSchemaDataDefaults() {
- $data = parent::getSchemaDataDefaults();
+ public function getSchemaStateDefaults() {
+ $data = parent::getSchemaStateDefaults();
+ $disabled = $this->getDisabledItems();
// Add options to 'data'
$source = $this->getSource();
$data['source'] = (is_array($source))
- ? array_map(function ($value, $title) {
+ ? array_map(function ($value, $title) use ($disabled) {
return [
'value' => $value,
'title' => $title,
+ 'disabled' => in_array($value, $disabled),
];
}, array_keys($source), $source)
: [];
- $data['data']['disabled'] = $this->getDisabledItems();
return $data;
}
diff --git a/tests/forms/FormSchemaTest.php b/tests/forms/FormSchemaTest.php
index a6dd76b5b..da3734176 100644
--- a/tests/forms/FormSchemaTest.php
+++ b/tests/forms/FormSchemaTest.php
@@ -271,7 +271,10 @@ class FormSchemaTest extends SapphireTest {
'disabled' => false,
'customValidationMessage' => '',
'attributes' => [],
- 'data' => [],
+ 'data' => [
+ 'popoverTitle' => null,
+ 'placement' => 'bottom',
+ ],
'children' => [
[
'id' => 'Form_TestForm_action_publish',