Dont let validation issues prevent saving (#9691)

* skipValidation for content save

* Correcting merge

* Use warning style when saving

* final corrections

* skip client side validation

* remove log

* show invariant property validation issues in the save dialog

* use warning color for .show-validation-type-warning

Co-authored-by: Niels Lyngsø <nsl@umbraco.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
This commit is contained in:
Niels Lyngsø
2021-05-05 11:18:46 +02:00
committed by GitHub
parent 7ab09cb404
commit 6e16550b84
16 changed files with 239 additions and 76 deletions

View File

@@ -442,7 +442,6 @@
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
//Used to check validility of nested form - coming from Content Apps mostly
//Set them all to be invalid
var fieldsToRollback = checkValidility();
@@ -455,7 +454,8 @@
create: $scope.page.isNew,
action: args.action,
showNotifications: args.showNotifications,
softRedirect: true
softRedirect: true,
skipValidation: args.skipValidation
}).then(function (data) {
//success
init();
@@ -467,23 +467,24 @@
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: true });
resetNestedFieldValiation(fieldsToRollback);
if($scope.contentForm.$invalid !== true) {
resetNestedFieldValiation(fieldsToRollback);
}
ensureDirtyIsSetIfAnyVariantIsDirty();
return $q.when(data);
},
function (err) {
syncTreeNode($scope.content, $scope.content.path);
if($scope.contentForm.$invalid !== true) {
resetNestedFieldValiation(fieldsToRollback);
}
if (err && err.status === 400 && err.data) {
// content was saved but is invalid.
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false });
}
resetNestedFieldValiation(fieldsToRollback);
return $q.reject(err);
});
}
@@ -735,48 +736,48 @@
clearNotifications($scope.content);
// TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (hasVariants($scope.content)) {
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) {
var dialog = {
parentScope: $scope,
view: "views/content/overlays/save.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "buttons_save",
submit: function (model) {
model.submitButtonState = "busy";
var dialog = {
parentScope: $scope,
view: "views/content/overlays/save.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "buttons_save",
submit: function (model) {
model.submitButtonState = "busy";
clearNotifications($scope.content);
//we need to return this promise so that the dialog can handle the result and wire up the validation response
return performSave({
saveMethod: $scope.saveMethod(),
action: "save",
showNotifications: false,
skipValidation: true
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
//we need to return this promise so that the dialog can handle the result and wire up the validation response
return performSave({
saveMethod: $scope.saveMethod(),
action: "save",
showNotifications: false
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
handleHttpException(err);
});
},
close: function (oldModel) {
overlayService.close();
}
};
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
//model.submitButtonState = "error";
// Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
model.submitButtonState = "success";
} else {
model.submitButtonState = "error";
}
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
handleHttpException(err);
});
},
close: function (oldModel) {
overlayService.close();
}
};
overlayService.open(dialog);
}
else {
showValidationNotification();
}
overlayService.open(dialog);
}
else {
//ensure the flags are set
@@ -784,11 +785,17 @@
$scope.page.saveButtonState = "busy";
return performSave({
saveMethod: $scope.saveMethod(),
action: "save"
action: "save",
skipValidation: true
}).then(function () {
$scope.page.saveButtonState = "success";
}, function (err) {
$scope.page.saveButtonState = "error";
// Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
$scope.page.saveButtonState = "success";
} else {
$scope.page.saveButtonState = "error";
}
handleHttpException(err);
});
}

View File

@@ -15,6 +15,7 @@
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) {
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
var SHOW_VALIDATION_Type_CLASS_NAME = "show-validation-type-";
var SAVING_EVENT_NAME = "formSubmitting";
var SAVED_EVENT_NAME = "formSubmitted";
@@ -44,6 +45,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
this.isShowingValidation = () => $scope.showValidation === true;
this.getValidationMessageType = () => $scope.valMsgType;
this.notify = notify;
this.isValid = function () {
@@ -94,6 +97,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1);
var subView = ctrls.length > 1 ? ctrls[2] : null;
var labels = {};
var valMsgType = 2;// error
var labelKeys = [
"prompt_unsavedChanges",
@@ -109,6 +113,45 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
labels.stayButton = values[3];
});
var lastValidationMessageType = null;
function setValidationMessageType(type) {
removeValidationMessageType();
scope.valMsgType = type;
// overall a copy of message types from notifications.service:
var postfix = "";
switch(type) {
case 0:
//save
break;
case 1:
//info
postfix = "info";
break;
case 2:
//error
postfix = "error";
break;
case 3:
//success
postfix = "success";
break;
case 4:
//warning
postfix = "warning";
break;
}
var cssClass = SHOW_VALIDATION_Type_CLASS_NAME+postfix;
element.addClass(cssClass);
lastValidationMessageType = cssClass;
}
function removeValidationMessageType() {
if(lastValidationMessageType) {
element.removeClass(lastValidationMessageType);
lastValidationMessageType = null;
}
}
// watch the list of validation errors to notify the application of any validation changes
scope.$watch(() => formCtrl.$invalid,
@@ -138,6 +181,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
var parentValMsgType = parentFormMgr ? parentFormMgr.getValidationMessageType() : 2;
setValidationMessageType(parentValMsgType || 2);
notifySubView();
}
@@ -145,8 +190,16 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
//listen for the forms saving event
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) {
var messageType = 2;//error
switch (args.action) {
case "save":
messageType = 4;//warning
break;
}
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
setValidationMessageType(messageType);
notifySubView();
//set the flag so we can check to see if we should display the error.
isSavingNewItem = $routeParams.create;
@@ -156,6 +209,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) {
//remove validation class
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
removeValidationMessageType();
scope.showValidation = false;
notifySubView();
}));

View File

@@ -32,7 +32,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
return true;
}
function showNotificationsForModelsState(ms) {
function showNotificationsForModelsState(ms, messageType) {
messageType = messageType || 2;
for (const [key, value] of Object.entries(ms)) {
var errorMsg = value[0];
@@ -42,12 +43,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, "");
idsToErrors.forEach(x => {
if (x.modelState) {
showNotificationsForModelsState(x.modelState);
showNotificationsForModelsState(x.modelState, messageType);
}
});
}
else if (value[0]) {
notificationsService.error("Validation", value[0]);
//notificationsService.error("Validation", value[0]);
console.log({type:messageType, header:"Validation", message:value[0]})
notificationsService.showNotification({type:messageType, header:"Validation", message:value[0]})
}
}
}
@@ -93,7 +96,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//we will use the default one for content if not specified
var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
if (formHelper.submitForm({ scope: args.scope, action: args.action })) {
var formSubmitOptions = { scope: args.scope, action: args.action };
if(args.skipValidation === true) {
formSubmitOptions.skipValidation = true;
formSubmitOptions.keepServerValidation = true;
}
if (formHelper.submitForm(formSubmitOptions)) {
return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications)
.then(function (data) {
@@ -124,6 +132,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
showNotifications: args.showNotifications,
softRedirect: args.softRedirect,
err: err,
action: args.action,
rebindCallback: function () {
// if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc.
if(err.data) {
@@ -639,9 +648,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//wire up the server validation errs
formHelper.handleServerValidation(args.err.data.ModelState);
var messageType = 2;//error
if (args.action === "save") {
messageType = 4;//warning
}
//add model state errors to notifications
if (args.showNotifications) {
showNotificationsForModelsState(args.err.data.ModelState);
showNotificationsForModelsState(args.err.data.ModelState, messageType);
}
if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) {

View File

@@ -54,6 +54,15 @@
border-color: @errorBorder;
color: @errorText;
}
.alert-warning() {
background-color: @warningBackground;
border-color: @warningBorder;
color: @warningText;
}
.alert-warning {
.alert-warning()
}
.alert-danger h4,
.alert-error h4 {
color: @errorText;
@@ -110,6 +119,14 @@
padding: 6px 16px 6px 12px;
margin-bottom: 6px;
.show-validation-type-warning & {
.alert-warning();
font-weight: bold;
&.alert-error::after {
border-top-color: @warningBackground;
}
}
&::after {
content:'';
position: absolute;

View File

@@ -50,6 +50,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
.show-validation-type-warning & {
background-color: @warningBackground;
color: @warningText;
}
animation-duration: 1.4s;
animation-iteration-count: infinite;
@@ -233,7 +237,10 @@ button.umb-variant-switcher__toggle {
.umb-variant-switcher__item.--error {
.umb-variant-switcher__name {
color: @red;
color: @formErrorText;
.show-validation-type-warning & {
color: @formWarningText;
}
&::after {
content: '!';
position: relative;
@@ -250,6 +257,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
.show-validation-type-warning & {
background-color: @warningBackground;
color: @warningText;
}
animation-duration: 1.4s;
animation-iteration-count: infinite;

View File

@@ -267,6 +267,9 @@
.umb-overlay .text-error {
color: @formErrorText;
}
.umb-overlay .text-warning {
color: @formWarningText;
}
.umb-overlay .text-success {
color: @formSuccessText;

View File

@@ -86,6 +86,16 @@
}
}
}
.show-validation.show-validation-type-warning &.-has-error {
color: @yellow-d2;
&:hover {
color: @yellow-d2 !important;
}
&::before {
background-color: @yellow-d2;
}
}
}
&__action:active,
@@ -122,14 +132,6 @@
line-height: 16px;
display: block;
&.-type-alert {
background-color: @red;
}
&.-type-warning {
background-color: @yellow-d2;
}
&:empty {
height: 12px;
min-width: 12px;
@@ -137,6 +139,11 @@
&.--error-badge {
display: none;
font-weight: 900;
background-color: @red;
.show-validation-type-warning & {
background-color: @yellow-d2;
}
}
}

View File

@@ -26,7 +26,10 @@ a.umb-list-item:focus {
}
.umb-list-item--error {
color: @red;
color: @formErrorText;
}
.umb-list-item--warning {
color: @formWarningText;
}
.umb-list-item:hover .umb-list-checkbox,

View File

@@ -48,6 +48,10 @@
&.--error {
border-color: @formErrorBorder !important;
}
.show-validation-type-warning &.--error {
border-color: @formWarningBorder !important;
}
}
.umb-nested-content__item.ui-sortable-placeholder {
@@ -292,4 +296,4 @@
.umb-textarea, .umb-textstring {
width:100%;
}
}
}

View File

@@ -86,6 +86,13 @@
background-color: @red !important;
border-color: @errorBorder;
}
.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button,
.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:hover,
.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:focus {
color: @white !important;
background-color: @yellow-d2 !important;
border-color: @warningBorder;
}
.show-validation .umb-tab--error .umb-tab-button:before {
content: "\e25d";

View File

@@ -506,10 +506,20 @@ input[type="checkbox"][readonly] {
.formFieldState(@formErrorText, @formErrorText, @formErrorBackground);
}
// ValidationError as a warning
.show-validation.show-validation-type-warning.ng-invalid .control-group.error,
.show-validation.show-validation-type-warning.ng-invalid .umb-editor-header__name-wrapper {
.formFieldState(@formWarningText, @formWarningText, @formWarningBackground);
}
//val-highlight directive styling
.highlight-error {
color: @formErrorText !important;
border-color: @red-l1 !important;
.show-validation-type-warning & {
color: @formWarningText !important;
border-color: @yellow-d2 !important;
}
}
// FORM ACTIONS

View File

@@ -291,8 +291,8 @@
@btnSuccessBackground: @ui-btn-positive;// updated 2019
@btnSuccessBackgroundHighlight: @ui-btn-positive-hover;// updated 2019
@btnWarningBackground: @orange;
@btnWarningBackgroundHighlight: lighten(@orange, 10%);
@btnWarningBackground: @yellow-d2;
@btnWarningBackgroundHighlight: lighten(@yellow-d2, 10%);
@btnDangerBackground: @red;
@btnDangerBackgroundHighlight: @red-l1;
@@ -480,7 +480,7 @@
@formWarningBorder: darken(spin(@warningBackground, -10), 3%);
@formErrorText: @errorBackground;
@formErrorBackground: lighten(@errorBackground, 55%);
@formErrorBackground: @errorBackground;
@formErrorBorder: @red;
@formSuccessText: @successBackground;

View File

@@ -16,7 +16,9 @@
<div class="umb-list-item"
ng-repeat="variant in vm.availableVariants track by variant.compositeId">
<ng-form name="saveVariantSelectorForm">
<div class="umb-variant-selector-entry" ng-class="{'umb-list-item--error': saveVariantSelectorForm.saveVariantSelector.$invalid}">
<div class="umb-variant-selector-entry" ng-class="{'umb-list-item--warning': saveVariantSelectorForm.saveVariantSelector.$invalid}">
<input type="hidden" name="saveInvariant" val-server-field="_content_variant_invariant_null_" ng-model="variant.save"></input>
<umb-checkbox input-id="{{variant.htmlId}}"
name="saveVariantSelector"
@@ -24,9 +26,10 @@
on-change="vm.changeSelection(variant)"
server-validation-field="{{variant.htmlId}}">
<span class="umb-variant-selector-entry__title" ng-if="!(variant.segment && variant.language)">
<span ng-bind="variant.displayName"></span>
<strong ng-if="variant.isMandatory" class="umb-control-required">*</strong>
</span>
<span class="umb-variant-selector-entry__title" ng-if="variant.segment && variant.language">
<span ng-bind="variant.segment"></span>
@@ -36,10 +39,13 @@
<span class="umb-variant-selector-entry__description" ng-if="!saveVariantSelectorForm.saveVariantSelector.$invalid && !(variant.notifications && variant.notifications.length > 0)">
<umb-variant-state variant="variant"></umb-variant-state>
<span ng-if="variant.isMandatory"> - </span>
<span ng-if="variant.isMandatory" ng-class="{'text-error': (variant.publish === false) }"><localize key="general_mandatory"></localize></span>
<span ng-if="variant.isMandatory"><localize key="general_mandatory"></localize></span>
</span>
<span class="umb-variant-selector-entry__description" ng-messages="saveVariantSelectorForm.saveVariantSelector.$error" show-validation-on-submit>
<span class="text-error" ng-message="valServerField">{{saveVariantSelectorForm.saveVariantSelector.errorMsg}}</span>
<span class="text-warning" ng-message="valServerField">{{saveVariantSelectorForm.saveVariantSelector.errorMsg}}</span>
</span>
<span class="umb-variant-selector-entry__description" ng-messages="saveVariantSelectorForm.saveInvariant.$error" show-validation-on-submit>
<span class="text-warning" ng-message="valServerField">{{saveVariantSelectorForm.saveInvariant.errorMsg}}</span>
</span>
</umb-checkbox>

View File

@@ -60,8 +60,14 @@
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & {
> button {
color: @formErrorText;
.show-validation-type-warning & {
color: @formWarningText;
}
span.caret {
border-top-color: @formErrorText;
.show-validation-type-warning & {
border-top-color: @formWarningText;
}
}
}
}
@@ -84,6 +90,9 @@
padding: 2px;
line-height: 10px;
background-color: @formErrorText;
.show-validation-type-warning & {
background-color: @formWarningText;
}
font-weight: 900;
animation-duration: 1.4s;

View File

@@ -42,6 +42,9 @@
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & {
color: @formErrorText;
.show-validation-type-warning & {
color: @formWarningText;
}
}
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & {
> span {
@@ -61,6 +64,9 @@
padding: 2px;
line-height: 10px;
background-color: @formErrorText;
.show-validation-type-warning & {
background-color: @formWarningText;
}
font-weight: 900;
animation-duration: 1.4s;

View File

@@ -23,10 +23,6 @@
> .umb-block-list__block--actions {
opacity: 0;
transition: opacity 120ms;
.--error {
color: @formErrorBorder !important;
}
}
&:hover,
@@ -100,6 +96,12 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo
&:hover {
color: @ui-action-discreet-type-hover;
}
&.--error {
color: @errorBackground;
.show-validation-type-warning & {
color: @warningBackground;
}
}
> .__error-badge {
position: absolute;
top: -2px;
@@ -113,7 +115,10 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo
font-weight: bold;
padding: 2px;
line-height: 8px;
background-color: @red;
background-color: @errorBackground;
.show-validation-type-warning & {
background-color: @warningBackground;
}
display: none;
font-weight: 900;
}