chg: [component:CRUD] Remove usage of custom header + added custom form

validation feedback
pull/37/head
mokaddem 2021-01-12 10:16:58 +01:00
parent 7029341e40
commit b93dd49232
5 changed files with 92 additions and 29 deletions

View File

@ -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')));
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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 = $('<div></div>')
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($('<li></li>').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()
}
}