From 20e3fbff837efd2dd510ea6f4a831a3e295b9d21 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 17 Oct 2023 19:24:46 +1300 Subject: [PATCH] SPIKE Inline validation --- src/Controllers/ElementalAreaController.php | 93 +++++++++++++++++++-- src/Forms/EditFormFactory.php | 24 ++++++ src/Models/BaseElement.php | 2 + 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/Controllers/ElementalAreaController.php b/src/Controllers/ElementalAreaController.php index 283b6b89..28524bd5 100644 --- a/src/Controllers/ElementalAreaController.php +++ b/src/Controllers/ElementalAreaController.php @@ -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 @@ -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; } @@ -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); @@ -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; @@ -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([ @@ -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. * diff --git a/src/Forms/EditFormFactory.php b/src/Forms/EditFormFactory.php index c8579af7..149e30a3 100644 --- a/src/Forms/EditFormFactory.php +++ b/src/Forms/EditFormFactory.php @@ -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 { @@ -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. diff --git a/src/Models/BaseElement.php b/src/Models/BaseElement.php index b7579eee..da76bf40 100644 --- a/src/Models/BaseElement.php +++ b/src/Models/BaseElement.php @@ -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; /**