From a8ee26ec505bce128cf79514431fe5e37614c34b Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 11 Aug 2015 18:21:43 +1200 Subject: [PATCH 1/6] API Add field group (unfinished) --- .../UserFormFieldEditorExtension.php | 44 +++-- .../GridFieldAddItemInlineButton.php | 163 +++++++++++++----- .../editableformfields/EditableFieldGroup.php | 16 ++ .../EditableFieldGroupEnd.php | 16 ++ .../editableformfields/EditableFormField.php | 8 + .../editableformfields/EditableFormStep.php | 8 + ...InlineButton.js => GridFieldExtensions.js} | 21 +-- .../gridfield/GridFieldAddItemInlineButton.ss | 2 +- ...ton_Row.ss => GridFieldAddItemTemplate.ss} | 0 9 files changed, 200 insertions(+), 78 deletions(-) create mode 100644 code/model/editableformfields/EditableFieldGroup.php create mode 100644 code/model/editableformfields/EditableFieldGroupEnd.php rename javascript/{GridFieldAddItemInlineButton.js => GridFieldExtensions.js} (61%) rename templates/gridfield/{GridFieldAddItemInlineButton_Row.ss => GridFieldAddItemTemplate.ss} (100%) diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php index 7526216..aa6cc15 100644 --- a/code/extensions/UserFormFieldEditorExtension.php +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -37,12 +37,17 @@ class UserFormFieldEditorExtension extends DataExtension { $this->createInitialFormStep(true); $editableColumns = new GridFieldEditableColumns(); + $fieldClasses = $this->getEditableFieldClasses(); $editableColumns->setDisplayFields(array( - 'ClassName' => function($record, $column, $grid) { + 'ClassName' => function($record, $column, $grid) use ($fieldClasses) { if($record instanceof EditableFormStep) { - return new LabelField($column, "Page Break"); + return new LabelField($column, _t('UserFormFieldEditorExtension.PAGE_BREAK', 'Page Break')); + } elseif($record instanceof EditableFieldGroup) { + return new LabelField($column, _t('UserFormFieldEditorExtension.FIELD_GROUP_START', 'Field Group (start)')); + } else if($record instanceof EditableFieldGroupEnd) { + return new LabelField($column, _t('UserFormFieldEditorExtension.FIELD_GROUP_END', 'Field Group (end)')); } else { - return DropdownField::create($column, '', $this->getEditableFieldClasses()); + return DropdownField::create($column, '', $fieldClasses); } }, 'Title' => function($record, $column, $grid) { @@ -55,8 +60,15 @@ class UserFormFieldEditorExtension extends DataExtension { ->addComponents( $editableColumns, new GridFieldButtonRow(), - $addField = new GridFieldAddNewInlineButton(), - $addStep = new GridFieldAddItemInlineButton('EditableFormStep'), + GridFieldAddItemInlineButton::create('EditableFormField') + ->setTitle(_t('UserFormFieldEditorExtension.ADD_FIELD', 'Add Field')) + ->setButtonClass('ss-ui-action-constructive'), + GridFieldAddItemInlineButton::create('EditableFormStep') + ->setTitle(_t('UserFormFieldEditorExtension.ADD_PAGE_BREAK', 'Add Page Break')) + ->setExtraClass('uf-gridfield-steprow'), + GridFieldAddItemInlineButton::create(array('EditableFieldGroup', 'EditableFieldGroupEnd')) + ->setTitle(_t('UserFormFieldEditorExtension.ADD_FIELD_GROUP', 'Add Field Group')) + ->setExtraClass('uf-gridfield-grouprow'), new GridFieldEditButton(), new GridFieldDeleteAction(), new GridFieldToolbarHeader(), @@ -64,9 +76,6 @@ class UserFormFieldEditorExtension extends DataExtension { new GridState_Component(), new GridFieldDetailForm() ); - $addField->setTitle('Add Field'); - $addStep->setTitle('Add Page Break'); - $addStep->setExtraClass('uf-gridfield-steprow'); $fieldEditor = GridField::create( 'Fields', @@ -126,21 +135,20 @@ class UserFormFieldEditorExtension extends DataExtension { $classes = ClassInfo::getValidSubClasses('EditableFormField'); // Remove classes we don't want to display in the dropdown. - $classes = array_diff($classes, array( - 'EditableFormField', - 'EditableMultipleOptionField' - )); - $editableFieldClasses = array(); - - foreach ($classes as $key => $className) { - $singleton = singleton($className); - + foreach ($classes as $class) { + if(in_array($class, array('EditableFormField', 'EditableMultipleOptionField')) + || Config::inst()->get($class, 'hidden') + ) { + continue; + } + + $singleton = singleton($class); if(!$singleton->canCreate()) { continue; } - $editableFieldClasses[$className] = $singleton->i18n_singular_name(); + $editableFieldClasses[$class] = $singleton->i18n_singular_name(); } return $editableFieldClasses; diff --git a/code/forms/gridfield/GridFieldAddItemInlineButton.php b/code/forms/gridfield/GridFieldAddItemInlineButton.php index b3b196a..b8fe9cb 100644 --- a/code/forms/gridfield/GridFieldAddItemInlineButton.php +++ b/code/forms/gridfield/GridFieldAddItemInlineButton.php @@ -5,7 +5,7 @@ * * Provides an alternative to GridFieldAddNewInlineButton, but allows you to set a classname */ -class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_SaveHandler { +class GridFieldAddItemInlineButton extends Object implements GridField_HTMLProvider, GridField_SaveHandler { /** * Fragment id @@ -14,6 +14,13 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ */ protected $fragment; + /** + * Additonal CSS classes for the button + * + * @var string + */ + protected $buttonClass = null; + /** * Button title * @@ -22,11 +29,11 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ protected $title; /** - * Class name + * Class names * - * @var string + * @var array */ - protected $modelClass = null; + protected $modelClasses = null; /** * Extra CSS classes for this row @@ -35,14 +42,13 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ */ protected $extraClass = null; - - /** - * @param string $class Name of class to default to + * @param array $classes Class or list of classes to create. + * If you enter more than one class, each click of the "add" button will create one of each * @param string $fragment The fragment to render the button into */ - public function __construct($class, $fragment = 'buttons-before-left') { - $this->setClass($class); + public function __construct($classes, $fragment = 'buttons-before-left') { + $this->setClasses($classes); $this->setFragment($fragment); $this->setTitle(_t('GridFieldExtensions.ADD', 'Add')); } @@ -88,43 +94,54 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ } public function getHTMLFragments($grid) { - if($grid->getList() && !singleton($grid->getModelClass())->canCreate()) { - return array(); + foreach($this->getClasses() as $class) { + if(!singleton($class)->canCreate()) { + return array(); + } } - $fragment = $this->getFragment(); - if(!$editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns')) { throw new Exception('Inline adding requires the editable columns component'); } Requirements::javascript(THIRDPARTY_DIR . '/javascript-templates/tmpl.js'); GridFieldExtensions::include_requirements(); - Requirements::javascript(USERFORMS_DIR . '/javascript/GridFieldAddItemInlineButton.js'); + Requirements::javascript(USERFORMS_DIR . '/javascript/GridFieldExtensions.js'); + // Build button content $data = new ArrayData(array( 'Title' => $this->getTitle(), - 'TemplateName' => $this->getRowTemplateName() + 'ButtonClass' => $this->getButtonClass(), + 'TemplateNames' => json_encode($this->getRowTemplateNames()) )); + // Build template body + $templates = ''; + foreach($this->getClasses() as $class) { + $templates .= $this->getItemRowTemplateFor($grid, $editable, $class); + } + return array( - $fragment => $data->renderWith(__CLASS__), - 'after' => $this->getItemRowTemplate($grid, $editable) + $this->getFragment() => $data->renderWith(__CLASS__), + 'after' => $templates ); } + + /** - * Because getRowTemplate is private + * Get the template for a given class * * @param GridField $grid * @param GridFieldEditableColumns $editable + * @param string $class Name of class * @return type */ - protected function getItemRowTemplate(GridField $grid, GridFieldEditableColumns $editable) { + protected function getItemRowTemplateFor(GridField $grid, GridFieldEditableColumns $editable, $class) { $columns = new ArrayList(); $handled = array_keys($editable->getDisplayFields($grid)); - $record = Object::create($this->getClass()); + $record = Object::create($class); $fields = $editable->getFields($grid, $record); @@ -134,7 +151,7 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ $field = $fields->dataFieldByName($column); if($field) { $field->setName(sprintf( - '%s[%s][%s][{%%=o.num%%}][%s]', $grid->getName(), __CLASS__, $this->getClass(), $field->getName() + '%s[%s][{%%=o.num%%}][%s][%s]', $grid->getName(), __CLASS__, $class, $field->getName() )); } else { $field = $fields->fieldByName($column); @@ -160,30 +177,50 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ $data = new ArrayData(array( 'Columns' => $columns, 'ExtraClass' => $this->getExtraClass(), - 'RecordClass' => $this->getClass(), - 'TemplateName' => $this->getRowTemplateName() + 'RecordClass' => $class, + 'TemplateName' => $this->getRowTemplateNameFor($class) )); - return $data->renderWith(__CLASS__ . '_Row'); + return $data->renderWith('GridFieldAddItemTemplate'); } public function handleSave(GridField $grid, DataObjectInterface $record) { - $list = $grid->getList(); + // Check that this submission relates to this component $value = $grid->Value(); - $class = $this->getClass(); - - if(!isset($value[__CLASS__][$class]) || !is_array($value[__CLASS__][$class])) { + if(empty($value[__CLASS__]) || !is_array($value[__CLASS__])) { return; } + // The way that this component works is that only the first component added to a form will + // be responsible for saving all records for each submission, in order to ensure creation + // and sort order is maintained + $addInlineComponents = $grid->getConfig()->getComponentsByType(__CLASS__); + if($this !== $addInlineComponents->first()) { + return; + } + + // Get allowed classes + $classes = array(); + foreach($addInlineComponents as $component) { + $classes = array_merge($classes, $component->getClasses()); + } + $classes = array_filter($classes, function($class) { + return singleton($class)->canCreate(); + }); + + // Get form $editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns'); - $form = $editable->getForm($grid, $record); + $form = $editable->getForm($grid, $record); - if(!singleton($class)->canCreate()) { - return; - } // Process records matching the specified class - foreach($value[__CLASS__][$class] as $fields) { + $list = $grid->getList(); + foreach($value[__CLASS__] as $row) { + $fields = reset($row); + $class = key($row); + if(!in_array($class, $classes)) { + continue; + } + $item = $class::create(); $extra = array(); @@ -200,21 +237,24 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ } /** - * Get the class of the object to create + * Get the classes of the objects to create * - * @return string + * @return array */ - public function getClass() { - return $this->modelClass; + public function getClasses() { + return $this->modelClasses; } /** - * Specify the class to create + * Specify the classes to create * - * @param string $class + * @param array $classes */ - public function setClass($class) { - $this->modelClass = $class; + public function setClasses($classes) { + if(!is_array($classes)) { + $classes = array($classes); + } + $this->modelClasses = $classes; } /** @@ -230,17 +270,52 @@ class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_ * Sets extra CSS classes for this row * * @param string $extraClass + * @return $this */ public function setExtraClass($extraClass) { $this->extraClass = $extraClass; + return $this; } /** - * Get name of item template + * Get extra button class * * @return string */ - public function getRowTemplateName() { - return 'ss-gridfield-add-inline-template-' . $this->getClass(); + public function getButtonClass() { + return $this->buttonClass; + } + + /** + * Sets extra CSS classes for this button + * + * @param string $buttonClass + * @return $this + */ + public function setButtonClass($buttonClass) { + $this->buttonClass = $buttonClass; + return $this; + } + + /** + * Get names of all item templates + * + * @return array + */ + public function getRowTemplateNames() { + $self = $this; + return array_map(function($class) use ($self) { + return $this->getRowTemplateNameFor($class); + }, $this->getClasses()); + } + + /** + * Get the template name for a single class + * + * @param string $class + * @return string + */ + public function getRowTemplateNameFor($class) { + return "ss-gridfield-add-inline-template-{$class}"; } } diff --git a/code/model/editableformfields/EditableFieldGroup.php b/code/model/editableformfields/EditableFieldGroup.php new file mode 100644 index 0000000..fe98f95 --- /dev/null +++ b/code/model/editableformfields/EditableFieldGroup.php @@ -0,0 +1,16 @@ + + diff --git a/templates/gridfield/GridFieldAddItemInlineButton_Row.ss b/templates/gridfield/GridFieldAddItemTemplate.ss similarity index 100% rename from templates/gridfield/GridFieldAddItemInlineButton_Row.ss rename to templates/gridfield/GridFieldAddItemTemplate.ss From 5ba3d9b2e044245a022861356bc786414b49f2fe Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 12 Aug 2015 12:45:25 +1200 Subject: [PATCH 2/6] API Directly create editable fields on add --- .../UserFormFieldEditorExtension.php | 14 +- code/forms/GridFieldAddClassesButton.php | 225 ++++++++++++ .../GridFieldAddItemInlineButton.php | 321 ------------------ javascript/GridFieldExtensions.js | 32 -- .../gridfield/GridFieldAddClassesButton.ss | 3 + .../gridfield/GridFieldAddItemInlineButton.ss | 3 - .../gridfield/GridFieldAddItemTemplate.ss | 13 - 7 files changed, 234 insertions(+), 377 deletions(-) create mode 100644 code/forms/GridFieldAddClassesButton.php delete mode 100644 code/forms/gridfield/GridFieldAddItemInlineButton.php delete mode 100644 javascript/GridFieldExtensions.js create mode 100644 templates/gridfield/GridFieldAddClassesButton.ss delete mode 100644 templates/gridfield/GridFieldAddItemInlineButton.ss delete mode 100644 templates/gridfield/GridFieldAddItemTemplate.ss diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php index aa6cc15..8c0aa10 100644 --- a/code/extensions/UserFormFieldEditorExtension.php +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -60,15 +60,13 @@ class UserFormFieldEditorExtension extends DataExtension { ->addComponents( $editableColumns, new GridFieldButtonRow(), - GridFieldAddItemInlineButton::create('EditableFormField') - ->setTitle(_t('UserFormFieldEditorExtension.ADD_FIELD', 'Add Field')) + GridFieldAddClassesButton::create('EditableFormField') + ->setButtonName(_t('UserFormFieldEditorExtension.ADD_FIELD', 'Add Field')) ->setButtonClass('ss-ui-action-constructive'), - GridFieldAddItemInlineButton::create('EditableFormStep') - ->setTitle(_t('UserFormFieldEditorExtension.ADD_PAGE_BREAK', 'Add Page Break')) - ->setExtraClass('uf-gridfield-steprow'), - GridFieldAddItemInlineButton::create(array('EditableFieldGroup', 'EditableFieldGroupEnd')) - ->setTitle(_t('UserFormFieldEditorExtension.ADD_FIELD_GROUP', 'Add Field Group')) - ->setExtraClass('uf-gridfield-grouprow'), + GridFieldAddClassesButton::create('EditableFormStep') + ->setButtonName(_t('UserFormFieldEditorExtension.ADD_PAGE_BREAK', 'Add Page Break')), + GridFieldAddClassesButton::create(array('EditableFieldGroup', 'EditableFieldGroupEnd')) + ->setButtonName(_t('UserFormFieldEditorExtension.ADD_FIELD_GROUP', 'Add Field Group')), new GridFieldEditButton(), new GridFieldDeleteAction(), new GridFieldToolbarHeader(), diff --git a/code/forms/GridFieldAddClassesButton.php b/code/forms/GridFieldAddClassesButton.php new file mode 100644 index 0000000..5810fe0 --- /dev/null +++ b/code/forms/GridFieldAddClassesButton.php @@ -0,0 +1,225 @@ +setClasses($classes); + $this->setFragment($targetFragment); + } + + /** + * Change the button name + * + * @param string $name + * @return $this + */ + public function setButtonName($name) { + $this->buttonName = $name; + return $this; + } + + /** + * Get the button name + * + * @return string + */ + public function getButtonName() { + return $this->buttonName; + } + + /** + * Gets the fragment name this button is rendered into. + * + * @return string + */ + public function getFragment() { + return $this->targetFragment; + } + + /** + * Sets the fragment name this button is rendered into. + * + * @param string $fragment + * @return GridFieldAddNewInlineButton $this + */ + public function setFragment($fragment) { + $this->targetFragment = $fragment; + return $this; + } + + /** + * Get extra button class + * + * @return string + */ + public function getButtonClass() { + return $this->buttonClass; + } + + /** + * Sets extra CSS classes for this button + * + * @param string $buttonClass + * @return $this + */ + public function setButtonClass($buttonClass) { + $this->buttonClass = $buttonClass; + return $this; + } + + + /** + * Get the classes of the objects to create + * + * @return array + */ + public function getClasses() { + return $this->modelClasses; + } + + /** + * Gets the list of classes which can be created, with checks for permissions. + * Will fallback to the default model class for the given DataGrid + * + * @param DataGrid $grid + * @return array + */ + public function getClassesCreate($grid) { + // Get explicit or fallback class list + $classes = $this->modelClasses; + if(empty($classes) && $grid) { + $classes = array($grid->getModelClass()); + } + + // Filter out classes without permission + return array_filter($classes, function($class) { + return singleton($class)->canCreate(); + }); + } + + /** + * Specify the classes to create + * + * @param array $classes + */ + public function setClasses($classes) { + if(!is_array($classes)) { + $classes = $classes ? array($classes) : array(); + } + $this->modelClasses = $classes; + } + + public function getHTMLFragments($grid) { + // Check create permission + $singleton = singleton($grid->getModelClass()); + if(!$singleton->canCreate()) { + return array(); + } + + // Get button name + $buttonName = $this->getButtonName(); + if(!$buttonName) { + // provide a default button name, can be changed by calling {@link setButtonName()} on this component + $objectName = $singleton->i18n_singular_name(); + $buttonName = _t('GridField.Add', 'Add {name}', array('name' => $objectName)); + } + + $data = new ArrayData(array( + 'Title' => $this->getButtonName(), + 'ButtonClass' => $this->getButtonClass(), + 'Link' => Controller::join_links($grid->Link(), $this->getAction()) + )); + + return array( + $this->getFragment() => $data->renderWith(__CLASS__) + ); + } + + /** + * Get the action suburl for this component + * + * @return type + */ + protected function getAction() { + $classes = implode('-', $this->getClasses()); + return Controller::join_links('add-classes', $classes); + } + + /** + * {@inheritDoc} + */ + public function getURLHandlers($grid) { + return array( + $this->getAction() => 'handleAdd' + ); + } + + /** + * Handles adding a new instance of a selected class. + * + * @param GridField $grid + * @param SS_HTTPRequest $request + */ + public function handleAdd($grid, $request) { + $classes = $this->getClassesCreate($grid); + + if(empty($classes)) { + throw new SS_HTTPResponse_Exception(400); + } + + // Add item to gridfield + $list = $grid->getList(); + foreach($classes as $class) { + $item = $class::create(); + $item->write(); + $list->add($item); + } + + // Return directly to the gridfield again + return $grid + ->getForm() + ->getController() + ->redirectBack(); + } +} diff --git a/code/forms/gridfield/GridFieldAddItemInlineButton.php b/code/forms/gridfield/GridFieldAddItemInlineButton.php deleted file mode 100644 index b8fe9cb..0000000 --- a/code/forms/gridfield/GridFieldAddItemInlineButton.php +++ /dev/null @@ -1,321 +0,0 @@ -setClasses($classes); - $this->setFragment($fragment); - $this->setTitle(_t('GridFieldExtensions.ADD', 'Add')); - } - - /** - * Gets the fragment name this button is rendered into. - * - * @return string - */ - public function getFragment() { - return $this->fragment; - } - - /** - * Sets the fragment name this button is rendered into. - * - * @param string $fragment - * @return GridFieldAddNewInlineButton $this - */ - public function setFragment($fragment) { - $this->fragment = $fragment; - return $this; - } - - /** - * Gets the button title text. - * - * @return string - */ - public function getTitle() { - return $this->title; - } - - /** - * Sets the button title text. - * - * @param string $title - * @return GridFieldAddNewInlineButton $this - */ - public function setTitle($title) { - $this->title = $title; - return $this; - } - - public function getHTMLFragments($grid) { - foreach($this->getClasses() as $class) { - if(!singleton($class)->canCreate()) { - return array(); - } - } - - if(!$editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns')) { - throw new Exception('Inline adding requires the editable columns component'); - } - - Requirements::javascript(THIRDPARTY_DIR . '/javascript-templates/tmpl.js'); - GridFieldExtensions::include_requirements(); - Requirements::javascript(USERFORMS_DIR . '/javascript/GridFieldExtensions.js'); - - // Build button content - $data = new ArrayData(array( - 'Title' => $this->getTitle(), - 'ButtonClass' => $this->getButtonClass(), - 'TemplateNames' => json_encode($this->getRowTemplateNames()) - )); - - // Build template body - $templates = ''; - foreach($this->getClasses() as $class) { - $templates .= $this->getItemRowTemplateFor($grid, $editable, $class); - } - - return array( - $this->getFragment() => $data->renderWith(__CLASS__), - 'after' => $templates - ); - } - - - - /** - * Get the template for a given class - * - * @param GridField $grid - * @param GridFieldEditableColumns $editable - * @param string $class Name of class - * @return type - */ - protected function getItemRowTemplateFor(GridField $grid, GridFieldEditableColumns $editable, $class) { - $columns = new ArrayList(); - $handled = array_keys($editable->getDisplayFields($grid)); - - $record = Object::create($class); - - $fields = $editable->getFields($grid, $record); - - foreach($grid->getColumns() as $column) { - $content = null; - if(in_array($column, $handled)) { - $field = $fields->dataFieldByName($column); - if($field) { - $field->setName(sprintf( - '%s[%s][{%%=o.num%%}][%s][%s]', $grid->getName(), __CLASS__, $class, $field->getName() - )); - } else { - $field = $fields->fieldByName($column); - } - if($field) { - $content = $field->Field(); - } - } - - $attrs = ''; - - foreach($grid->getColumnAttributes($record, $column) as $attr => $val) { - $attrs .= sprintf(' %s="%s"', $attr, Convert::raw2att($val)); - } - - $columns->push(new ArrayData(array( - 'Content' => $content, - 'Attributes' => $attrs, - 'IsActions' => $column == 'Actions' - ))); - } - - $data = new ArrayData(array( - 'Columns' => $columns, - 'ExtraClass' => $this->getExtraClass(), - 'RecordClass' => $class, - 'TemplateName' => $this->getRowTemplateNameFor($class) - )); - return $data->renderWith('GridFieldAddItemTemplate'); - } - - public function handleSave(GridField $grid, DataObjectInterface $record) { - // Check that this submission relates to this component - $value = $grid->Value(); - if(empty($value[__CLASS__]) || !is_array($value[__CLASS__])) { - return; - } - - // The way that this component works is that only the first component added to a form will - // be responsible for saving all records for each submission, in order to ensure creation - // and sort order is maintained - $addInlineComponents = $grid->getConfig()->getComponentsByType(__CLASS__); - if($this !== $addInlineComponents->first()) { - return; - } - - // Get allowed classes - $classes = array(); - foreach($addInlineComponents as $component) { - $classes = array_merge($classes, $component->getClasses()); - } - $classes = array_filter($classes, function($class) { - return singleton($class)->canCreate(); - }); - - // Get form - $editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns'); - $form = $editable->getForm($grid, $record); - - - // Process records matching the specified class - $list = $grid->getList(); - foreach($value[__CLASS__] as $row) { - $fields = reset($row); - $class = key($row); - if(!in_array($class, $classes)) { - continue; - } - - $item = $class::create(); - $extra = array(); - - $form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING); - $form->saveInto($item); - - if($list instanceof ManyManyList) { - $extra = array_intersect_key($form->getData(), (array) $list->getExtraFields()); - } - - $item->write(); - $list->add($item, $extra); - } - } - - /** - * Get the classes of the objects to create - * - * @return array - */ - public function getClasses() { - return $this->modelClasses; - } - - /** - * Specify the classes to create - * - * @param array $classes - */ - public function setClasses($classes) { - if(!is_array($classes)) { - $classes = array($classes); - } - $this->modelClasses = $classes; - } - - /** - * Get extra CSS classes for this row - * - * @return type - */ - public function getExtraClass() { - return $this->extraClass; - } - - /** - * Sets extra CSS classes for this row - * - * @param string $extraClass - * @return $this - */ - public function setExtraClass($extraClass) { - $this->extraClass = $extraClass; - return $this; - } - - /** - * Get extra button class - * - * @return string - */ - public function getButtonClass() { - return $this->buttonClass; - } - - /** - * Sets extra CSS classes for this button - * - * @param string $buttonClass - * @return $this - */ - public function setButtonClass($buttonClass) { - $this->buttonClass = $buttonClass; - return $this; - } - - /** - * Get names of all item templates - * - * @return array - */ - public function getRowTemplateNames() { - $self = $this; - return array_map(function($class) use ($self) { - return $this->getRowTemplateNameFor($class); - }, $this->getClasses()); - } - - /** - * Get the template name for a single class - * - * @param string $class - * @return string - */ - public function getRowTemplateNameFor($class) { - return "ss-gridfield-add-inline-template-{$class}"; - } -} diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js deleted file mode 100644 index 21d1fb1..0000000 --- a/javascript/GridFieldExtensions.js +++ /dev/null @@ -1,32 +0,0 @@ -(function($) { - $.entwine("ss", function($) { - // See gridfieldextensions/javascript/GridFieldExtensions.js - - $(".ss-gridfield.ss-gridfield-editable").entwine({ - onaddnewiteminline: function(e, template) { - var tmpl = window.tmpl; - var row = this.find("." + template); - var num = this.data("add-inline-num") || 1; - - tmpl.cache[template] = tmpl(row.html()); - - this.find("tbody").append(tmpl(template, { num: num })); - this.find(".ss-gridfield-no-items").hide(); - this.data("add-inline-num", num + 1); - } - }); - - $(".ss-gridfield-add-new-item-inline").entwine({ - onclick: function() { - // Create each template - var gridfield = this.getGridField(); - $.each(this.data('template-names'), function(index, template) { - console.log(template); - gridfield.trigger("addnewiteminline", template); - }); - return false; - } - }); - }); - -})(jQuery); diff --git a/templates/gridfield/GridFieldAddClassesButton.ss b/templates/gridfield/GridFieldAddClassesButton.ss new file mode 100644 index 0000000..aabfe88 --- /dev/null +++ b/templates/gridfield/GridFieldAddClassesButton.ss @@ -0,0 +1,3 @@ + + $Title.XML + \ No newline at end of file diff --git a/templates/gridfield/GridFieldAddItemInlineButton.ss b/templates/gridfield/GridFieldAddItemInlineButton.ss deleted file mode 100644 index 6ce5103..0000000 --- a/templates/gridfield/GridFieldAddItemInlineButton.ss +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/templates/gridfield/GridFieldAddItemTemplate.ss b/templates/gridfield/GridFieldAddItemTemplate.ss deleted file mode 100644 index fd345ac..0000000 --- a/templates/gridfield/GridFieldAddItemTemplate.ss +++ /dev/null @@ -1,13 +0,0 @@ - From 4adc698e0f43666680a0ff9bad9c5985c5908c83 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 12 Aug 2015 16:08:32 +1200 Subject: [PATCH 3/6] API Frontend form fields for nested composite fields --- .../UserFormFieldEditorExtension.php | 15 ++---- code/formfields/UserFormsCompositeField.php | 47 +++++++++++++++++++ code/formfields/UserFormsFieldContainer.php | 30 ++++++++++++ code/formfields/UserFormsFieldList.php | 32 +++++++++++++ code/formfields/UserFormsGroupField.php | 27 +++++++++++ code/formfields/UserFormsStepField.php | 7 +++ code/forms/UserForm.php | 26 ++-------- .../editableformfields/EditableFieldGroup.php | 37 +++++++++++++++ .../EditableFieldGroupEnd.php | 29 ++++++++++++ .../editableformfields/EditableFormField.php | 23 +++++++++ .../editableformfields/EditableFormStep.php | 14 ++++-- 11 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 code/formfields/UserFormsCompositeField.php create mode 100644 code/formfields/UserFormsFieldContainer.php create mode 100644 code/formfields/UserFormsFieldList.php create mode 100644 code/formfields/UserFormsGroupField.php create mode 100644 code/formfields/UserFormsStepField.php diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php index 8c0aa10..3dba1a8 100644 --- a/code/extensions/UserFormFieldEditorExtension.php +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -40,19 +40,14 @@ class UserFormFieldEditorExtension extends DataExtension { $fieldClasses = $this->getEditableFieldClasses(); $editableColumns->setDisplayFields(array( 'ClassName' => function($record, $column, $grid) use ($fieldClasses) { - if($record instanceof EditableFormStep) { - return new LabelField($column, _t('UserFormFieldEditorExtension.PAGE_BREAK', 'Page Break')); - } elseif($record instanceof EditableFieldGroup) { - return new LabelField($column, _t('UserFormFieldEditorExtension.FIELD_GROUP_START', 'Field Group (start)')); - } else if($record instanceof EditableFieldGroupEnd) { - return new LabelField($column, _t('UserFormFieldEditorExtension.FIELD_GROUP_END', 'Field Group (end)')); - } else { - return DropdownField::create($column, '', $fieldClasses); + if($record instanceof EditableFormField) { + return $record->getInlineClassnameField($column, $fieldClasses); } }, 'Title' => function($record, $column, $grid) { - return TextField::create($column, ' ') - ->setAttribute('placeholder', _t('UserDefinedForm.TITLE', 'Title')); + if($record instanceof EditableFormField) { + return $record->getInlineTitleField($column); + } } )); diff --git a/code/formfields/UserFormsCompositeField.php b/code/formfields/UserFormsCompositeField.php new file mode 100644 index 0000000..5188b54 --- /dev/null +++ b/code/formfields/UserFormsCompositeField.php @@ -0,0 +1,47 @@ +parent; + } + + public function setParent(UserFormsFieldContainer $parent) { + $this->parent = $parent; + return $this; + } + + public function processNext(EditableFormField $field) { + // When we find a step, bubble up to the top + if($field instanceof EditableFormStep) { + return $this->getParent()->processNext($field); + } + + // Skip over fields that don't generate formfields + $formField = $field->getFormField(); + if(!$formField) { + return $this; + } + + // Save this field + $this->push($formField); + + // Nest fields that are containers + if($formField instanceof UserFormsFieldContainer) { + return $formField->setParent($this); + } + + // Add any subsequent fields to this + return $this; + } +} diff --git a/code/formfields/UserFormsFieldContainer.php b/code/formfields/UserFormsFieldContainer.php new file mode 100644 index 0000000..b43f7b0 --- /dev/null +++ b/code/formfields/UserFormsFieldContainer.php @@ -0,0 +1,30 @@ +getFormField(); + if(!$formField) { + return $this; + } + + $this->push($formField); + + if($formField instanceof UserFormsFieldContainer) { + return $formField->setParent($this); + } + + return $this; + } + + public function getParent() { + // Field list does not have a parent + return null; + } + + public function setParent(UserFormsFieldContainer $parent) { + return $this; + } + +} diff --git a/code/formfields/UserFormsGroupField.php b/code/formfields/UserFormsGroupField.php new file mode 100644 index 0000000..8860766 --- /dev/null +++ b/code/formfields/UserFormsGroupField.php @@ -0,0 +1,27 @@ +setTag('fieldset'); + } + + public function getLegend() { + // Legend defaults to title + return parent::getLegend() ?: $this->Title(); + } + + public function processNext(EditableFormField $field) { + // When ending a group, jump up one level + if($field instanceof EditableFieldGroupEnd) { + return $this->getParent(); + } + + // Otherwise behave as per normal composite field + return parent::processNext($field); + } +} diff --git a/code/formfields/UserFormsStepField.php b/code/formfields/UserFormsStepField.php new file mode 100644 index 0000000..efcf563 --- /dev/null +++ b/code/formfields/UserFormsStepField.php @@ -0,0 +1,7 @@ +controller->Fields() as $field) { - // When we encounter a step, save it - if ($field instanceof EditableFormStep) { - $emptyStep = $field->getFormField(); - continue; - } - - // Ensure that the last field is a step - if($emptyStep) { - // When we reach the first non-step field, any empty step will no longer be empty - $fields->push($emptyStep); - $emptyStep = null; - - } elseif(! $fields->last()) { - // If no steps have been saved yet, warn - trigger_error('Missing first step in form', E_USER_WARNING); - $fields->push(singleton('EditableFormStep')->getFormField()); - } - - $fields->last()->push($field->getFormField()); + $target = $target->processNext($field); } - $this->extend('updateFormFields', $fields); return $fields; } diff --git a/code/model/editableformfields/EditableFieldGroup.php b/code/model/editableformfields/EditableFieldGroup.php index fe98f95..83c8fa1 100644 --- a/code/model/editableformfields/EditableFieldGroup.php +++ b/code/model/editableformfields/EditableFieldGroup.php @@ -12,5 +12,42 @@ class EditableFieldGroup extends EditableFormField { * @var bool */ private static $hidden = true; + + public function getCMSFields() { + $fields = parent::getCMSFields(); + $fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules')); + return $fields; + } + + public function getInlineClassnameField($column, $fieldClasses) { + return new LabelField( + $column, + _t('EditableFieldGroup.FIELD_GROUP_START', 'Field Group (start)') + ); + } + + public function showInReports() { + return false; + } + + public function getFormField() { + $field = UserFormsGroupField::create() + ->setTitle($this->EscapedTitle ?: false); + $this->doUpdateFormField($field); + return $field; + } + + protected function updateFormField($field) { + // set the right title on this field + if($this->RightTitle) { + // Since this field expects raw html, safely escape the user data prior + $field->setRightTitle(Convert::raw2xml($this->RightTitle)); + } + + // if this field has an extra class + if($field->ExtraClass) { + $field->addExtraClass($field->ExtraClass); + } + } } diff --git a/code/model/editableformfields/EditableFieldGroupEnd.php b/code/model/editableformfields/EditableFieldGroupEnd.php index 8e0eaf6..53a4e52 100644 --- a/code/model/editableformfields/EditableFieldGroupEnd.php +++ b/code/model/editableformfields/EditableFieldGroupEnd.php @@ -12,5 +12,34 @@ class EditableFieldGroupEnd extends EditableFormField { * @var bool */ private static $hidden = true; + + public function getCMSFields() { + $fields = parent::getCMSFields(); + $fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules')); + return $fields; + } + + public function getInlineClassnameField($column, $fieldClasses) { + return new LabelField( + $column, + _t('EditableFieldGroupEnd.FIELD_GROUP_END', 'Field Group (end)') + ); + } + + public function getInlineTitleField($column) { + return HiddenField::create($column); + } + + public function getFormField() { + return null; + } + + public function showInReports() { + return false; + } + + public function canEdit($member = null) { + return false; + } } diff --git a/code/model/editableformfields/EditableFormField.php b/code/model/editableformfields/EditableFormField.php index ca913ea..49de232 100755 --- a/code/model/editableformfields/EditableFormField.php +++ b/code/model/editableformfields/EditableFormField.php @@ -624,4 +624,27 @@ class EditableFormField extends DataObject { } } } + + /** + * Get the formfield to use when editing this inline in gridfield + * + * @param string $column name of column + * @param array $fieldClasses List of allowed classnames if this formfield has a selectable class + * @return FormField + */ + public function getInlineClassnameField($column, $fieldClasses) { + return DropdownField::create($column, false, $fieldClasses); + } + + /** + * Get the formfield to use when editing the title inline + * + * @param string $column + * @return FormField + */ + public function getInlineTitleField($column) { + return TextField::create($column, false) + ->setAttribute('placeholder', _t('EditableFormField.TITLE', 'Title')) + ->setAttribute('data-placeholder', _t('EditableFormField.TITLE', 'Title')); + } } diff --git a/code/model/editableformfields/EditableFormStep.php b/code/model/editableformfields/EditableFormStep.php index dac78b7..6de7062 100644 --- a/code/model/editableformfields/EditableFormStep.php +++ b/code/model/editableformfields/EditableFormStep.php @@ -32,10 +32,7 @@ class EditableFormStep extends EditableFormField { public function getCMSFields() { $fields = parent::getCMSFields(); - $fields->removeByName('MergeField'); - $fields->removeByName('Default'); - $fields->removeByName('Validation'); - $fields->removeByName('CustomRules'); + $fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules')); return $fields; } @@ -44,7 +41,7 @@ class EditableFormStep extends EditableFormField { * @return FormField */ public function getFormField() { - $field = CompositeField::create() + $field = UserFormsStepField::create() ->setTitle($this->EscapedTitle); $this->doUpdateFormField($field); return $field; @@ -63,4 +60,11 @@ class EditableFormStep extends EditableFormField { public function showInReports() { return false; } + + public function getInlineClassnameField($column, $fieldClasses) { + return new LabelField( + $column, + _t('EditableFieldGroupEnd.PAGE_BREAK', 'Page Break') + ); + } } From b483a105662eb411baa3a50c264b80e84f5bb70c Mon Sep 17 00:00:00 2001 From: scott1702 Date: Wed, 12 Aug 2015 12:50:14 +1200 Subject: [PATCH 4/6] Cms styling for fieldgroups --- .gitignore | 1 + code/model/UserDefinedForm.php | 2 ++ config.rb | 12 ++++++++ css/UserForm.css | 55 +++++++++++++++++----------------- css/UserForm_cms.css | 46 +++++++++++++++------------- javascript/Gridfield.js | 23 ++++++++++++++ scss/UserForm.scss | 47 +++++++++++++++++++++++++++++ scss/UserForm_cms.scss | 50 +++++++++++++++++++++++++++++++ 8 files changed, 187 insertions(+), 49 deletions(-) create mode 100644 config.rb create mode 100644 javascript/Gridfield.js create mode 100644 scss/UserForm.scss create mode 100644 scss/UserForm_cms.scss diff --git a/.gitignore b/.gitignore index e43b0f9..92b0407 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.sass-cache/ diff --git a/code/model/UserDefinedForm.php b/code/model/UserDefinedForm.php index 69e0feb..9f0cccf 100755 --- a/code/model/UserDefinedForm.php +++ b/code/model/UserDefinedForm.php @@ -177,6 +177,8 @@ SQL; $config->addComponent(new GridFieldDetailForm()); $config->addComponent($export = new GridFieldExportButton()); $config->addComponent($print = new GridFieldPrintButton()); + + Requirements::javascript(USERFORMS_DIR . '/javascript/Gridfield.js'); /** * Support for {@link https://github.com/colymba/GridFieldBulkEditingTools} diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..ad60521 --- /dev/null +++ b/config.rb @@ -0,0 +1,12 @@ +require 'compass/import-once/activate' +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "css" +sass_dir = "scss" +images_dir = "images" +javascripts_dir = "javascript" + +line_comments = false + diff --git a/css/UserForm.css b/css/UserForm.css index cf667cf..612f290 100644 --- a/css/UserForm.css +++ b/css/UserForm.css @@ -1,38 +1,37 @@ /** * Lightweight base styles for the front-end form. */ -.userform-progress .step-buttons, -.step-navigation .step-buttons { - margin-left: 0; -} - -.userform-progress .step-buttons { - position: relative; -} - -.userform-progress .step-button-jump { - position: absolute; - top: 0; -} - -.userform-progress .step-buttons .step-button-wrapper, -.step-navigation .step-buttons .step-button-wrapper { - display: inline-block; - list-style-type: none; -} - .userform-progress .progress { - position: relative; - height: 1em; - background: #eee; + position: relative; + height: 1em; + background: #eee; +} +.userform-progress .progress .progress-bar { + position: absolute; + height: 1em; + background: #666; +} +.userform-progress .step-buttons { + margin-left: 0; + position: relative; +} +.userform-progress .step-buttons .step-button-wrapper { + display: inline-block; + list-style-type: none; +} +.userform-progress .step-button-jump { + position: absolute; + top: 0; } -.userform-progress .progress .progress-bar { - position: absolute; - height: 1em; - background: #666; +.step-navigation .step-buttons { + margin-left: 0; +} +.step-navigation .step-buttons .step-button-wrapper { + display: inline-block; + list-style-type: none; } .userform { - clear: both; + clear: both; } diff --git a/css/UserForm_cms.css b/css/UserForm_cms.css index 7953279..0280219 100644 --- a/css/UserForm_cms.css +++ b/css/UserForm_cms.css @@ -1,24 +1,28 @@ - -.cms .ss-gridfield > div.ss-gridfield-buttonrow-before { - margin-bottom: 10px; - overflow: auto; -} - /** - * Styles for page steps + * Styles for cms */ -.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow, -.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'], -.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow:hover, -.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep']:hover -{ - background: #dae2e7; +.cms .ss-gridfield > div.ss-gridfield-buttonrow-before { + margin-bottom: 10px; + overflow: auto; +} +.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'], .cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep']:hover { + background: #dae2e7; +} +.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'] label { + font-weight: bold; + color: #444; + font-size: 1.1em; +} +.cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup'], .cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup']:hover { + background: #E7EFF4; +} +.cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup'] label { + font-weight: bold; +} +.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFieldGroupEnd'] .col-Title input { + display: none; +} +.cms table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .col-reorder, +.cms table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .handle { + background: #E7EFF4; } - -.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow label, -.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'] label -{ - font-weight: bold; - color: #393939; - font-size: 1.1em; -} \ No newline at end of file diff --git a/javascript/Gridfield.js b/javascript/Gridfield.js new file mode 100644 index 0000000..ffedf66 --- /dev/null +++ b/javascript/Gridfield.js @@ -0,0 +1,23 @@ +/** + * form builder behaviour. + */ + + (function($) { + $.entwine('ss', function($) { + $(".ss-gridfield-orderable tbody").entwine({ + onmatch: function() { + this._super(); + + this.find('.ss-gridfield-item') + .removeClass('inFieldGroup'); + + this.find('.ss-gridfield-item[data-class="EditableFieldGroup"]') + .nextUntil('.ss-gridfield-item[data-class="EditableFieldGroupEnd"]') + .addClass('inFieldGroup'); + }, + onunmatch: function () { + this._super(); + } + }); + }); +}(jQuery)); diff --git a/scss/UserForm.scss b/scss/UserForm.scss new file mode 100644 index 0000000..5842929 --- /dev/null +++ b/scss/UserForm.scss @@ -0,0 +1,47 @@ +/** + * Lightweight base styles for the front-end form. + */ + +.userform-progress { + .progress { + position: relative; + height: 1em; + background: #eee; + + .progress-bar { + position: absolute; + height: 1em; + background: #666; + } + } + + .step-buttons { + margin-left: 0; + position: relative; + + .step-button-wrapper { + display: inline-block; + list-style-type: none; + } + } + + .step-button-jump { + position: absolute; + top: 0; + } +} + +.step-navigation { + .step-buttons { + margin-left: 0; + + .step-button-wrapper { + display: inline-block; + list-style-type: none; + } + } +} + +.userform { + clear: both; +} diff --git a/scss/UserForm_cms.scss b/scss/UserForm_cms.scss new file mode 100644 index 0000000..52b2dad --- /dev/null +++ b/scss/UserForm_cms.scss @@ -0,0 +1,50 @@ +/** + * Styles for cms + */ + +.cms { + .ss-gridfield > div.ss-gridfield-buttonrow-before { + margin-bottom: 10px; + overflow: auto; + } + + table.ss-gridfield-table { + .ss-gridfield-item[data-class='EditableFormStep'] { + &, &:hover { + background: #dae2e7; + } + + label { + font-weight: bold; + color: #444; + font-size: 1.1em; + } + } + + .ss-gridfield-item[data-class^='EditableFieldGroup'] { + &, &:hover { + background: #E7EFF4; + } + + label { + font-weight: bold; + } + } + + .ss-gridfield-item[data-class='EditableFieldGroupEnd'] { + .col-Title input { + display: none; + } + } + + .ss-gridfield-item.inFieldGroup { + .col-reorder, + .handle { + background: #E7EFF4; + } + } + } +} + + + From 2bd70c3805e6cad68f5178c95c0d15e583cda59f Mon Sep 17 00:00:00 2001 From: scott1702 Date: Thu, 13 Aug 2015 11:13:13 +1200 Subject: [PATCH 5/6] allow js positioning to be removed --- javascript/UserForm.js | 5 +++-- templates/Includes/UserFormProgress.ss | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/javascript/UserForm.js b/javascript/UserForm.js index 1a4a4f3..f4bc055 100644 --- a/javascript/UserForm.js +++ b/javascript/UserForm.js @@ -322,6 +322,7 @@ jQuery(function ($) { this.$el = element instanceof jQuery ? element : $(element); this.$buttons = this.$el.find('.step-button-jump'); + this.$jsAlign = this.$el.find('.js-align'); // Update the progress bar when 'step' buttons are clicked. this.$buttons.each(function (i, stepButton) { @@ -337,9 +338,9 @@ jQuery(function ($) { }); // Spaces out the steps below progress bar evenly - this.$buttons.each(function (index, button) { + this.$jsAlign.each(function (index, button) { var $button = $(button), - leftPercent = (100 / (self.$buttons.length - 1) * index + '%'), + leftPercent = (100 / (self.$jsAlign.length - 1) * index + '%'), buttonOffset = -1 * ($button.innerWidth() / 2); $button.css({left: leftPercent, marginLeft: buttonOffset}); diff --git a/templates/Includes/UserFormProgress.ss b/templates/Includes/UserFormProgress.ss index 0af9a18..7496a89 100644 --- a/templates/Includes/UserFormProgress.ss +++ b/templates/Includes/UserFormProgress.ss @@ -10,7 +10,7 @@
    <% loop $NumberOfSteps %>
  • - +
  • <% end_loop %>
      From 7107ad70728c3b236591d459d1a873e5acd572fb Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 12 Aug 2015 17:18:43 +1200 Subject: [PATCH 6/6] API Formfield validation for nested groups and pages --- .../UserFormFieldEditorExtension.php | 2 +- code/extensions/UserFormValidator.php | 108 ++++++++++++++++++ code/model/UserDefinedForm.php | 8 ++ .../editableformfields/EditableFieldGroup.php | 35 +++++- .../EditableFieldGroupEnd.php | 53 ++++++++- .../editableformfields/EditableFormField.php | 4 + .../editableformfields/EditableFormStep.php | 26 ++--- 7 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 code/extensions/UserFormValidator.php diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php index 3dba1a8..29e342b 100644 --- a/code/extensions/UserFormFieldEditorExtension.php +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -115,7 +115,7 @@ class UserFormFieldEditorExtension extends DataExtension { // Add step $step = EditableFormStep::create(); - $step->Title = _t('EditableFormStep.TITLE_FIRST', 'First Step'); + $step->Title = _t('EditableFormStep.TITLE_FIRST', 'First Page'); $step->Sort = 1; $step->write(); $fields->add($step); diff --git a/code/extensions/UserFormValidator.php b/code/extensions/UserFormValidator.php new file mode 100644 index 0000000..90fb3ff --- /dev/null +++ b/code/extensions/UserFormValidator.php @@ -0,0 +1,108 @@ +filter('ParentID', $data['ID'])->sort('"Sort" ASC'); + + // Current nesting + $stack = array(); + foreach($fields as $field) { + if($field instanceof EditableFormStep) { + // Page at top level, or after another page is ok + if(empty($stack) || (count($stack) === 1 && $stack[0] instanceof EditableFormStep)) { + $stack = array($field); + continue; + } + + $this->validationError( + 'FormFields', + _t( + "UserFormValidator.UNEXPECTED_BREAK", + "Unexpected page break '{name}' inside nested field '{group}'", + array( + 'name' => $field->CMSTitle, + 'group' => end($stack)->CMSTitle + ) + ), + 'error' + ); + return false; + } + + // Validate no pages + if(empty($stack)) { + $this->validationError( + 'FormFields', + _t( + "UserFormValidator.NO_PAGE", + "Field '{name}' found before any pages", + array( + 'name' => $field->CMSTitle + ) + ), + 'error' + ); + return false; + } + + // Nest field group + if($field instanceof EditableFieldGroup) { + $stack[] = $field; + continue; + } + + // Unnest field group + if($field instanceof EditableFieldGroupEnd) { + $top = end($stack); + + // Check that the top is a group at all + if(!$top instanceof EditableFieldGroup) { + $this->validationError( + 'FormFields', + _t( + "UserFormValidator.UNEXPECTED_GROUP_END", + "'{name}' found without a matching group", + array( + 'name' => $field->CMSTitle + ) + ), + 'error' + ); + return false; + } + + // Check that the top is the right group + if($top->EndID != $field->ID) { + $this->validationError( + 'FormFields', + _t( + "UserFormValidator.WRONG_GROUP_END", + "'{name}' found closes the wrong group '{group}'", + array( + 'name' => $field->CMSTitle, + 'group' => $top->CMSTitle + ) + ), + 'error' + ); + return false; + } + + // Unnest group + array_pop($stack); + } + } + + return true; + } +} diff --git a/code/model/UserDefinedForm.php b/code/model/UserDefinedForm.php index 9f0cccf..5b5f215 100755 --- a/code/model/UserDefinedForm.php +++ b/code/model/UserDefinedForm.php @@ -286,6 +286,14 @@ SQL; DB::alteration_message('Migrated userforms', 'changed'); } + + + /** + * Validate formfields + */ + public function getCMSValidator() { + return new UserFormValidator(); + } } /** diff --git a/code/model/editableformfields/EditableFieldGroup.php b/code/model/editableformfields/EditableFieldGroup.php index 83c8fa1..24c0bfe 100644 --- a/code/model/editableformfields/EditableFieldGroup.php +++ b/code/model/editableformfields/EditableFieldGroup.php @@ -5,6 +5,10 @@ */ class EditableFieldGroup extends EditableFormField { + private static $has_one = array( + 'End' => 'EditableFieldGroupEnd' + ); + /** * Disable selection of group class * @@ -19,13 +23,20 @@ class EditableFieldGroup extends EditableFormField { return $fields; } - public function getInlineClassnameField($column, $fieldClasses) { - return new LabelField( - $column, - _t('EditableFieldGroup.FIELD_GROUP_START', 'Field Group (start)') + public function getCMSTitle() { + return _t( + 'EditableFieldGroupEnd.FIELD_GROUP_START', + 'Start of {group}', + array( + 'group' => $this->Title ?: 'group' + ) ); } + public function getInlineClassnameField($column, $fieldClasses) { + return new LabelField($column, $this->CMSTitle); + } + public function showInReports() { return false; } @@ -49,5 +60,21 @@ class EditableFieldGroup extends EditableFormField { $field->addExtraClass($field->ExtraClass); } } + + protected function onBeforeDelete() { + parent::onBeforeDelete(); + + // Ensures EndID is lazy-loaded for onAfterDelete + $this->EndID; + } + + protected function onAfterDelete() { + parent::onAfterDelete(); + + // Delete end + if(($end = $this->End()) && $end->exists()) { + $end->delete(); + } + } } diff --git a/code/model/editableformfields/EditableFieldGroupEnd.php b/code/model/editableformfields/EditableFieldGroupEnd.php index 53a4e52..e1a6e6f 100644 --- a/code/model/editableformfields/EditableFieldGroupEnd.php +++ b/code/model/editableformfields/EditableFieldGroupEnd.php @@ -5,6 +5,10 @@ */ class EditableFieldGroupEnd extends EditableFormField { + private static $belongs_to = array( + 'Group' => 'EditableFieldGroup' + ); + /** * Disable selection of group class * @@ -13,6 +17,17 @@ class EditableFieldGroupEnd extends EditableFormField { */ private static $hidden = true; + public function getCMSTitle() { + $group = $this->Group(); + return _t( + 'EditableFieldGroupEnd.FIELD_GROUP_END', + 'End of {group}', + array( + 'group' => ($group && $group->exists() && $group->Title) ? $group->Title : 'group' + ) + ); + } + public function getCMSFields() { $fields = parent::getCMSFields(); $fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules')); @@ -20,10 +35,7 @@ class EditableFieldGroupEnd extends EditableFormField { } public function getInlineClassnameField($column, $fieldClasses) { - return new LabelField( - $column, - _t('EditableFieldGroupEnd.FIELD_GROUP_END', 'Field Group (end)') - ); + return new LabelField($column, $this->CMSTitle); } public function getInlineTitleField($column) { @@ -38,8 +50,37 @@ class EditableFieldGroupEnd extends EditableFormField { return false; } - public function canEdit($member = null) { - return false; + public function onAfterWrite() { + parent::onAfterWrite(); + + // If this is not attached to a group, find the first group prior to this + // with no end attached + $group = $this->Group(); + if(!($group && $group->exists()) && $this->ParentID) { + $group = EditableFieldGroup::get() + ->filter(array( + 'ParentID' => $this->ParentID, + 'Sort:LessThanOrEqual' => $this->Sort + )) + ->where('"EditableFieldGroup"."EndID" IS NULL OR "EditableFieldGroup"."EndID" = 0') + ->sort('"Sort" DESC') + ->first(); + + // When a group is found, attach it to this end + if($group) { + $group->EndID = $this->ID; + $group->write(); + } + } + } + + protected function onAfterDelete() { + parent::onAfterDelete(); + + // Delete group + if(($group = $this->Group()) && $group->exists()) { + $group->delete(); + } } } diff --git a/code/model/editableformfields/EditableFormField.php b/code/model/editableformfields/EditableFormField.php index 49de232..1dc0c2a 100755 --- a/code/model/editableformfields/EditableFormField.php +++ b/code/model/editableformfields/EditableFormField.php @@ -450,6 +450,10 @@ class EditableFormField extends DataObject { return Convert::raw2xml($this->Title); } + public function getCMSTitle() { + return $this->i18n_singular_name() . ' (' . $this->Title . ')'; + } + /** * @deprecated since version 4.0 */ diff --git a/code/model/editableformfields/EditableFormStep.php b/code/model/editableformfields/EditableFormStep.php index 6de7062..a89face 100644 --- a/code/model/editableformfields/EditableFormStep.php +++ b/code/model/editableformfields/EditableFormStep.php @@ -6,6 +6,10 @@ */ class EditableFormStep extends EditableFormField { + private static $singular_name = 'Page Break'; + + private static $plural_name = 'Page Breaks'; + /** * Disable selection of step class * @@ -14,18 +18,6 @@ class EditableFormStep extends EditableFormField { */ private static $hidden = true; - /** - * @config - * @var string - */ - private static $singular_name = 'Step'; - - /** - * @config - * @var string - */ - private static $plural_name = 'Steps'; - /** * @return FieldList */ @@ -64,7 +56,15 @@ class EditableFormStep extends EditableFormField { public function getInlineClassnameField($column, $fieldClasses) { return new LabelField( $column, - _t('EditableFieldGroupEnd.PAGE_BREAK', 'Page Break') + $this->CMSTitle ); } + + public function getCMSTitle() { + $title = $this->i18n_singular_name(); + if($this->Title) { + $title .= ' (' . $this->Title . ')'; + } + return $title; + } }