diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index f3f8f47b57..56462fcc40 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -52,7 +52,7 @@
-
+
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 3c8fed78f5..614a816f3f 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -53,9 +53,9 @@
-
-
-
+
+
+
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
index 2295745637..d86c682bd5 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
@@ -389,7 +389,7 @@ namespace Umbraco.Core.Migrations.Install
private DatabaseSchemaResult ValidateSchema(IScope scope)
{
- if (_databaseFactory.Configured == false)
+ if (_databaseFactory.Initialized == false)
return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax);
if (_databaseSchemaValidationResult != null)
@@ -513,7 +513,7 @@ namespace Umbraco.Core.Migrations.Install
private Attempt CheckReadyForInstall()
{
- if (_databaseFactory.Configured == false)
+ if (_databaseFactory.CanConnect == false)
{
return Attempt.Fail(new Result
{
diff --git a/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs
index 0236fc4bd5..c2d65b824f 100644
--- a/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs
+++ b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs
@@ -10,22 +10,35 @@ namespace Umbraco.Core.Persistence
///
/// Creates a new database.
///
- /// The new database must be disposed after being used.
+ ///
+ /// The new database must be disposed after being used.
+ /// Creating a database causes the factory to initialize if it is not already initialized.
+ ///
IUmbracoDatabase CreateDatabase();
///
- /// Gets a value indicating whether the database factory is configured.
+ /// Gets a value indicating whether the database factory is configured, i.e. whether
+ /// its connection string and provider name have been set. The factory may however not
+ /// be initialized (see ).
///
bool Configured { get; }
+ ///
+ /// Gets a value indicating whether the database factory is initialized, i.e. whether
+ /// its internal state is ready and it has been possible to connect to the database.
+ ///
+ bool Initialized { get; }
+
///
/// Gets the connection string.
///
- /// Throws if the factory is not configured.
+ /// May return null if the database factory is not configured.
string ConnectionString { get; }
///
- /// Gets a value indicating whether the database can connect.
+ /// Gets a value indicating whether the database factory is configured (see ),
+ /// and it is possible to connect to the database. The factory may however not be initialized (see
+ /// ).
///
bool CanConnect { get; }
@@ -37,6 +50,9 @@ namespace Umbraco.Core.Persistence
///
/// Gets the Sql context.
///
+ ///
+ /// Getting the Sql context causes the factory to initialize if it is not already initialized.
+ ///
ISqlContext SqlContext { get; }
///
diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
index c9d85feb25..0574e37c4c 100644
--- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
+++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
@@ -232,6 +232,14 @@ namespace Umbraco.Core.Persistence
using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName })
using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax))
{
+ //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared
+ //to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses
+ //the names instead of their ordering.
+ foreach(var col in bulkReader.ColumnMappings)
+ {
+ copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn);
+ }
+
copy.WriteToServer(bulkReader);
return bulkReader.RecordsAffected;
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 645ab9f924..ccafb9f771 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
@@ -140,10 +140,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
while (templateDtoIx < templateDtos.Count && templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id)
{
var allowedDto = templateDtos[templateDtoIx];
+ templateDtoIx++;
if (!templates.TryGetValue(allowedDto.TemplateNodeId, out var template)) continue;
allowedTemplates.Add(template);
- templateDtoIx++;
-
+
if (allowedDto.IsDefault)
defaultTemplateId = template.Id;
}
diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs
index dc86ff060c..13422f43b1 100644
--- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs
+++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs
@@ -28,7 +28,8 @@ namespace Umbraco.Core.Persistence
{
private readonly Lazy _mappers;
private readonly ILogger _logger;
- private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
+
+ private object _lock = new object();
private DatabaseFactory _npocoDatabaseFactory;
private IPocoDataFactory _pocoDataFactory;
@@ -36,12 +37,13 @@ namespace Umbraco.Core.Persistence
private string _providerName;
private DbProviderFactory _dbProviderFactory;
private DatabaseType _databaseType;
- private bool _serverVersionDetected;
private ISqlSyntaxProvider _sqlSyntax;
private RetryPolicy _connectionRetryPolicy;
private RetryPolicy _commandRetryPolicy;
private NPoco.MapperCollection _pocoMappers;
+ private SqlContext _sqlContext;
private bool _upgrading;
+ private bool _initialized;
#region Constructors
@@ -106,36 +108,30 @@ namespace Umbraco.Core.Persistence
#endregion
///
- public bool Configured { get; private set; }
-
- ///
- public string ConnectionString
+ public bool Configured
{
get
{
- EnsureConfigured();
- return _connectionString;
+ lock (_lock)
+ {
+ return !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace();
+ }
}
}
///
- public bool CanConnect
- {
- get
- {
- if (!Configured || !DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName)) return false;
+ public bool Initialized => Volatile.Read(ref _initialized);
- if (_serverVersionDetected) return true;
+ ///
+ public string ConnectionString => _connectionString;
- if (_databaseType.IsSqlServer())
- DetectSqlServerVersion();
- _serverVersionDetected = true;
+ ///
+ public bool CanConnect =>
+ // actually tries to connect to the database (regardless of configured/initialized)
+ !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() &&
+ DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName);
- return true;
- }
- }
-
- private void DetectSqlServerVersion()
+ private void UpdateSqlServerDatabaseType()
{
// replace NPoco database type by a more efficient one
@@ -171,7 +167,15 @@ namespace Umbraco.Core.Persistence
}
///
- public ISqlContext SqlContext { get; private set; }
+ public ISqlContext SqlContext
+ {
+ get
+ {
+ // must be initialized to have a context
+ EnsureInitialized();
+ return _sqlContext;
+ }
+ }
///
public void ConfigureForUpgrade()
@@ -182,63 +186,79 @@ namespace Umbraco.Core.Persistence
///
public void Configure(string connectionString, string providerName)
{
- try
+ if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString));
+ if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName));
+
+ lock (_lock)
{
- _lock.EnterWriteLock();
-
- _logger.Debug("Configuring.");
-
- if (Configured) throw new InvalidOperationException("Already configured.");
-
- if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString));
- if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName));
+ if (Volatile.Read(ref _initialized))
+ throw new InvalidOperationException("Already initialized.");
_connectionString = connectionString;
_providerName = providerName;
-
- _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString);
- _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString);
-
- _dbProviderFactory = DbProviderFactories.GetFactory(_providerName);
- if (_dbProviderFactory == null)
- throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\".");
- _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName);
- if (_databaseType == null)
- throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\".");
-
- _sqlSyntax = GetSqlSyntaxProvider(_providerName);
- if (_sqlSyntax == null)
- throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\".");
-
- // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database
- // so that everything NPoco is properly cached for the lifetime of the application
- _pocoMappers = new NPoco.MapperCollection { new PocoMapper() };
- var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver);
- _pocoDataFactory = factory;
- var config = new FluentConfig(xmappers => factory);
-
- // create the database factory
- _npocoDatabaseFactory = DatabaseFactory.Config(x => x
- .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances
- .WithFluentConfig(config)); // with proper configuration
-
- if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance.");
-
- SqlContext = new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers);
-
- _logger.Debug("Configured.");
- Configured = true;
- }
- finally
- {
- if (_lock.IsWriteLockHeld)
- _lock.ExitWriteLock();
}
+
+ // rest to be lazy-initialized
+ }
+
+ private void EnsureInitialized()
+ {
+ LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize);
+ }
+
+ private SqlContext Initialize()
+ {
+ _logger.Debug("Initializing.");
+
+ if (_connectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string.");
+ if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name.");
+
+ // cannot initialize without being able to talk to the database
+ if (!DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName))
+ throw new Exception("Cannot connect to the database.");
+
+ _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString);
+ _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString);
+
+ _dbProviderFactory = DbProviderFactories.GetFactory(_providerName);
+ if (_dbProviderFactory == null)
+ throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\".");
+ _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName);
+ if (_databaseType == null)
+ throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\".");
+
+ _sqlSyntax = GetSqlSyntaxProvider(_providerName);
+ if (_sqlSyntax == null)
+ throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\".");
+
+ if (_databaseType.IsSqlServer())
+ UpdateSqlServerDatabaseType();
+
+ // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database
+ // so that everything NPoco is properly cached for the lifetime of the application
+ _pocoMappers = new NPoco.MapperCollection { new PocoMapper() };
+ var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver);
+ _pocoDataFactory = factory;
+ var config = new FluentConfig(xmappers => factory);
+
+ // create the database factory
+ _npocoDatabaseFactory = DatabaseFactory.Config(x => x
+ .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances
+ .WithFluentConfig(config)); // with proper configuration
+
+ if (_npocoDatabaseFactory == null)
+ throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance.");
+
+ _logger.Debug("Initialized.");
+
+ return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers);
}
///
public IUmbracoDatabase CreateDatabase()
{
+ // must be initialized to create a database
+ EnsureInitialized();
return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase();
}
@@ -260,22 +280,6 @@ namespace Umbraco.Core.Persistence
}
}
- // ensures that the database is configured, else throws
- private void EnsureConfigured()
- {
- _lock.EnterReadLock();
- try
- {
- if (Configured == false)
- throw new InvalidOperationException("Not configured.");
- }
- finally
- {
- if (_lock.IsReadLockHeld)
- _lock.ExitReadLock();
- }
- }
-
// method used by NPoco's UmbracoDatabaseFactory to actually create the database instance
private UmbracoDatabase CreateDatabaseInstance()
{
@@ -292,7 +296,7 @@ namespace Umbraco.Core.Persistence
//var db = _umbracoDatabaseAccessor.UmbracoDatabase;
//_umbracoDatabaseAccessor.UmbracoDatabase = null;
//db?.Dispose();
- Configured = false;
+ Volatile.Write(ref _initialized, false);
}
// during tests, the thread static var can leak between tests
diff --git a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs
index c276dc35ca..fb451b1d5c 100644
--- a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs
+++ b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs
@@ -43,16 +43,6 @@ namespace Umbraco.Tests.Persistence
_databaseFactory = null;
}
- [Test]
- public void GetDatabaseType()
- {
- using (var database = _databaseFactory.CreateDatabase())
- {
- var databaseType = database.DatabaseType;
- Assert.AreEqual(DatabaseType.SQLCe, databaseType);
- }
- }
-
[Test]
public void CreateDatabase() // FIXME: move to DatabaseBuilderTest!
{
@@ -79,6 +69,13 @@ namespace Umbraco.Tests.Persistence
// re-create the database factory and database context with proper connection string
_databaseFactory = new UmbracoDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _logger, new Lazy(() => Mock.Of()));
+ // test get database type (requires an actual database)
+ using (var database = _databaseFactory.CreateDatabase())
+ {
+ var databaseType = database.DatabaseType;
+ Assert.AreEqual(DatabaseType.SQLCe, databaseType);
+ }
+
// create application context
//var appCtx = new ApplicationContext(
// _databaseFactory,
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
index 4999f7007a..75fa0469bb 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
@@ -1,10 +1,12 @@
(function () {
'use strict';
- function EditorContentHeader() {
+ function EditorContentHeader(serverValidationManager) {
function link(scope, el, attr, ctrl) {
-
+
+ var unsubscribe = [];
+
if (!scope.serverValidationNameField) {
scope.serverValidationNameField = "Name";
}
@@ -15,9 +17,44 @@
scope.vm = {};
scope.vm.dropdownOpen = false;
scope.vm.currentVariant = "";
-
+ scope.vm.variantsWithError = [];
+ scope.vm.defaultVariant = null;
+
+ scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors.
+
+ function checkErrorsOnOtherVariants() {
+ var check = false;
+ angular.forEach(scope.content.variants, function (variant) {
+ if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) {
+ check = true;
+ }
+ });
+ scope.vm.errorsOnOtherVariants = check;
+ }
+
+ function onCultureValidation(valid, errors, allErrors, culture) {
+ var index = scope.vm.variantsWithError.indexOf(culture);
+ if(valid === true) {
+ if (index !== -1) {
+ scope.vm.variantsWithError.splice(index, 1);
+ }
+ } else {
+ if (index === -1) {
+ scope.vm.variantsWithError.push(culture);
+ }
+ }
+ checkErrorsOnOtherVariants();
+ }
+
function onInit() {
+ // find default.
+ angular.forEach(scope.content.variants, function (variant) {
+ if (variant.language.isDefault) {
+ scope.vm.defaultVariant = variant;
+ }
+ });
+
setCurrentVariant();
angular.forEach(scope.content.apps, (app) => {
@@ -26,12 +63,22 @@
}
});
+
+ angular.forEach(scope.content.variants, function (variant) {
+ unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation));
+ });
+
+ unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation));
+
+
+
}
function setCurrentVariant() {
angular.forEach(scope.content.variants, function (variant) {
if (variant.active) {
scope.vm.currentVariant = variant;
+ checkErrorsOnOtherVariants();
}
});
}
@@ -80,9 +127,24 @@
* @param {any} culture
*/
scope.variantIsOpen = function(culture) {
- if(scope.openVariants.indexOf(culture) !== -1) {
+ return (scope.openVariants.indexOf(culture) !== -1);
+ }
+
+ /**
+ * Check whether a variant has a error, used to display errors in variant switcher.
+ * @param {any} culture
+ */
+ scope.variantHasError = function(culture) {
+ // if we are looking for the default language we also want to check for invariant.
+ if (culture === scope.vm.defaultVariant.language.culture) {
+ if(scope.vm.variantsWithError.indexOf("invariant") !== -1) {
+ return true;
+ }
+ }
+ if(scope.vm.variantsWithError.indexOf(culture) !== -1) {
return true;
}
+ return false;
}
onInit();
@@ -103,6 +165,12 @@
}
});
}
+
+ scope.$on('$destroy', function () {
+ for (var u in unsubscribe) {
+ unsubscribe[u]();
+ }
+ });
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
index eb5fd055eb..77f2ffb54a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
@@ -101,8 +101,14 @@ angular.module('umbraco.directives')
var eventBindings = [];
function oneTimeClick(event) {
+ var el = event.target.nodeName;
+
+ //ignore link and button clicks
+ var els = ["INPUT","A","BUTTON"];
+ if(els.indexOf(el) >= 0){return;}
+
// ignore clicks on new overlay
- var parents = $(event.target).parents(".umb-overlay,.umb-tour");
+ var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour");
if(parents.length > 0){
return;
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js
index bbbdd392b2..396699866c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js
@@ -505,6 +505,7 @@
property.showOnMemberProfile = propertyModel.showOnMemberProfile;
property.memberCanEdit = propertyModel.memberCanEdit;
property.isSensitiveValue = propertyModel.isSensitiveValue;
+ property.allowCultureVariant = propertyModel.allowCultureVariant;
// update existing data types
if(model.updateSameDataTypes) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
index 61f7f039d9..39d903d85e 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
@@ -12,27 +12,50 @@
function valPropertyMsg(serverValidationManager) {
return {
- require: ['^^form', '^^valFormManager', '^^umbProperty'],
+ require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'],
replace: true,
restrict: "E",
template: "{{errorMsg}}
",
scope: {},
link: function (scope, element, attrs, ctrl) {
+
+ var unsubscribe = [];
+ var watcher = null;
+ var hasError = false;
+ //create properties on our custom scope so we can use it in our template
+ scope.errorMsg = "";
+
+
//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 variants controller api
+ var umbVariantCtrl = ctrl[3];
- scope.currentProperty = umbPropCtrl.property;
+ var currentProperty = umbPropCtrl.property;
+ scope.currentProperty = currentProperty;
+ var currentCulture = currentProperty.culture;
- //if the property is invariant (no culture), then we will explicitly set it to the string 'invariant'
- //since this matches the value that the server will return for an invariant property.
- var currentCulture = scope.currentProperty.culture || "invariant";
+ if (umbVariantCtrl) {
+ //if we are inside of an umbVariantContent directive
- var watcher = null;
+ var currentVariant = umbVariantCtrl.editor.content;
+
+ // Lets check if we have variants and we are on the default language then ...
+ if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) {
+ //This property is locked cause its a invariant property shown on a non-default language.
+ //Therefor do not validate this field.
+ return;
+ }
+ }
+
+ // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
+ currentCulture = currentCulture || "invariant";
+
// Gets the error message to display
function getErrorMsg() {
@@ -138,13 +161,6 @@ function valPropertyMsg(serverValidationManager) {
}
- var hasError = false;
-
- //create properties on our custom scope so we can use it in our template
- scope.errorMsg = "";
-
- var unsubscribe = [];
-
//listen for form validation changes.
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
// subscribing to this single watch.
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js
index eb522fe783..a0cc7e3033 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js
@@ -7,7 +7,7 @@
**/
function valServer(serverValidationManager) {
return {
- require: ['ngModel', '?^^umbProperty'],
+ require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent'],
restrict: "A",
scope: {},
link: function (scope, element, attr, ctrls) {
@@ -18,9 +18,29 @@ function valServer(serverValidationManager) {
//we cannot proceed, this validator will be disabled
return;
}
+
+ // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
+ var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null;
var currentProperty = umbPropCtrl.property;
var currentCulture = currentProperty.culture;
+
+ if (umbVariantCtrl) {
+ //if we are inside of an umbVariantContent directive
+
+ var currentVariant = umbVariantCtrl.editor.content;
+
+ // Lets check if we have variants and we are on the default language then ...
+ if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) {
+ //This property is locked cause its a invariant property shown on a non-default language.
+ //Therefor do not validate this field.
+ return;
+ }
+ }
+
+ // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
+ currentCulture = currentCulture || "invariant";
+
var watcher = null;
var unsubscribe = [];
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
index b6bcafbddf..f8d95ef1b3 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
@@ -184,8 +184,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]);
}
- }
- else {
+ } else {
//Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
// Groups[0].Properties[2].Alias
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js
index 43022d0e86..b9bfa51122 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js
@@ -13,12 +13,15 @@ function serverValidationManager($timeout) {
var callbacks = [];
/** calls the callback specified with the errors specified, used internally */
- function executeCallback(self, errorsForCallback, callback) {
+ function executeCallback(self, errorsForCallback, callback, culture) {
callback.apply(self, [
- false, //pass in a value indicating it is invalid
- errorsForCallback, //pass in the errors for this item
- self.items]); //pass in all errors in total
+ false, // pass in a value indicating it is invalid
+ errorsForCallback, // pass in the errors for this item
+ self.items, // pass in all errors in total
+ culture // pass the culture that we are listing for.
+ ]
+ );
}
function getFieldErrors(self, fieldName) {
@@ -28,7 +31,7 @@ function serverValidationManager($timeout) {
//find errors for this field name
return _.filter(self.items, function (item) {
- return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
+ return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
}
@@ -39,27 +42,50 @@ function serverValidationManager($timeout) {
if (fieldName && !angular.isString(fieldName)) {
throw "fieldName must be a string";
}
+
+ if (!culture) {
+ culture = "invariant";
+ }
//find all errors for this property
- return _.filter(self.items, function (item) {
+ return _.filter(self.items, function (item) {
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
}
+
+ function getCultureErrors(self, culture) {
+
+ if (!culture) {
+ culture = "invariant";
+ }
+
+ //find all errors for this property
+ return _.filter(self.items, function (item) {
+ return (item.culture === culture);
+ });
+ }
function notifyCallbacks(self) {
for (var cb in callbacks) {
- if (callbacks[cb].propertyAlias === null) {
+ if (callbacks[cb].propertyAlias === null && callbacks[cb].fieldName !== null) {
//its a field error callback
var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName);
if (fieldErrors.length > 0) {
- executeCallback(self, fieldErrors, callbacks[cb].callback);
+ executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture);
}
}
- else {
+ else if (callbacks[cb].propertyAlias != null) {
//its a property error
var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName);
if (propErrors.length > 0) {
- executeCallback(self, propErrors, callbacks[cb].callback);
+ executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture);
+ }
+ }
+ else {
+ //its a culture error
+ var cultureErrors = getCultureErrors(self, callbacks[cb].culture);
+ if (cultureErrors.length > 0) {
+ executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture);
}
}
}
@@ -130,11 +156,14 @@ function serverValidationManager($timeout) {
}
var id = String.CreateGuid();
-
+ if (!culture) {
+ culture = "invariant";
+ }
+
if (propertyAlias === null) {
callbacks.push({
propertyAlias: null,
- culture: null,
+ culture: culture,
fieldName: fieldName,
callback: callback,
id: id
@@ -142,12 +171,11 @@ function serverValidationManager($timeout) {
}
else if (propertyAlias !== undefined) {
//normalize culture to null
- if (!culture) {
- culture = null;
- }
+
callbacks.push({
propertyAlias: propertyAlias,
- culture: culture, fieldName: fieldName,
+ culture: culture,
+ fieldName: fieldName,
callback: callback,
id: id
});
@@ -173,21 +201,20 @@ function serverValidationManager($timeout) {
*/
unsubscribe: function (propertyAlias, culture, fieldName) {
+ //normalize culture to null
+ if (!culture) {
+ culture = "invariant";
+ }
+
if (propertyAlias === null) {
//remove all callbacks for the content field
callbacks = _.reject(callbacks, function (item) {
- return item.propertyAlias === null && item.culture === null && item.fieldName === fieldName;
+ return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName;
});
}
else if (propertyAlias !== undefined) {
-
- //normalize culture to null
- if (!culture) {
- culture = null;
- }
-
//remove all callbacks for the content property
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === propertyAlias && item.culture === culture &&
@@ -213,7 +240,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
- culture = null;
+ culture = "invariant";
}
var found = _.filter(callbacks, function (item) {
@@ -235,7 +262,24 @@ 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.culture === null && item.fieldName === fieldName);
+ return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
+ });
+ return found;
+ },
+
+ /**
+ * @ngdoc function
+ * @name getCultureCallbacks
+ * @methodOf umbraco.services.serverValidationManager
+ * @function
+ *
+ * @description
+ * Gets all callbacks that has been registered using the subscribe method for the culture.
+ */
+ getCultureCallbacks: function (culture) {
+ var found = _.filter(callbacks, function (item) {
+ //returns any callback that have been registered directly/ONLY against the culture
+ return (item.culture === culture && item.propertyAlias === null && item.fieldName === null);
});
return found;
},
@@ -258,7 +302,7 @@ function serverValidationManager($timeout) {
if (!this.hasFieldError(fieldName)) {
this.items.push({
propertyAlias: null,
- culture: null,
+ culture: "invariant",
fieldName: fieldName,
errorMsg: errorMsg
});
@@ -270,7 +314,7 @@ function serverValidationManager($timeout) {
var cbs = this.getFieldCallbacks(fieldName);
//call each callback for this error
for (var cb in cbs) {
- executeCallback(this, errorsForCallback, cbs[cb].callback);
+ executeCallback(this, errorsForCallback, cbs[cb].callback, null);
}
},
@@ -288,9 +332,9 @@ function serverValidationManager($timeout) {
return;
}
- //normalize culture to null
+ //normalize culture to "invariant"
if (!culture) {
- culture = null;
+ culture = "invariant";
}
//only add the item if it doesn't exist
@@ -309,9 +353,16 @@ function serverValidationManager($timeout) {
var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName);
//call each callback for this error
for (var cb in cbs) {
- executeCallback(this, errorsForCallback, cbs[cb].callback);
+ executeCallback(this, errorsForCallback, cbs[cb].callback, culture);
}
- },
+
+ //execute culture specific callbacks here too when a propery error is added
+ var cultureCbs = this.getCultureCallbacks(culture);
+ //call each callback for this error
+ for (var cb in cultureCbs) {
+ executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture);
+ }
+ },
/**
* @ngdoc function
@@ -330,7 +381,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
- culture = null;
+ culture = "invariant";
}
//remove the item
@@ -384,7 +435,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
- culture = null;
+ culture = "invariant";
}
var err = _.find(this.items, function (item) {
@@ -406,7 +457,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.culture === null && item.fieldName === fieldName);
+ return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
return err;
},
@@ -424,7 +475,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
- culture = null;
+ culture = "invariant";
}
var err = _.find(this.items, function (item) {
@@ -446,11 +497,33 @@ 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.culture === null && item.fieldName === fieldName);
+ return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
return err ? true : false;
},
+
+ /**
+ * @ngdoc function
+ * @name hasCultureError
+ * @methodOf umbraco.services.serverValidationManager
+ * @function
+ *
+ * @description
+ * Checks if the given culture has an error
+ */
+ hasCultureError: function (culture) {
+
+ //normalize culture to null
+ if (!culture) {
+ culture = "invariant";
+ }
+
+ var err = _.find(this.items, function (item) {
+ return item.culture === culture;
+ });
+ return err ? true : false;
+ },
/** The array of error messages */
items: []
};
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less
index 21d459c598..52dd7ea678 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less
@@ -174,6 +174,8 @@ a.umb-editor-header__close-split-view:hover {
max-width: 50%;
white-space: nowrap;
+ user-select: none;
+
span {
text-overflow: ellipsis;
overflow: hidden;
@@ -189,6 +191,25 @@ a.umb-variant-switcher__toggle {
color: @ui-action-discreet-type-hover;
}
}
+
+ &.--error {
+ &::before {
+ content: '!';
+ position: absolute;
+ top: -8px;
+ right: -10px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ text-align: center;
+ font-weight: bold;
+ background-color: @errorBackground;
+ color: @errorText;
+ }
+ }
}
.umb-variant-switcher__expand {
@@ -205,20 +226,16 @@ a.umb-variant-switcher__toggle {
align-items: center;
border-bottom: 1px solid @gray-9;
position: relative;
- &:hover .umb-variant-switcher__name-wrapper {
-
- }
}
.umb-variant-switcher__item:last-child {
border-bottom: none;
}
-.umb-variant-switcher_item--current {
+.umb-variant-switcher__item.--current {
color: @ui-light-active-type;
}
-.umb-variant-switcher_item--current .umb-variant-switcher__name-wrapper {
- //background-color: @gray-10;
+.umb-variant-switcher__item.--current .umb-variant-switcher__name-wrapper {
border-left: 4px solid @ui-active;
}
@@ -227,7 +244,7 @@ a.umb-variant-switcher__toggle {
outline: none;
}
-.umb-variant-switcher_item--not-allowed:not(.umb-variant-switcher_item--current) .umb-variant-switcher__name-wrapper:hover {
+.umb-variant-switcher__item.--not-allowed:not(.--current) .umb-variant-switcher__name-wrapper:hover {
//background-color: @white !important;
cursor: default;
}
@@ -237,6 +254,29 @@ a.umb-variant-switcher__toggle {
cursor: pointer;
}
+.umb-variant-switcher__item.--error {
+ .umb-variant-switcher__name {
+ color: @red;
+ &::after {
+ content: '!';
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 5px;
+ top: -6px;
+ width: 14px;
+ height: 14px;
+ border-radius: 7px;
+ font-size: 8px;
+ text-align: center;
+ font-weight: bold;
+ background-color: @errorBackground;
+ color: @errorText;
+ }
+ }
+}
+
.umb-variant-switcher__name-wrapper {
font-size: 14px;
flex: 1;
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html
index 0f7a61fa01..84bab1fe7a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html
@@ -29,7 +29,7 @@
autocomplete="off" maxlength="255" />
-
+
{{vm.currentVariant.language.name}}
@@ -39,7 +39,7 @@
-
+
{{variant.language.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
index 3c34890fa9..3ed5f49b92 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
@@ -20,6 +20,13 @@ angular.module("umbraco")
editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize;
}
+ var width = editorConfig.dimensions ? parseInt(editorConfig.dimensions.width, 10) || null : null;
+ var height = editorConfig.dimensions ? parseInt(editorConfig.dimensions.height, 10) || null : null;
+
+ $scope.containerWidth = editorConfig.mode === "distraction-free" ? (width ? width : "auto") : "auto";
+ $scope.containerHeight = editorConfig.mode === "distraction-free" ? (height ? height : "auto") : "auto";
+ $scope.containerOverflow = editorConfig.mode === "distraction-free" ? (height ? "auto" : "inherit") : "inherit";
+
var promises = [];
//queue file loading
@@ -41,10 +48,16 @@ angular.module("umbraco")
$q.all(promises).then(function (result) {
var standardConfig = result[promises.length - 1];
-
+
+ if (height !== null) {
+ standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1);
+ }
+
//create a baseline Config to extend upon
var baseLineConfigObj = {
- maxImageSize: editorConfig.maxImageSize
+ maxImageSize: editorConfig.maxImageSize,
+ width: width,
+ height: height
};
angular.extend(baseLineConfigObj, standardConfig);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
index a68bfe978e..7dde17bb06 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
@@ -1,6 +1,6 @@
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js
index 3f953fffcd..966731f0f7 100644
--- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js
+++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js
@@ -22,6 +22,7 @@
expect(err.propertyAlias).toBeNull();
expect(err.fieldName).toEqual("Name");
expect(err.errorMsg).toEqual("Required");
+ expect(err.culture).toEqual("invariant");
});
it('will return null for a non-existing field error', function () {
@@ -72,11 +73,43 @@
expect(err1.propertyAlias).toEqual("myProperty");
expect(err1.fieldName).toEqual("value1");
expect(err1.errorMsg).toEqual("Some value 1");
+ expect(err1.culture).toEqual("invariant");
expect(err2).not.toBeUndefined();
expect(err2.propertyAlias).toEqual("myProperty");
expect(err2.fieldName).toEqual("value2");
expect(err2.errorMsg).toEqual("Another value 2");
+ expect(err2.culture).toEqual("invariant");
+ });
+
+ it('can retrieve property validation errors for a sub field for culture', function () {
+
+ //arrange
+ serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1");
+ serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2");
+
+ //act
+ var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1");
+ var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2");
+ var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2");
+ var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2");
+
+
+ //assert
+ expect(err1NotFound).toBeUndefined();
+ expect(err2NotFound).toBeUndefined();
+
+ expect(err1).not.toBeUndefined();
+ expect(err1.propertyAlias).toEqual("myProperty");
+ expect(err1.fieldName).toEqual("value1");
+ expect(err1.errorMsg).toEqual("Some value 1");
+ expect(err1.culture).toEqual("en-US");
+
+ expect(err2).not.toBeUndefined();
+ expect(err2.propertyAlias).toEqual("myProperty");
+ expect(err2.fieldName).toEqual("value2");
+ expect(err2.errorMsg).toEqual("Another value 2");
+ expect(err2.culture).toEqual("fr-FR");
});
it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () {
@@ -93,6 +126,7 @@
expect(err.propertyAlias).toEqual("myProperty");
expect(err.fieldName).toEqual("value1");
expect(err.errorMsg).toEqual("Some value 1");
+ expect(err.culture).toEqual("invariant");
});
it('will return null for a non-existing property error', function () {
@@ -162,6 +196,26 @@
});
+ describe('managing culture validation errors', function () {
+
+ it('can retrieve culture validation errors', function () {
+
+ //arrange
+ serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
+ serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2");
+ serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2");
+ serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3");
+
+ //assert
+ expect(serverValidationManager.hasCultureError(null)).toBe(true);
+ expect(serverValidationManager.hasCultureError("en-US")).toBe(true);
+ expect(serverValidationManager.hasCultureError("fr-FR")).toBe(true);
+ expect(serverValidationManager.hasCultureError("es-ES")).toBe(false);
+
+ });
+
+ });
+
describe('validation error subscriptions', function() {
it('can subscribe to a field error', function() {
@@ -267,6 +321,45 @@
// if the property has errors existing.
expect(numCalled).toEqual(3);
});
+
+ it('can subscribe to a culture error for both a property and its sub field', function () {
+ var args1;
+ var args2;
+ var numCalled = 0;
+
+ //arrange
+ serverValidationManager.subscribe(null, "en-US", null, function (isValid, propertyErrors, allErrors) {
+ numCalled++;
+ args1 = {
+ isValid: isValid,
+ propertyErrors: propertyErrors,
+ allErrors: allErrors
+ };
+ });
+
+ serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) {
+ numCalled++;
+ args2 = {
+ isValid: isValid,
+ propertyErrors: propertyErrors,
+ allErrors: allErrors
+ };
+ });
+
+ //act
+ serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1");
+ serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1");
+ serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2");
+ serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3");
+
+ //assert
+ expect(args1).not.toBeUndefined();
+ expect(args1.isValid).toBe(false);
+
+ expect(args2).toBeUndefined();
+
+ expect(numCalled).toEqual(2);
+ });
// TODO: Finish testing the rest!
diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
index e62438cc6f..9b8a098e21 100644
--- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
+++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
@@ -304,9 +304,12 @@ namespace Umbraco.Web.Editors
//check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type
//always filter these 0 types out
var allowItselfAsChild = false;
+ var allowIfselfAsChildSortOrder = -1;
if (contentTypeSave.AllowedContentTypes != null)
{
+ allowIfselfAsChildSortOrder = contentTypeSave.AllowedContentTypes.IndexOf(0);
allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0);
+
contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList();
}
@@ -335,10 +338,12 @@ namespace Umbraco.Web.Editors
saveContentType(newCt);
//we need to save it twice to allow itself under itself.
- if (allowItselfAsChild)
+ if (allowItselfAsChild && newCt != null)
{
- //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage
- newCt.AddContentType(newCt);
+ newCt.AllowedContentTypes =
+ newCt.AllowedContentTypes.Union(
+ new []{ new ContentTypeSort(newCt.Id, allowIfselfAsChildSortOrder) }
+ );
saveContentType(newCt);
}
return newCt;
diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs
index 3bbcdfb0ac..706ebf2825 100644
--- a/src/Umbraco.Web/ModelStateExtensions.cs
+++ b/src/Umbraco.Web/ModelStateExtensions.cs
@@ -148,11 +148,33 @@ namespace Umbraco.Web
var delimitedParts = string.Join(".", parts);
foreach (var memberName in result.MemberNames)
{
- modelState.AddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage);
+ modelState.TryAddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage);
withNames = true;
}
if (!withNames)
- modelState.AddModelError($"{delimitedParts}", result.ErrorMessage);
+ {
+ modelState.TryAddModelError($"{delimitedParts}", result.ErrorMessage);
+ }
+
+ }
+
+ ///
+ /// Will add an error to model state for a key if that key and error don't already exist
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool TryAddModelError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string key, string errorMsg)
+ {
+ if (modelState.TryGetValue(key, out var errs))
+ {
+ foreach(var e in errs.Errors)
+ if (e.ErrorMessage == errorMsg) return false; //if this same error message exists for the same key, just exit
+ }
+
+ modelState.AddModelError(key, errorMsg);
+ return true;
}
public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)
diff --git a/src/Umbraco.Web/Routing/IContentFinder.cs b/src/Umbraco.Web/Routing/IContentFinder.cs
index fe5699ac7b..2e388c4814 100644
--- a/src/Umbraco.Web/Routing/IContentFinder.cs
+++ b/src/Umbraco.Web/Routing/IContentFinder.cs
@@ -8,9 +8,9 @@ namespace Umbraco.Web.Routing
///
/// Tries to find and assign an Umbraco document to a PublishedRequest.
///
- /// The PublishedRequest.
+ /// The PublishedRequest.
/// A value indicating whether an Umbraco document was found and assigned.
/// Optionally, can also assign the template or anything else on the document request, although that is not required.
- bool TryFindContent(PublishedRequest frequest);
+ bool TryFindContent(PublishedRequest request);
}
}