chg: [component:CRUD] Remove usage of custom header + added custom form
validation feedbackpull/37/head
parent
7029341e40
commit
b93dd49232
|
@ -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')));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -697,3 +697,70 @@ 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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue