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',