From b93dd4923294cb7ee2df04f38390f25962c58fb1 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Tue, 12 Jan 2021 10:16:58 +0100 Subject: [PATCH] chg: [component:CRUD] Remove usage of custom header + added custom form validation feedback --- src/Controller/AppController.php | 3 - src/Controller/Component/CRUDComponent.php | 9 +-- src/Model/Table/UsersTable.php | 4 +- webroot/js/api-helper.js | 38 ++++++------ webroot/js/bootstrap-helper.js | 67 ++++++++++++++++++++++ 5 files changed, 92 insertions(+), 29 deletions(-) diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 70d77a2..07ec3aa 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -110,9 +110,6 @@ class AppController extends Controller $this->ACL->checkAccess(); $this->set('menu', $this->ACL->getMenu()); $this->set('ajax', $this->request->is('ajax')); - if (!empty($this->request->getHeader('X-Force-HTML-On-Validation-Failure'))) { - $this->ajax_with_html_on_failure = true; - } $this->request->getParam('prefix'); $this->set('darkMode', !empty(Configure::read('Cerebrate.dark'))); } diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 171533c..d96dd11 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -59,9 +59,7 @@ class CRUDComponent extends Component if ($this->Controller->ParamHandler->isRest()) { return $this->Controller->restResponsePayload; } else if ($this->Controller->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) { - if (empty($this->Controller->isFailResponse) || empty($this->Controller->ajax_with_html_on_failure)) { - return $this->Controller->ajaxResponsePayload; - } + return $this->Controller->ajaxResponsePayload; } return false; } @@ -245,13 +243,12 @@ class CRUDComponent extends Component } else { $validationMessage = $this->prepareValidationError($data); $message = __( - '{0} could not be modified.{1}', + __('{0} could not be modified.'), $this->ObjectAlias, - empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $data, $message, $validationMessage); + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $data->getErrors()); } else { $this->Controller->Flash->error($message); } diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index d294914..09a142e 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -56,7 +56,9 @@ class UsersTable extends AppTable }, 'message' => __('Password confirmation missing or not matching the password.') ] - ]); + ]) + ->requirePresence(['username'], 'create') + ->notEmptyString('username', 'Please fill this field'); return $validator; } diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index bb8e128..8e0a1b5 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -11,22 +11,16 @@ class AJAXApi { redirect: 'manual', method: 'POST', } - static renderHTMLOnFailureHeader = { - name: 'X-Force-HTML-On-Validation-Failure', - value: '1' - } /** * @namespace * @property {boolean} provideFeedback - The ID to be used for the toast's container * @property {(jQuery|string)} statusNode - The node on which the loading overlay should be placed (OverlayFactory.node) - * @property {boolean} forceHTMLOnValidationFailure - If true, attach a special header to ask for HTML instead of JSON in case of form validation failure * @property {Object} errorToast - The options supported by Toaster#defaultOptions */ static defaultOptions = { provideFeedback: true, statusNode: false, - forceHTMLOnValidationFailure: false, errorToast: { delay: 10000 } @@ -222,9 +216,6 @@ class AJAXApi { let formData = new FormData(form) formData = AJAXApi.mergeFormData(formData, dataToMerge) let requestConfig = AJAXApi.genericRequestConfigPOST - if (this.options.forceHTMLOnValidationFailure) { - requestConfig.headers.append(AJAXApi.renderHTMLOnFailureHeader.name, AJAXApi.renderHTMLOnFailureHeader.value) - } let options = { ...requestConfig, body: formData, @@ -246,19 +237,19 @@ class AJAXApi { this.provideFeedback({ variant: 'danger', title: 'There has been a problem with the operation', - body: data.errors + body: data.message }, true, skipFeedback); feedbackShown = true + this.injectFormValidation(form, data.errors) toReturn = Promise.reject(data.errors); } - } catch (error) { // could not parse JSON - if (this.options.forceHTMLOnValidationFailure) { - const data = await clonedResponse.text(); - toReturn = { - success: 0, - html: data, - } - } + } catch (error) { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: error + }, true, feedbackShown); + toReturn = Promise.reject(error); } } catch (error) { this.provideFeedback({ @@ -268,7 +259,7 @@ class AJAXApi { }, true, feedbackShown); toReturn = Promise.reject(error); } - } catch (error) { // -> probably not useful + } catch (error) { toReturn = Promise.reject(error); } finally { if (!skipRequestHooks) { @@ -302,6 +293,15 @@ class AJAXApi { return toReturn } + /** + * @param {HTMLFormElement} form - The form form which the POST operation is coming from + * @param {Object} [validationErrors={}] - Validation errors reported by the server + */ + injectFormValidation(form, validationErrors) { + const formHelper = new FormHelper(form) + formHelper.injectValidationErrors(validationErrors) + } + /** Based on the configuration, show the loading overlay */ beforeRequest() { if (this.options.statusNode !== false) { diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 93bf054..c740e7d 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -696,4 +696,71 @@ class OverlayFactory { } return ''; } +} + +/** Class representing a FormHelper */ +class FormHelper { + /** + * Create a FormHelper. + * @param {Object} options - The options supported by Toaster#defaultOptions + */ + constructor(form, options={}) { + this.form = form + this.options = Object.assign({}, Toaster.defaultOptions, options) + } + + /** + * @namespace + */ + static defaultOptions = { + } + + /** + * Create node containing validation information from validationError. If no field can be associated to the error, it will be placed on top + * @param {Object} validationErrors - The validation errors to be displayed + */ + injectValidationErrors(validationErrors) { + this.cleanValidationErrors() + for (const [fieldName, errors] of Object.entries(validationErrors)) { + this.injectValidationErrorInForm(fieldName, errors) + } + } + + injectValidationErrorInForm(fieldName, errors) { + const inputField = Array.from(this.form).find(node => { return node.name == fieldName }) + if (inputField !== undefined) { + const $messageNode = this.buildValidationMessageNode(errors) + const $inputField = $(inputField) + $inputField.addClass('is-invalid') + $messageNode.insertAfter($inputField) + } else { + const $messageNode = this.buildValidationMessageNode(errors, true) + const $flashContainer = $(this.form).parent().find('#flashContainer') + $messageNode.insertAfter($flashContainer) + } + } + + buildValidationMessageNode(errors, isAlert=false) { + const $messageNode = $('
') + if (isAlert) { + $messageNode.addClass('alert alert-danger').attr('role', 'alert') + } else { + $messageNode.addClass('invalid-feedback') + } + const isList = Object.keys(errors).length > 1 + for (const [ruleName, error] of Object.entries(errors)) { + if (isList) { + $messageNode.append($('
  • ').text(error)) + } else { + $messageNode.text(error) + } + } + return $messageNode + } + + cleanValidationErrors() { + $(this.form).find('textarea, input, select').removeClass('is-invalid') + $(this.form).find('.invalid-feedback').remove() + $(this.form).parent().find('.alert').remove() + } } \ No newline at end of file