Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX Improve non-inline validation #1178

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Controllers/ElementalAreaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public function save(array $data, Form $form): HTTPResponse
$dataWithoutNamespaces = static::removeNamespacesFromFields($data, $element->ID);

// Update and write the data object which will trigger model validation
$element->update($dataWithoutNamespaces);
$element->updateFromFormData($dataWithoutNamespaces);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this code change in from a different PR that's blocked on this one which is required to get the inline-block-validation.feature behat tests to pass

if ($element->isChanged()) {
try {
$element->write();
Expand Down
82 changes: 68 additions & 14 deletions src/Forms/ElementalAreaField.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataObjectInterface;
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;

class ElementalAreaField extends GridField
{
Expand Down Expand Up @@ -214,33 +217,84 @@ public function setSubmittedValue($value, $data = null)
return $this->setValue(json_decode($value ?? '', true));
}

/**
* This will perform FormField validation
* DataObject validation will happen in saveInto() as part of $element->write()
*/
public function validate($validator)
Copy link
Member Author

@emteknetnz emteknetnz Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously FormField validation wasn't happening on fields on elements e.g. $db = [ 'MyUrl' => UrlField::class ] when saving the page (as opposed to editing the element directly)

{
$result = true;
foreach ($this->getElementData() as $arr) {
/** @var BaseElement $element */
/** @var array $data */
$element = $arr['element'];
$data = $arr['data'];
$elementForm = $this->getElementForm($element);
if (!$elementForm) {
continue;
}
$elementForm->loadDataFrom($data);
$formValidatorResult = $elementForm->getValidator()->validate();
if (!$formValidatorResult->isValid()) {
$validator->getResult()->combineAnd($formValidatorResult);
$result = false;
}
}
return $this->extendValidationResult($result, $validator);
}

public function saveInto(DataObjectInterface $dataObject)
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
{
parent::saveInto($dataObject);

$elementData = $this->Value();
$idPrefixLength = strlen(sprintf(ElementalAreaController::FORM_NAME_TEMPLATE ?? '', ''));

if (!$elementData) {
return;
foreach ($this->getElementData() as $arr) {
/** @var BaseElement $element */
/** @var array $data */
$element = $arr['element'];
$data = $arr['data'];
$element->updateFromFormData($data);
$element->write();
}
}

foreach ($elementData as $form => $data) {
private function getElementData(): array
{
$elementData = [];
$value = $this->Value();
if (!$value) {
return [];
}
$idPrefixLength = strlen(sprintf(ElementalAreaController::FORM_NAME_TEMPLATE ?? '', ''));
foreach ($value as $form => $data) {
// Extract the ID
$elementId = (int) substr($form ?? '', $idPrefixLength ?? 0);

/** @var BaseElement $element */
$element = $this->getArea()->Elements()->byID($elementId);

if (!$element) {
// Ignore invalid elements
continue;
}

$data = ElementalAreaController::removeNamespacesFromFields($data, $element->ID);

$element->updateFromFormData($data);
$element->write();
$elementData[] = ['element' => $element, 'data' => $data];
}
return $elementData;
}

private function getElementForm(BaseElement $element): ?Form
{
// This is essentially the same method used to generate a form to edit an element
// that a non-inline edit form will use - see GridFieldDetailForm::handleItem()
$requestHandler = $requestHandler = $this->getForm()->getController();
$gridFieldDetailForm = new GridFieldDetailForm();
// The validator needs to be explicitly copied from the element to the form
$validator = $element->getCMSCompositeValidator();
$gridFieldDetailForm->setValidator($validator);
$gridFieldDetailFormItemRequest = new GridFieldDetailForm_ItemRequest(
$this,
$gridFieldDetailForm,
$element,
$requestHandler,
''
);
$form = $gridFieldDetailFormItemRequest->ItemEditForm();
return $form;
}
}
43 changes: 43 additions & 0 deletions src/Forms/TextCheckboxGroupField.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,49 @@ public function __construct($title = null)
$this->setTitle($title);
}

public function getMessage()
{
$message = parent::getMessage();
if ($message) {
return $message;
}
foreach ($this->getChildren() as $field) {
$message = $field->getMessage();
if ($message) {
return $message;
}
}
michalkleiner marked this conversation as resolved.
Show resolved Hide resolved
return '';
}

public function getMessageType()
{
$message = parent::getMessage();
if ($message) {
return parent::getMessageType();
}
foreach ($this->getChildren() as $field) {
if ($field->getMessage()) {
return $field->getMessageType();
}
}
return '';
}

public function getMessageCast()
{
$message = parent::getMessage();
if ($message) {
return parent::getMessageCast();
}
foreach ($this->getChildren() as $field) {
if ($field->getMessage()) {
return $field->getMessageCast();
}
}
return '';
}

/**
* Don't use the custom template for readonly states
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@retry
Feature: Blocks are validated when inline saving individual blocks
As a CMS user
I want to blocks have be validating when individual saving them
Expand All @@ -14,6 +15,10 @@ Feature: Blocks are validated when inline saving individual blocks
And I go to "/admin/pages"
And I follow "Blocks Page"
And I click on the caret button for block 1
And I click on the "#Form_ElementForm_1_PageElements_1_MyPageID" element
And I click on the ".ss-searchable-dropdown-field__option:nth-of-type(2)" element
And I press the "View actions" button
And I click on the ".element-editor__actions-save" element

# Note that each test is split into a seperate scenario instead a large single scenario which would
# be faster due to a limitation with behat testing react where changing the value of a field can
Expand All @@ -26,6 +31,7 @@ Feature: Blocks are validated when inline saving individual blocks
# Will not be an inline save button because formDirty not set yet, intercepted by JS validation
Then I should not see a ".element-editor__actions-save" element

@sboyd
Scenario: Field validation error
When I fill in "x" for "Title" for block 1
When I press the "View actions" button
Expand Down
56 changes: 56 additions & 0 deletions tests/Behat/features/non-inline-block-validation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@retry
Feature: Blocks are validated when non-inline saving blocks
As a CMS user
I want to blocks have be validating when non-inline saving them
So that I can see what content needs to be fixed

Background:
Given I add an extension "DNADesign\Elemental\Extensions\ElementalPageExtension" to the "Page" class
And I add an extension "SilverStripe\FrameworkTest\Elemental\Extension\ElementContentExtension" to the "DNADesign\Elemental\Models\ElementContent" class
And I add an extension "SilverStripe\FrameworkTest\Elemental\Extension\NumericFieldExtension" to the "SilverStripe\Forms\NumericField" class
And content blocks are not in-line editable
And I go to "/dev/build?flush"
And a "page" "Blocks Page" with a "My title" content element with "My content" content
And the "group" "EDITOR" has permissions "Access to 'Pages' section"
And I am logged in as a member of "EDITOR" group
And I go to "/admin/pages"
And I follow "Blocks Page"
And I click on the caret button for block 1

Scenario: Non-inline block validation

# Related has_one RequiredFields
When I press the "Save" button
Then I should see "\"My page\" is required" in the "#message-Form_ItemEditForm_MyPageID" element
And I click on the "#Form_ItemEditForm_MyPageID" element
And I click on the ".ss-searchable-dropdown-field__option:nth-of-type(2)" element

# RequiredFields on TextCheckboxGroupField (composite) field
When I fill in "Title" with ""
And I press the "Save" button
Then I should see "\"Title\" is required" in the "#message-Form_ItemEditForm_Title" element
And I fill in "Title" with "My title"

# FormField::validate()
When I fill in "My Int" with "1"
And I press the "Save" button
Then I should see "This field cannot be 1" in the "#message-Form_ItemEditForm_MyInt" element
And I fill in "My Int" with "2"

# DataObject::validate() addFieldError()
When I fill in "My Field" with "x"
And I press the "Save" button
Then I should see "MyField cannot be x" in the "#message-Form_ItemEditForm_MyField" element
And I fill in "My Field" with "lorem"

# DataObject::validate() addError()
When I fill in "Title" with "z"
And I fill in "My Field" with "z"
And I press the "Save" button
Then I should see "This is a general error message" in the "#Form_ItemEditForm_error" element
And I fill in "Title" with "My title"
And I fill in "My Field" with "lorem"

# Success message
When I press the "Save" button
Then I should see "Saved content block \"My title\"" in the "#Form_ItemEditForm_error" element
60 changes: 60 additions & 0 deletions tests/Behat/features/page-save-validation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@retry
Feature: Blocks are validated when page saving blocks
As a CMS user
I want to blocks be validated when page saving blocks
So that I can see what content needs to be fixed

Background:
Given I add an extension "DNADesign\Elemental\Extensions\ElementalPageExtension" to the "Page" class
And I add an extension "SilverStripe\FrameworkTest\Elemental\Extension\ElementContentExtension" to the "DNADesign\Elemental\Models\ElementContent" class
And I add an extension "SilverStripe\FrameworkTest\Elemental\Extension\NumericFieldExtension" to the "SilverStripe\Forms\NumericField" class
And I go to "/dev/build?flush"
And a "page" "Blocks Page" with a "My title" content element with "My content" content
And the "group" "EDITOR" has permissions "Access to 'Pages' section"
And I am logged in as a member of "EDITOR" group
And I go to "/admin/pages"
And I follow "Blocks Page"
And I click on the caret button for block 1
And I click on the "#Form_ElementForm_1_PageElements_1_MyPageID" element
And I click on the ".ss-searchable-dropdown-field__option:nth-of-type(2)" element

Scenario: Validation when page saving inline blocks

# Related has_one RequiredFields
When I click on the "#Form_ElementForm_1_PageElements_1_MyPageID" element
And I click on the ".ss-searchable-dropdown-field__option:nth-of-type(1)" element
And I press the "Save" button
Then I should see "\"My page\" is required" in the "#Form_EditForm_error" element
When I click on the caret button for block 1
And I click on the "#Form_ElementForm_1_PageElements_1_MyPageID" element
And I click on the ".ss-searchable-dropdown-field__option:nth-of-type(2)" element

# FormField::validate()
And I fill in "1" for "My Int" for block 1
And I press the "Save" button
Then I should see "This field cannot be 1" in the "#Form_EditForm_error" element
And I click on the caret button for block 1
And I fill in "2" for "My Int" for block 1

# DataObject::validate() addFieldError()
# Note that this currently has poor UX and will show the validation message on the
# page Title field, rather than on the element Title field. This is expected.
And I fill in "x" for "Title" for block 1
And I press the "Save" button
Then I should see "There are validation errors on this page, please fix them before saving or publishing." in the "#Form_EditForm_error" element
Then I should see "Title cannot be x" in the "#message-Form_EditForm_Title" element
And I click on the caret button for block 1
And I fill in "lorem" for "Title" for block 1

# DataObject::validate() addError()
And I fill in "z" for "Title" for block 1
And I fill in "z" for "My Field" for block 1
And I press the "Save" button
Then I should see "This is a general error message" in the "#Form_EditForm_error" element
And I click on the caret button for block 1
And I fill in "some title" for "Title" for block 1
And I fill in "lorem" for "My Field" for block 1

# Success message
When I press the "Save" button
Then I should see a "Saved 'Blocks Page' successfully." success toast
Loading
Loading