Gets contextual culture server validation working

This commit is contained in:
Shannon
2018-08-02 20:00:35 +10:00
parent d926f1b3a2
commit 1f25847cd7
19 changed files with 254 additions and 142 deletions

View File

@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Umbraco.Core.Composing;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.Validators
{
@@ -11,6 +13,7 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
internal sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator
{
private readonly ILocalizedTextService _textService;
private string _regex;
/// <inheritdoc cref="IManifestValueValidator.ValidationName"/>
@@ -22,8 +25,8 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// <remarks>Use this constructor when the validator is used as an <see cref="IValueFormatValidator"/>,
/// and the regular expression is supplied at validation time. This constructor is also used when
/// the validator is used as an <see cref="IManifestValueValidator"/> and the regular expression
/// is supplied via the <see cref="Configure"/> method.</remarks>
public RegexValidator()
/// is supplied via the <see cref="Configuration"/> method.</remarks>
public RegexValidator() : this(Current.Services.TextService, null)
{ }
/// <summary>
@@ -31,10 +34,9 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
/// <remarks>Use this constructor when the validator is used as an <see cref="IValueValidator"/>,
/// and the regular expression must be supplied when the validator is created.</remarks>
public RegexValidator(string regex)
public RegexValidator(ILocalizedTextService textService, string regex)
{
if (string.IsNullOrWhiteSpace(regex))
throw new ArgumentNullOrEmptyException(nameof(regex));
_textService = textService;
_regex = regex;
}
@@ -66,7 +68,7 @@ namespace Umbraco.Core.PropertyEditors.Validators
{
if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullOrEmptyException(nameof(format));
if (value == null || !new Regex(format).IsMatch(value.ToString()))
yield return new ValidationResult("Value is invalid, it does not match the correct pattern", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidPattern"), new[] { "value" });
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.Validators
{
@@ -8,6 +9,17 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
internal sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator
{
private readonly ILocalizedTextService _textService;
public RequiredValidator()
{
}
public RequiredValidator(ILocalizedTextService textService)
{
_textService = textService;
}
/// <inheritdoc cref="IManifestValueValidator.ValidationName"/>
public string ValidationName => "Required";
@@ -22,20 +34,20 @@ namespace Umbraco.Core.PropertyEditors.Validators
{
if (value == null)
{
yield return new ValidationResult("Value cannot be null", new[] {"value"});
yield return new ValidationResult(_textService.Localize("validation", "invalidNull"), new[] {"value"});
yield break;
}
if (valueType.InvariantEquals(ValueTypes.Json))
{
if (value.ToString().DetectIsEmptyJson())
yield return new ValidationResult("Value cannot be empty", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" });
yield break;
}
if (value.ToString().IsNullOrWhiteSpace())
{
yield return new ValidationResult("Value cannot be empty", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" });
}
}
}

View File

@@ -0,0 +1,38 @@
(function () {
'use strict';
function tabbedContentDirective() {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/content/tabbed-content.html',
controller: function ($scope) {
//expose the property/methods for other directives to use
this.content = $scope.content;
},
link: function(scope) {
function onInit() {
angular.forEach(scope.content.tabs, function (group) {
group.open = true;
});
}
onInit();
},
scope: {
content: "="
}
};
return directive;
}
angular.module('umbraco.directives').directive('tabbedContent', tabbedContentDirective);
})();

View File

@@ -12,10 +12,7 @@
function valPropertyMsg(serverValidationManager) {
return {
scope: {
property: "="
},
require: ['^^form', '^^valFormManager'],
require: ['^^form', '^^valFormManager', '^^umbProperty', '^^tabbedContent'],
replace: true,
restrict: "E",
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
@@ -24,25 +21,31 @@ function valPropertyMsg(serverValidationManager) {
//the property form controller api
var formCtrl = ctrl[0];
//the valFormManager controller api
var valFormManager = ctrl[1];
//the property controller api
var umbPropCtrl = ctrl[2];
//the tabbed content controller api
var tabbedContent = ctrl[3];
var currentProperty = umbPropCtrl.property;
var currentCulture = tabbedContent.content.language.culture;
var watcher = null;
// Gets the error message to display
function getErrorMsg() {
//this can be null if no property was assigned
if (scope.property) {
if (currentProperty) {
//first try to get the error msg from the server collection
var err = serverValidationManager.getPropertyError(scope.property.alias, "");
var err = serverValidationManager.getPropertyError(currentProperty.alias, null, "");
//if there's an error message use it
if (err && err.errorMsg) {
return err.errorMsg;
}
else {
//TODO: localize
return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors";
return currentProperty.propertyErrorMessage ? currentProperty.propertyErrorMessage : "Property has errors";
}
}
@@ -59,8 +62,10 @@ function valPropertyMsg(serverValidationManager) {
function startWatch() {
//if there's not already a watch
if (!watcher) {
watcher = scope.$watch("property.value", function (newValue, oldValue) {
watcher = scope.$watch(function () {
return currentProperty.value;
}, function (newValue, oldValue) {
if (!newValue || angular.equals(newValue, oldValue)) {
return;
}
@@ -133,7 +138,7 @@ function valPropertyMsg(serverValidationManager) {
});
//listen for the forms saving event
unsubscribe.push(scope.$on("formSubmitting", function(ev, args) {
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = getErrorMsg();
@@ -145,7 +150,7 @@ function valPropertyMsg(serverValidationManager) {
}));
//listen for the forms saved event
unsubscribe.push(scope.$on("formSubmitted", function(ev, args) {
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
showValidation = false;
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
@@ -160,8 +165,8 @@ function valPropertyMsg(serverValidationManager) {
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
if (scope.property) { //this can be null if no property was assigned
serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) {
if (currentProperty) { //this can be null if no property was assigned
serverValidationManager.subscribe(currentProperty.alias, currentCulture, "", function (isValid, propertyErrors, allErrors) {
hasError = !isValid;
if (hasError) {
//set the error message to the server message
@@ -183,7 +188,7 @@ function valPropertyMsg(serverValidationManager) {
// but they are a different callback instance than the above.
element.bind('$destroy', function () {
stopWatch();
serverValidationManager.unsubscribe(scope.property.alias, "");
serverValidationManager.unsubscribe(currentProperty.alias, currentCulture, "");
});
}

View File

@@ -7,13 +7,14 @@
**/
function valServer(serverValidationManager) {
return {
require: ['ngModel', '?^umbProperty'],
require: ['ngModel', '?^^umbProperty', '?^^tabbedContent'],
restrict: "A",
link: function (scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null;
if (!umbPropCtrl) {
var tabbedContent = ctrls.length > 2 ? ctrls[2] : null;
if (!umbPropCtrl || !tabbedContent) {
//we cannot proceed, this validator will be disabled
return;
}
@@ -54,6 +55,7 @@ function valServer(serverValidationManager) {
}
var currentProperty = umbPropCtrl.property;
var currentCulture = tabbedContent.content.language.culture;
//default to 'value' if nothing is set
var fieldName = "value";
@@ -66,7 +68,7 @@ function valServer(serverValidationManager) {
}
//subscribe to the server validation changes
serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) {
serverValidationManager.subscribe(currentProperty.alias, currentCulture, fieldName, function (isValid, propertyErrors, allErrors) {
if (!isValid) {
modelCtrl.$setValidity('valServer', false);
//assign an error msg property to the current validator
@@ -86,9 +88,9 @@ function valServer(serverValidationManager) {
// but they are a different callback instance than the above.
element.bind('$destroy', function () {
stopWatch();
serverValidationManager.unsubscribe(currentProperty.alias, fieldName);
serverValidationManager.unsubscribe(currentProperty.alias, currentCulture, fieldName);
});
}
};
}
angular.module('umbraco.directives.validation').directive("valServer", valServer);
angular.module('umbraco.directives.validation').directive("valServer", valServer);

View File

@@ -33,7 +33,7 @@ function valServerField(serverValidationManager) {
}));
//subscribe to the server validation changes
serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) {
serverValidationManager.subscribe(null, null, fieldName, function (isValid, fieldErrors, allErrors) {
if (!isValid) {
ngModel.$setValidity('valServerField', false);
//assign an error msg property to the current validator
@@ -50,7 +50,7 @@ function valServerField(serverValidationManager) {
// NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
// but they are a different callback instance than the above.
element.bind('$destroy', function () {
serverValidationManager.unsubscribe(null, fieldName);
serverValidationManager.unsubscribe(null, null, fieldName);
});
}
});

View File

@@ -145,32 +145,33 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
// that each property is a User Developer property editor.
// The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations
// system.
// So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties,
// which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect
// this, then we know it's a Property.
// So, to do this there's some special ModelState syntax we need to know about.
// For Content Properties, which are user defined, we know that they will exist with a prefixed
// ModelState of "_Properties.", so if we detect this, then we know it's for a content Property.
//the alias in model state can be in dot notation which indicates
// * the first part is the content property alias
// * the second part is the field to which the valiation msg is associated with
//There will always be at least 2 parts for properties since all model errors for properties are prefixed with "Properties"
//If it is not prefixed with "Properties" that means the error is for a field of the object directly.
//There will always be at least 3 parts for content properties since all model errors for properties are prefixed with "_Properties"
//If it is not prefixed with "_Properties" that means the error is for a field of the object directly.
var parts = e.split(".");
//Check if this is for content properties - specific to content/media/member editors because those are special
// user defined properties with custom controls.
if (parts.length > 1 && parts[0] === "_Properties") {
if (parts.length > 2 && parts[0] === "_Properties") {
var propertyAlias = parts[1];
var culture = parts[2];
//if it contains 2 '.' then we will wire it up to a property's field
if (parts.length > 2) {
//if it contains 3 '.' then we will wire it up to a property's html field
if (parts.length > 3) {
//add an error with a reference to the field for which the validation belongs too
serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]);
serverValidationManager.addPropertyError(propertyAlias, culture, parts[3], modelState[e][0]);
}
else {
//add a generic error for the property, no reference to a specific field
serverValidationManager.addPropertyError(propertyAlias, "", modelState[e][0]);
//add a generic error for the property, no reference to a specific html field
serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]);
}
}

View File

@@ -28,11 +28,11 @@ function serverValidationManager($timeout) {
//find errors for this field name
return _.filter(self.items, function (item) {
return (item.propertyAlias === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
});
}
function getPropertyErrors(self, propertyAlias, fieldName) {
function getPropertyErrors(self, propertyAlias, culture, fieldName) {
if (!angular.isString(propertyAlias)) {
throw "propertyAlias must be a string";
}
@@ -42,7 +42,7 @@ function serverValidationManager($timeout) {
//find all errors for this property
return _.filter(self.items, function (item) {
return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
}
@@ -104,7 +104,7 @@ function serverValidationManager($timeout) {
* field alias to listen for.
* If propertyAlias is null, then this subscription is for a field property (not a user defined property).
*/
subscribe: function (propertyAlias, fieldName, callback) {
subscribe: function (propertyAlias, culture, fieldName, callback) {
if (!callback) {
return;
}
@@ -115,41 +115,46 @@ function serverValidationManager($timeout) {
return item.propertyAlias === null && item.fieldName === fieldName;
});
if (!exists1) {
callbacks.push({ propertyAlias: null, fieldName: fieldName, callback: callback });
callbacks.push({ propertyAlias: null, culture: null, fieldName: fieldName, callback: callback });
}
}
else if (propertyAlias !== undefined) {
if (!culture) {
culture = null; // if empty or null, always make null
}
//don't add it if it already exists
var exists2 = _.find(callbacks, function (item) {
return item.propertyAlias === propertyAlias && item.fieldName === fieldName;
return item.propertyAlias === propertyAlias && item.culture === culture && item.fieldName === fieldName;
});
if (!exists2) {
callbacks.push({ propertyAlias: propertyAlias, fieldName: fieldName, callback: callback });
callbacks.push({ propertyAlias: propertyAlias, culture: culture, fieldName: fieldName, callback: callback });
}
}
},
unsubscribe: function (propertyAlias, fieldName) {
unsubscribe: function (propertyAlias, culture, fieldName) {
if (propertyAlias === null) {
//remove all callbacks for the content field
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === null && item.fieldName === fieldName;
return item.propertyAlias === null && item.culture === null && item.fieldName === fieldName;
});
}
else if (propertyAlias !== undefined) {
if (!culture) {
culture = null; // if empty or null, always make null
}
//remove all callbacks for the content property
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === propertyAlias &&
return item.propertyAlias === propertyAlias && item.culture === culture &&
(item.fieldName === fieldName ||
((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
});
}
},
@@ -164,10 +169,15 @@ function serverValidationManager($timeout) {
* This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an
* explicit field name set.
*/
getPropertyCallbacks: function (propertyAlias, fieldName) {
getPropertyCallbacks: function (propertyAlias, culture, fieldName) {
if (!culture) {
culture = null; // if empty or null, always make null
}
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field and for only the property
return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === "")));
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === "")));
});
return found;
},
@@ -184,7 +194,7 @@ function serverValidationManager($timeout) {
getFieldCallbacks: function (fieldName) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field
return (item.propertyAlias === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
});
return found;
},
@@ -207,6 +217,7 @@ function serverValidationManager($timeout) {
if (!this.hasFieldError(fieldName)) {
this.items.push({
propertyAlias: null,
culture: null,
fieldName: fieldName,
errorMsg: errorMsg
});
@@ -231,24 +242,29 @@ function serverValidationManager($timeout) {
* @description
* Adds an error message for the content property
*/
addPropertyError: function (propertyAlias, fieldName, errorMsg) {
addPropertyError: function (propertyAlias, culture, fieldName, errorMsg) {
if (!propertyAlias) {
return;
}
if (!culture) {
culture = null; // if empty or null, always make null
}
//only add the item if it doesn't exist
if (!this.hasPropertyError(propertyAlias, fieldName)) {
if (!this.hasPropertyError(propertyAlias, culture, fieldName)) {
this.items.push({
propertyAlias: propertyAlias,
culture: culture,
fieldName: fieldName,
errorMsg: errorMsg
});
}
//find all errors for this item
var errorsForCallback = getPropertyErrors(this, propertyAlias, fieldName);
var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, fieldName);
//we should now call all of the call backs registered for this error
var cbs = this.getPropertyCallbacks(propertyAlias, fieldName);
var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName);
//call each callback for this error
for (var cb in cbs) {
executeCallback(this, errorsForCallback, cbs[cb].callback);
@@ -264,14 +280,14 @@ function serverValidationManager($timeout) {
* @description
* Removes an error message for the content property
*/
removePropertyError: function (propertyAlias, fieldName) {
removePropertyError: function (propertyAlias, culture, fieldName) {
if (!propertyAlias) {
return;
}
//remove the item
this.items = _.reject(this.items, function (item) {
return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
},
@@ -316,10 +332,10 @@ function serverValidationManager($timeout) {
* @description
* Gets the error message for the content property
*/
getPropertyError: function (propertyAlias, fieldName) {
getPropertyError: function (propertyAlias, culture, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
return err;
},
@@ -336,7 +352,7 @@ function serverValidationManager($timeout) {
getFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
});
return err;
},
@@ -348,12 +364,12 @@ function serverValidationManager($timeout) {
* @function
*
* @description
* Checks if the content property + field name combo has an error
* Checks if the content property + culture + field name combo has an error
*/
hasPropertyError: function (propertyAlias, fieldName) {
hasPropertyError: function (propertyAlias, culture, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
return err ? true : false;
},
@@ -370,7 +386,7 @@ function serverValidationManager($timeout) {
hasFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
});
return err ? true : false;
},
@@ -380,4 +396,4 @@ function serverValidationManager($timeout) {
};
}
angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager);
angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager);

View File

@@ -0,0 +1,17 @@
<div>
<div class="umb-expansion-panel" ng-repeat="group in content.tabs | filter: { hide : '!' + true } track by group.label">
<div class="umb-expansion-panel__header" ng-click="group.open = !group.open">
<div>{{ group.label }}</div>
<ins class="umb-expansion-panel__expand icon-navigation-right" ng-class="{'icon-navigation-down': !group.open, 'icon-navigation-up': group.open}">&nbsp;</ins>
</div>
<div class="umb-expansion-panel__content" ng-show="group.open">
<umb-property data-element="property-{{group.alias}}" ng-repeat="property in group.properties track by property.alias" property="property">
<umb-property-editor model="property"></umb-property-editor>
</umb-property>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
<ng-form name="propertyForm">
<div class="control-group umb-control-group" ng-class="{hidelabel:property.hideLabel}">
<val-property-msg property="property"></val-property-msg>
<val-property-msg></val-property-msg>
<div class="umb-el-wrap">
@@ -19,4 +19,4 @@
</div>
</div>
</ng-form>
</div>
</div>

View File

@@ -6,14 +6,9 @@
var vm = this;
vm.loading = true;
//TODO: Figure out what we need to do to maintain validation states since this will re-init the editor
function onInit() {
vm.content = $scope.model.viewModel;
angular.forEach(vm.content.tabs, function (group) {
group.open = true;
});
vm.loading = false;
}

View File

@@ -1,18 +1,5 @@
<div class="form-horizontal" ng-controller="Umbraco.Editors.Content.Apps.ContentController as vm">
<div class="umb-expansion-panel" ng-if="!vm.loading" ng-repeat="group in vm.content.tabs | filter: { hide : '!' + true } track by group.label">
<div class="umb-expansion-panel__header" ng-click="group.open = !group.open">
<div>{{ group.label }}</div>
<ins class="umb-expansion-panel__expand icon-navigation-right" ng-class="{'icon-navigation-down': !group.open, 'icon-navigation-up': group.open}">&nbsp;</ins>
</div>
<div class="umb-expansion-panel__content" ng-show="group.open">
<umb-property data-element="property-{{group.alias}}" ng-repeat="property in group.properties track by property.alias" property="property">
<umb-property-editor model="property"></umb-property-editor>
</umb-property>
</div>
</div>
<tabbed-content ng-if="!vm.loading" content="vm.content"></tabbed-content>
</div>

View File

@@ -60,12 +60,12 @@
it('can retrieve property validation errors for a sub field', function () {
//arrange
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Another value 2");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
//act
var err1 = serverValidationManager.getPropertyError("myProperty", "value1");
var err2 = serverValidationManager.getPropertyError("myProperty", "value2");
var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1");
var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2");
//assert
expect(err1).not.toBeUndefined();
@@ -82,8 +82,8 @@
it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () {
//arrange
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Another value 2");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
//act
var err = serverValidationManager.getPropertyError("myProperty");
@@ -98,10 +98,10 @@
it('will return null for a non-existing property error', function () {
//arrage
serverValidationManager.addPropertyError("myProperty", "value", "Required");
serverValidationManager.addPropertyError("myProperty", null, "value", "Required");
//act
var err = serverValidationManager.getPropertyError("DoesntExist", "value");
var err = serverValidationManager.getPropertyError("DoesntExist", null, "value");
//assert
expect(err).toBeUndefined();
@@ -111,15 +111,15 @@
it('detects if a property error exists', function () {
//arrange
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Another value 2");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
//act
var err1 = serverValidationManager.hasPropertyError("myProperty");
var err2 = serverValidationManager.hasPropertyError("myProperty", "value1");
var err3 = serverValidationManager.hasPropertyError("myProperty", "value2");
var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1");
var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2");
var err4 = serverValidationManager.hasPropertyError("notFound");
var err5 = serverValidationManager.hasPropertyError("myProperty", "notFound");
var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound");
//assert
expect(err1).toBe(true);
@@ -133,30 +133,30 @@
it('can remove a property error with a sub field specified', function () {
//arrage
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Another value 2");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
//act
serverValidationManager.removePropertyError("myProperty", "value1");
serverValidationManager.removePropertyError("myProperty", null, "value1");
//assert
expect(serverValidationManager.hasPropertyError("myProperty", "value1")).toBe(false);
expect(serverValidationManager.hasPropertyError("myProperty", "value2")).toBe(true);
expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false);
expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(true);
});
it('can remove a property error and all sub field errors by specifying only the property', function () {
//arrage
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Another value 2");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
//act
serverValidationManager.removePropertyError("myProperty");
//assert
expect(serverValidationManager.hasPropertyError("myProperty", "value1")).toBe(false);
expect(serverValidationManager.hasPropertyError("myProperty", "value2")).toBe(false);
expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false);
expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(false);
});
@@ -168,7 +168,7 @@
var args;
//arrange
serverValidationManager.subscribe(null, "Name", function (isValid, propertyErrors, allErrors) {
serverValidationManager.subscribe(null, null, "Name", function (isValid, propertyErrors, allErrors) {
args = {
isValid: isValid,
propertyErrors: propertyErrors,
@@ -178,7 +178,7 @@
//act
serverValidationManager.addFieldError("Name", "Required");
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
//assert
expect(args).not.toBeUndefined();
@@ -195,8 +195,8 @@
};
var cb2 = function () {
};
serverValidationManager.subscribe(null, "Name", cb1);
serverValidationManager.subscribe(null, "Title", cb2);
serverValidationManager.subscribe(null, null, "Name", cb1);
serverValidationManager.subscribe(null, null, "Title", cb2);
//act
serverValidationManager.addFieldError("Name", "Required");
@@ -224,7 +224,7 @@
var numCalled = 0;
//arrange
serverValidationManager.subscribe("myProperty", "value1", function (isValid, propertyErrors, allErrors) {
serverValidationManager.subscribe("myProperty", null, "value1", function (isValid, propertyErrors, allErrors) {
args1 = {
isValid: isValid,
propertyErrors: propertyErrors,
@@ -232,7 +232,7 @@
};
});
serverValidationManager.subscribe("myProperty", "", function (isValid, propertyErrors, allErrors) {
serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) {
numCalled++;
args2 = {
isValid: isValid,
@@ -242,9 +242,9 @@
});
//act
serverValidationManager.addPropertyError("myProperty", "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", "value2", "Some value 2");
serverValidationManager.addPropertyError("myProperty", "", "Some value 3");
serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2");
serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3");
//assert
expect(args1).not.toBeUndefined();
@@ -272,4 +272,4 @@
});
});
});

View File

@@ -2063,6 +2063,9 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="invalidDate">Invalid date</key>
<key alias="invalidNumber">Not a number</key>
<key alias="invalidEmail">Invalid email</key>
<key alias="invalidNull">Value cannot be null</key>
<key alias="invalidEmpty">Value cannot be empty</key>
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:

View File

@@ -148,7 +148,7 @@ namespace Umbraco.Web.Editors.Filters
// validate
var valueEditor = editor.GetValueEditor(p.DataType.Configuration);
foreach (var r in valueEditor.Validate(postedValue, p.IsRequired, p.ValidationRegExp))
modelState.AddPropertyError(r, p.Alias);
modelState.AddPropertyError(r, p.Alias, p.Culture);
}
return modelState.IsValid;

View File

@@ -57,24 +57,40 @@ namespace Umbraco.Web
/// <param name="modelState"></param>
/// <param name="result"></param>
/// <param name="propertyAlias"></param>
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, ValidationResult result, string propertyAlias)
/// <param name="culture">The culture for the property, if the property is invariant than this is empty</param>
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, string propertyAlias, string culture = "")
{
modelState.AddValidationError(result, "_Properties", propertyAlias);
if (culture == null)
culture = "";
modelState.AddValidationError(result, "_Properties", propertyAlias, culture);
}
internal static void AddValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, ValidationResult result, string prefix, string owner)
/// <summary>
/// Adds the error to model state correctly for a property so we can use it on the client side.
/// </summary>
/// <param name="modelState"></param>
/// <param name="result"></param>
/// <param name="parts">
/// Each model state validation error has a name and in most cases this name is made up of parts which are delimited by a '.'
/// </param>
internal static void AddValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, params string[] parts)
{
// if there are assigned member names, we combine the member name with the owner name
// so that we can try to match it up to a real field. otherwise, we assume that the
// validation message is for the overall owner.
// Owner = the component being validated, like a content property but could be just an html field on another editor
var withNames = false;
var delimitedParts = string.Join(".", parts);
foreach (var memberName in result.MemberNames)
{
modelState.AddModelError($"{prefix}.{owner}.{memberName}", result.ErrorMessage);
modelState.AddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage);
withNames = true;
}
if (!withNames)
modelState.AddModelError($"{prefix}.{owner}", result.ErrorMessage);
modelState.AddModelError($"{delimitedParts}", result.ErrorMessage);
}
public static IDictionary<string, object> ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)

View File

@@ -15,5 +15,13 @@ namespace Umbraco.Web.Models.ContentEditing
public string Description { get; set; }
public bool IsRequired { get; set; }
public string ValidationRegExp { get; set; }
/// <summary>
/// The culture for the property which is only relevant for variant properties
/// </summary>
/// <remarks>
/// Since content currently is the only thing that can be variant this will only be relevant to content
/// </remarks>
public string Culture { get; set; }
}
}

View File

@@ -70,7 +70,7 @@ namespace Umbraco.Web.Models.Mapping
//a culture needs to be in the context for a property type that can vary
if (culture == null && property.PropertyType.VariesByCulture())
throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}");
throw new InvalidOperationException($"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}");
//set the culture to null if it's an invariant property type
culture = !property.PropertyType.VariesByCulture() ? null : culture;

View File

@@ -1,5 +1,6 @@
using System;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
@@ -17,15 +18,24 @@ namespace Umbraco.Web.Models.Mapping
: base(dataTypeService, logger, propertyEditors)
{ }
public override ContentPropertyDto Convert(Property originalProperty, ContentPropertyDto dest, ResolutionContext context)
public override ContentPropertyDto Convert(Property property, ContentPropertyDto dest, ResolutionContext context)
{
var propertyDto = base.Convert(originalProperty, dest, context);
var propertyDto = base.Convert(property, dest, context);
propertyDto.IsRequired = originalProperty.PropertyType.Mandatory;
propertyDto.ValidationRegExp = originalProperty.PropertyType.ValidationRegExp;
propertyDto.Description = originalProperty.PropertyType.Description;
propertyDto.Label = originalProperty.PropertyType.Name;
propertyDto.DataType = DataTypeService.GetDataType(originalProperty.PropertyType.DataTypeId);
propertyDto.IsRequired = property.PropertyType.Mandatory;
propertyDto.ValidationRegExp = property.PropertyType.ValidationRegExp;
propertyDto.Description = property.PropertyType.Description;
propertyDto.Label = property.PropertyType.Name;
propertyDto.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId);
//Get the culture from the context which will be set during the mapping operation for each property
var culture = context.GetCulture();
//a culture needs to be in the context for a property type that can vary
if (culture == null && property.PropertyType.VariesByCulture())
throw new InvalidOperationException($"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}");
propertyDto.Culture = culture;
return propertyDto;
}