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