Skip to content

Commit

Permalink
SPIKE Inline validation
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Oct 17, 2023
1 parent 768854b commit 20e3fbf
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 6 deletions.
93 changes: 87 additions & 6 deletions src/Controllers/ElementalAreaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\SecurityToken;
use SilverStripe\ORM\ValidationResult;

/**
* Controller for "ElementalArea" - handles loading and saving of in-line edit forms in an elemental area in admin
Expand Down Expand Up @@ -115,8 +117,9 @@ public function getElementForm($elementID)
*/
public function apiSaveForm(HTTPRequest $request)
{
$id = $this->urlParams['ID'] ?? 0;
// Validate required input data
if (!isset($this->urlParams['ID'])) {
if ($id === 0) {
$this->jsonError(400);
return null;
}
Expand All @@ -139,7 +142,7 @@ public function apiSaveForm(HTTPRequest $request)
}

/** @var BaseElement $element */
$element = BaseElement::get()->byID($this->urlParams['ID']);
$element = BaseElement::get()->byID($id);
// Ensure the element can be edited by the current user
if (!$element || !$element->canEdit()) {
$this->jsonError(403);
Expand All @@ -149,6 +152,42 @@ public function apiSaveForm(HTTPRequest $request)
// Remove the pseudo namespaces that were added by the form factory
$data = $this->removeNamespacesFromFields($data, $element->ID);

// create a temporary Form to use for validation - will contain existing dataobject values
$form = $this->getElementForm($id);
// remove element namespaces from fields so that something like RequiredFields('Title') works
// element namespaces are added in DNADesign\Elemental\Forms\EditFormFactory
foreach ($form->Fields()->flattenFields() as $field) {
$rx = '#^PageElements_[0-9]+_#';
$namespacedName = $field->getName();
if (!preg_match($rx, $namespacedName)) {
continue;
}
$regularName = preg_replace($rx, '', $namespacedName);
// If there's an existing field with the same name, remove it
// this is probably a workaround for EditFormFactory creating too many fields?
// e.g. for element #2 there's a "Title" field and a "PageElements_2_Title" field
// same with "SecurityID" and "PageElements_2_SecurityID"
// possibly this would be better to just remove fields if they match the rx, not sure,
// this approach seems more conservative
if ($form->Fields()->flattenFields()->fieldByName($regularName)) {
$form->Fields()->removeByName($regularName);
}
// update the name of the field
$field->setName($regularName);
}
// merge submitted data into the form
$form->loadDataFrom($data);

$errorMessages = [];

// Validate the Form
/** @var ValidationResult|null $validationResult */
$validationResult = $form->getValidator()?->validate();
if ($validationResult && !$validationResult->isValid()) {
// add error messages from Form validation
$errorMessages = array_merge($errorMessages, $validationResult->getMessages());
}

try {
$updated = false;

Expand All @@ -159,11 +198,18 @@ public function apiSaveForm(HTTPRequest $request)
// Track changes so we can return to the client
$updated = true;
}
} catch (Exception $ex) {
Injector::inst()->get(LoggerInterface::class)->debug($ex->getMessage());
} catch (ValidationException $e) {
// add error messages from DataObject validation
$errorMessages = array_merge($errorMessages, $e->getResult()->getMessages());
}

$this->jsonError(500);
return null;
if (count($errorMessages) > 0) {
// re-prefix fields before sending json error
foreach ($errorMessages as $key => &$message) {
$fieldName = $message['fieldName'];
$message['fieldName'] = "PageElements_{$id}_{$fieldName}";
}
$this->jsonError(400, $errorMessages);
}

$body = json_encode([
Expand All @@ -173,6 +219,41 @@ public function apiSaveForm(HTTPRequest $request)
return HTTPResponse::create($body)->addHeader('Content-Type', 'application/json');
}

/**
* Override LeftAndMain::jsonError() to allow multiple error messages
*
* This is fairly ridicious, it's really for demo purposes
* We could use this though we'd be better off updating LeftAndMain::jsonError() to support multiple errors
*/
public function jsonError($errorCode, $errorMessage = null)
{
try {
parent::jsonError($errorCode, $errorMessage);
} catch (HTTPResponse_Exception $e) {
// JeftAndMain::jsonError() will always throw this exception
if (!is_array($errorMessage)) {
// single error, no need to update
throw $e;
}
// multiple errors
$response = $e->getResponse();
$json = json_decode($response->getBody(), true);
$errors = [];
foreach ($errorMessage as $message) {
$errors[] = [
'type' => 'error',
'code' => $errorCode,
'value' => $message
];
}
$json['errors'] = $errors;
$body = json_encode($json);
$response->setBody($body);
$e->setResponse($response);
throw $e;
}
}

/**
* Provides action control for form fields that are request handlers when they're used in an in-line edit form.
*
Expand Down
24 changes: 24 additions & 0 deletions src/Forms/EditFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use SilverStripe\Forms\DefaultFormFactory;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\ORM\DataObject;
use SilverStripe\Forms\RequiredFields;

class EditFormFactory extends DefaultFormFactory
{
Expand Down Expand Up @@ -55,6 +57,28 @@ protected function getFormFields(RequestHandler $controller = null, $name, $cont
return $fields;
}

protected function getFormValidator(RequestHandler $controller = null, $name, $context = [])
{
$compositeValidator = parent::getFormValidator($controller, $name, $context);
if (!$compositeValidator) {
return null;
}
$id = $context['Record']->ID;
// append field prefixes
// this is done so that front end validation works, at least for RequiredFields
foreach ($compositeValidator->getValidators() as $validator) {
if (is_a($validator, RequiredFields::class)) {
$requiredFields = $validator->getRequired();
foreach ($requiredFields as $requiredField) {
$validator->removeRequiredField($requiredField);
$prefixedRequiredField = "PageElements_{$id}_$requiredField";
$validator->addRequiredField($prefixedRequiredField);
}
}
}
return $compositeValidator;
}

/**
* Given a {@link FieldList}, give all fields a unique name so they can be used in the same context as
* other elemental edit forms and the page (or other DataObject) that owns them.
Expand Down
2 changes: 2 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\Requirements;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\DataObjectSchema;

/**
Expand Down

0 comments on commit 20e3fbf

Please sign in to comment.