Skip to content

Commit

Permalink
ENH Show validation errors next to fields
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Aug 26, 2021
1 parent f626840 commit b543861
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 14 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/src/components/ElementEditor/Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class Element extends Component {
* Expand the element to show the preview
* If the element is not inline-editable, take user to the GridFieldDetailForm to edit the record
*/
handleExpand(event) {
handleExpand(event) { // sboyd
const { type, link } = this.props;
const { loadingError } = this.state;

Expand Down
48 changes: 38 additions & 10 deletions client/src/legacy/ElementEditor/entwine.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { destroy } from 'redux-form';
/**
* Reset the Apollo and Redux stores holding data relating to elemental inline edit forms
*/
const resetStores = () => {
const resetStores = (invalidFields) => {
// After page level saves we need to reload all the blocks from the server. We can remove
// this if we can figure out a way to optimistically update the apollo cache. See:
// https://github.com/dnadesign/silverstripe-elemental/pull/439#issuecomment-428773370
Expand All @@ -28,9 +28,31 @@ const resetStores = () => {
}

// We can introspect the store to find form names in the `element` namespace
store.dispatch(destroy(
...Object.keys(store.getState().form.formState.element || {}).map(name => `element.${name}`)
));
const keys = Object.keys(store.getState().form.formState.element || {});

const keysToDelete = {};
keys.forEach(key => {
keysToDelete[key] = true;
});

// However we don't want to remove elements from the store that have invalid fields
// This is because we want redux to hydrate the form, rather than the fresh request apollo
// from Apollo which will return a value from the database. Instead the user should still
// see the invalidate value
// they just attempted to enter
invalidFields.forEach(invalidField => {
const match = invalidField.match(/^PageElements_([0-9]+)_/);
if (!match) {
return;
}
const invalidElementKey = `'ElementForm_${match[1]}`;
if (keysToDelete.hasOwnProperty(invalidElementKey)) {
delete keysToDelete[invalidElementKey];
}
});

// Delete data of valid elements from redux so that Apollo can refresh from the server
store.dispatch(destroy(...Object.keys(keysToDelete).map(name => `element.${name}`)));
}, 0);
};

Expand Down Expand Up @@ -60,16 +82,22 @@ jQuery.entwine('ss', ($) => {
},

onunmatch() {
resetStores();
resetStores([]);
ReactDOM.unmountComponentAtNode(this[0]);
},

/**
* Invalidate cache after the form is submitted to force apollo to re-fetch.
*/
'from .cms-edit-form': {
onaftersubmitform() {
resetStores();
onaftersubmitform(event, data) {
const validationResultPjax = JSON.parse(data.xhr.responseText).ValidationResult;
const validationResult = JSON.parse(validationResultPjax.replace(/<\/?div>/g, ''));
const invalidFields = [];
if (!validationResult.isValid) {
validationResult.messages.forEach(message => {
invalidFields.push(message.fieldName);
});
}
// Invalidate cache after the form is submitted to force apollo to re-fetch.
resetStores(invalidFields);
}
},
});
Expand Down
37 changes: 35 additions & 2 deletions src/Forms/ElementalAreaField.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;

class ElementalAreaField extends GridField
Expand Down Expand Up @@ -231,6 +233,7 @@ public function saveInto(DataObjectInterface $dataObject)
return;
}

$elements = [];
foreach ($elementData as $form => $data) {
// Extract the ID
$elementId = (int) substr($form, $idPrefixLength);
Expand All @@ -242,10 +245,40 @@ public function saveInto(DataObjectInterface $dataObject)
// Ignore invalid elements
continue;
}

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

$element->updateFromFormData($data);
$elements[] = $element;
}

// big validation result containing all the element validation errors within the elemental area
$thrownValidationResult = null;
/** @var BaseElement $element */
foreach ($elements as $element) {
/** @var ValidationResult $validationResult */
$validationResult = $element->validate();
if ($validationResult->isValid()) {
continue;
}
foreach ($validationResult->getMessages() as $message) {
if (!$thrownValidationResult) {
$thrownValidationResult = new ValidationResult();
}
$thrownValidationResult->addFieldError(
// TODO: update PageElements to whatever it is above
implode('_', ['PageElements', $element->ID, $message['fieldName'] ?? '']),
$message['message'] ?? '',
$message['messageType'] ?? ValidationResult::TYPE_ERROR,
$message['messageCode'] ?? null,
$message['messageCast'] ?? ValidationResult::CAST_TEXT
);
}
}

if ($thrownValidationResult) {
throw new ValidationException($thrownValidationResult);
}

foreach ($elements as $element) {
$element->write();
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/Models/BaseElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\View\Requirements;
use RuntimeException;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\ValidationResult;

/**
* Class BaseElement
Expand Down Expand Up @@ -1069,4 +1071,19 @@ public static function getGraphQLTypeName(): string
? StaticSchema::inst()->typeNameForDataObject(static::class)
: str_replace('\\', '_', static::class);
}

public function validate()
{
/** @var ValidationResult $result */
$result = parent::validate(); // DataObject::validate()
if (empty($this->Title)) { // sboyd
$result->addFieldError('Title', 'Is empty!!!!');
}
return $result;
}

public function getCMSValidator()
{
return new RequiredFields(['Title']);
}
}

0 comments on commit b543861

Please sign in to comment.